From b9cfdea76c99548ec90033be58ed283fc191587b Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Sun, 30 Mar 2025 01:45:28 -0500 Subject: [PATCH] [Zone] Zone State Automated Testing and Improvements (#4808) * [Zone] Zone State Automated Testing and Improvements * Spawn condition * Update zone.cpp * Remove redundant logic * Update zone_state.cpp * TestZLocationDrift * Protect NPC resumed NPC's from being able to die --- common/timer.cpp | 22 + common/timer.h | 1 + utils/scripts/build/linux-build.sh | 1 + zone/CMakeLists.txt | 2 + zone/attack.cpp | 8 +- zone/cli/tests/_test_util.cpp | 57 + zone/cli/{ => tests}/databuckets.cpp | 18 +- zone/cli/{ => tests}/npc_handins.cpp | 19 +- .../{ => tests}/npc_handins_multiquest.cpp | 6 +- zone/cli/tests/zone_state.cpp | 1094 +++++++++++++++++ zone/entity.cpp | 11 + zone/entity.h | 1 + zone/mob.h | 2 +- zone/npc.cpp | 35 - zone/npc.h | 16 +- zone/spawn2.cpp | 14 +- zone/spawn2.h | 2 + zone/zone.cpp | 7 +- zone/zone_cli.cpp | 22 +- zone/zone_cli.h | 9 +- zone/zone_loot.cpp | 12 +- zone/zone_save_state.cpp | 73 +- zone/zone_save_state.h | 48 + 23 files changed, 1312 insertions(+), 168 deletions(-) create mode 100644 zone/cli/tests/_test_util.cpp rename zone/cli/{ => tests}/databuckets.cpp (95%) rename zone/cli/{ => tests}/npc_handins.cpp (96%) rename zone/cli/{ => tests}/npc_handins_multiquest.cpp (96%) create mode 100644 zone/cli/tests/zone_state.cpp create mode 100644 zone/zone_save_state.h diff --git a/common/timer.cpp b/common/timer.cpp index 280b82518..191074618 100644 --- a/common/timer.cpp +++ b/common/timer.cpp @@ -196,3 +196,25 @@ const uint32 Timer::SetCurrentTime() return current_time; } +const uint32 Timer::RollForward(uint32 seconds) +{ + struct timeval read_time{}; + uint32 this_time; + + gettimeofday(&read_time, nullptr); + this_time = read_time.tv_sec * 1000 + read_time.tv_usec / 1000; + + if (last_time == 0) { + current_time = 0; + } + else { + current_time += this_time - last_time; + } + + last_time = this_time; + + // Roll forward the specified number of seconds (converted to milliseconds) + current_time += seconds * 1000; + + return current_time; +} diff --git a/common/timer.h b/common/timer.h index 599f7b2f2..cd550c9df 100644 --- a/common/timer.h +++ b/common/timer.h @@ -51,6 +51,7 @@ public: inline uint32 GetDuration() { return(timer_time); } static const uint32 SetCurrentTime(); + static const uint32 RollForward(uint32 seconds); static const uint32 GetCurrentTime(); static const uint32 GetTimeSeconds(); diff --git a/utils/scripts/build/linux-build.sh b/utils/scripts/build/linux-build.sh index 863391b40..414ebda6d 100755 --- a/utils/scripts/build/linux-build.sh +++ b/utils/scripts/build/linux-build.sh @@ -57,6 +57,7 @@ echo "# Running NPC hand-in tests" ./bin/zone tests:npc-handins 2>&1 | tee test_output.log ./bin/zone tests:npc-handins-multiquest 2>&1 | tee -a test_output.log ./bin/zone tests:databuckets 2>&1 | tee -a test_output.log +./bin/zone tests:zone-state 2>&1 | tee -a test_output.log if grep -E -q "QueryErr|Error|FAILED" test_output.log; then echo "Error found in test output! Failing build." diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index 7f3ecb5a8..35a9203bf 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -173,6 +173,7 @@ SET(zone_sources zone_event_scheduler.cpp zone_npc_factions.cpp zone_reload.cpp + zone_save_state.cpp zoning.cpp ) @@ -292,6 +293,7 @@ SET(zone_headers zonedump.h zone_cli.h zone_reload.h + zone_save_state.h zone_cli.cpp) ADD_EXECUTABLE(zone ${zone_sources} ${zone_headers}) diff --git a/zone/attack.cpp b/zone/attack.cpp index 6ed5d568d..78c7334f2 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -2507,6 +2507,12 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy return false; } + if (m_resumed_from_zone_suspend && !IsQueuedForCorpse()) { + LogInfo("NPC [{}] is resumed from zone suspend, cannot kill until zone resume is complete.", GetCleanName()); + SetHP(0); + return false; + } + if (IsMultiQuestEnabled()) { for (auto &i: m_hand_in.items) { if (i.is_multiquest_item && i.item->GetItem()->NoDrop != 0) { @@ -2627,7 +2633,7 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy bool pet_owner_is_client = give_exp->IsPet() && owner->IsClient(); bool pet_owner_is_bot = give_exp->IsPet() && owner->IsBot(); bool owner_is_client = owner->IsClient(); - + bool is_in_same_group_or_raid = ( pet_owner_is_client || (pet_owner_is_bot && owner->IsInGroupOrRaid(ulimate_owner)) || diff --git a/zone/cli/tests/_test_util.cpp b/zone/cli/tests/_test_util.cpp new file mode 100644 index 000000000..eba6aa01e --- /dev/null +++ b/zone/cli/tests/_test_util.cpp @@ -0,0 +1,57 @@ +#include "../../zone.h" + +inline void RunTest(const std::string &test_name, const std::string &expected, const std::string &actual) +{ + if (expected == actual) { + std::cout << "[✅] " << test_name << " PASSED\n"; + } else { + std::cerr << "[❌] " << test_name << " FAILED\n"; + std::cerr << " 📌 Expected: " << expected << "\n"; + std::cerr << " ❌ Got: " << actual << "\n"; + std::exit(1); + } +} + +inline void RunTest(const std::string &test_name, bool expected, bool actual) +{ + if (expected == actual) { + std::cout << "[✅] " << test_name << " PASSED\n"; + } + else { + std::cerr << "[❌] " << test_name << " FAILED\n"; + std::cerr << " 📌 Expected: " << (expected ? "true" : "false") << "\n"; + std::cerr << " ❌ Got: " << (actual ? "true" : "false") << "\n"; + std::exit(1); + } +} + +inline void RunTest(const std::string &test_name, int expected, int actual) +{ + if (expected == actual) { + std::cout << "[✅] " << test_name << " PASSED\n"; + } + else { + std::cerr << "[❌] " << test_name << " FAILED\n"; + std::cerr << " 📌 Expected: " << expected << "\n"; + std::cerr << " ❌ Got: " << actual << "\n"; + std::exit(1); + } +} + +extern Zone *zone; + +inline void SetupZone(std::string zone_short_name, uint32 instance_id = 0) { + LogSys.SilenceConsoleLogging(); + + LogSys.log_settings[Logs::ZoneState].log_to_console = std::getenv("DEBUG") ? 3 : 0; + LogSys.log_settings[Logs::Info].log_to_console = std::getenv("DEBUG") ? 3 : 0; + LogSys.log_settings[Logs::Spawns].log_to_console = std::getenv("DEBUG") ? 3 : 0; + + // boot shell zone for testing + Zone::Bootup(ZoneID(zone_short_name), 0, false); + zone->StopShutdownTimer(); + entity_list.Process(); + entity_list.MobProcess(); + + LogSys.EnableConsoleLogging(); +} diff --git a/zone/cli/databuckets.cpp b/zone/cli/tests/databuckets.cpp similarity index 95% rename from zone/cli/databuckets.cpp rename to zone/cli/tests/databuckets.cpp index e0656c5de..0c61cb393 100644 --- a/zone/cli/databuckets.cpp +++ b/zone/cli/tests/databuckets.cpp @@ -1,25 +1,13 @@ #include "../../common/http/httplib.h" #include "../../common/eqemu_logsys.h" #include "../../common/platform.h" -#include "../zone.h" -#include "../client.h" +#include "../../zone.h" +#include "../../client.h" #include "../../common/net/eqstream.h" extern Zone *zone; -void RunTest(const std::string &test_name, const std::string &expected, const std::string &actual) -{ - if (expected == actual) { - std::cout << "[✅] " << test_name << " PASSED\n"; - } else { - std::cerr << "[❌] " << test_name << " FAILED\n"; - std::cerr << " 📌 Expected: " << expected << "\n"; - std::cerr << " ❌ Got: " << actual << "\n"; - std::exit(1); - } -} - -void ZoneCLI::DataBuckets(int argc, char **argv, argh::parser &cmd, std::string &description) +void ZoneCLI::TestDataBuckets(int argc, char **argv, argh::parser &cmd, std::string &description) { if (cmd[{"-h", "--help"}]) { return; diff --git a/zone/cli/npc_handins.cpp b/zone/cli/tests/npc_handins.cpp similarity index 96% rename from zone/cli/npc_handins.cpp rename to zone/cli/tests/npc_handins.cpp index 636d69484..eceaa1d37 100644 --- a/zone/cli/npc_handins.cpp +++ b/zone/cli/tests/npc_handins.cpp @@ -1,8 +1,8 @@ #include "../../common/http/httplib.h" #include "../../common/eqemu_logsys.h" #include "../../common/platform.h" -#include "../zone.h" -#include "../client.h" +#include "../../zone.h" +#include "../../client.h" #include "../../common/net/eqstream.h" #include "../../common/json/json.hpp" @@ -36,19 +36,6 @@ struct TestCase { bool handin_check_result; }; -void RunTest(const std::string &test_name, bool expected, bool actual) -{ - if (expected == actual) { - std::cout << "[✅] " << test_name << " PASSED\n"; - } - else { - std::cerr << "[❌] " << test_name << " FAILED\n"; - std::cerr << " 📌 Expected: " << (expected ? "true" : "false") << "\n"; - std::cerr << " ❌ Got: " << (actual ? "true" : "false") << "\n"; - std::exit(1); - } -} - void RunSerializedTest(const std::string &test_name, const std::string &expected, const std::string &actual) { if (expected == actual) { @@ -75,7 +62,7 @@ std::string SerializeHandin(const std::map &items, const Ha return j.dump(); } -void ZoneCLI::NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &description) +void ZoneCLI::TestNpcHandins(int argc, char **argv, argh::parser &cmd, std::string &description) { if (cmd[{"-h", "--help"}]) { return; diff --git a/zone/cli/npc_handins_multiquest.cpp b/zone/cli/tests/npc_handins_multiquest.cpp similarity index 96% rename from zone/cli/npc_handins_multiquest.cpp rename to zone/cli/tests/npc_handins_multiquest.cpp index 3ea526da1..43f141591 100644 --- a/zone/cli/npc_handins_multiquest.cpp +++ b/zone/cli/tests/npc_handins_multiquest.cpp @@ -1,13 +1,13 @@ #include "../../common/http/httplib.h" #include "../../common/eqemu_logsys.h" #include "../../common/platform.h" -#include "../zone.h" -#include "../client.h" +#include "../../zone.h" +#include "../../client.h" #include "../../common/net/eqstream.h" extern Zone *zone; -void ZoneCLI::NpcHandinsMultiQuest(int argc, char **argv, argh::parser &cmd, std::string &description) +void ZoneCLI::TestNpcHandinsMultiQuest(int argc, char **argv, argh::parser &cmd, std::string &description) { if (cmd[{"-h", "--help"}]) { return; diff --git a/zone/cli/tests/zone_state.cpp b/zone/cli/tests/zone_state.cpp new file mode 100644 index 000000000..62ada2f5f --- /dev/null +++ b/zone/cli/tests/zone_state.cpp @@ -0,0 +1,1094 @@ +extern Zone *zone; + +#include +#include +#include "../../common/repositories/npc_types_repository.h" +#include "../../corpse.h" +#include "../../../common/repositories/respawn_times_repository.h" + +inline void ClearState() +{ + ZoneStateSpawnsRepository::DeleteWhere(database, "zone_id = 32 and instance_id = 0"); +} + +inline std::vector GetStateSpawns() +{ + return ZoneStateSpawnsRepository::GetWhere(database, "zone_id = 32 and instance_id = 0"); +} + +inline void PrintEntityCounts() +{ + std::cout << " NPC count: " << entity_list.GetNPCList().size() << std::endl; + std::cout << " Corpse count: " << entity_list.GetCorpseList().size() << std::endl; +} + +inline void PrintZoneNpcs() +{ + int npc_count = 0; + for (auto &npc: entity_list.GetNPCList()) { + std::cout << npc.second->GetNPCTypeID() << " " << npc.second->GetCleanName() << std::endl; + npc_count++; + } + + std::cout << "Total spawned NPCs: " << npc_count << std::endl; +} + +inline void SetupStateZone() +{ + SetupZone("soldungb"); + zone->Process(); + // depop the zone controller + entity_list.GetNPCByNPCTypeID(ZONE_CONTROLLER_NPC_ID)->Depop(); + entity_list.MobProcess(); // process the depop +} + +inline int GetStateSpawnSpawn2Count() +{ + int count = 0; + for (auto &e: GetStateSpawns()) { + if (e.spawn2_id > 0) { + count++; + } + } + return count; +} + +inline uint32_t SeedLootTable() +{ + const std::string table_name = "zone_state_test"; + + auto entries = LoottableRepository::GetWhere(database, fmt::format("name = '{}'", table_name)); + if (!entries.empty()) { + zone->LoadLootTable(entries[0].id); + return entries[0].id; + } + + // seed the loot table + auto loot_table = LoottableRepository::NewEntity(); + loot_table.name = table_name; + auto inserted = LoottableRepository::InsertOne(database, loot_table); + + auto loot_drop = LootdropRepository::NewEntity(); + loot_drop.name = table_name; + auto inserted_loot_drop = LootdropRepository::InsertOne(database, loot_drop); + + auto loot_table_entries = LoottableEntriesRepository::NewEntity(); + loot_table_entries.lootdrop_id = inserted_loot_drop.id; + loot_table_entries.loottable_id = inserted.id; + LoottableEntriesRepository::InsertOne(database, loot_table_entries); + + auto loot_drop_entries = LootdropEntriesRepository::NewEntity(); + loot_drop_entries.lootdrop_id = inserted_loot_drop.id; + loot_drop_entries.item_id = 11621; // cloak of flames + loot_drop_entries.chance = 100; + LootdropEntriesRepository::InsertOne(database, loot_drop_entries); + + zone->LoadLootTable(inserted.id); + + return inserted.id; +} + +inline std::map GetVariablesDeserialized(const std::string &entity_variables) +{ + std::map deserialized_map; + + if (entity_variables.empty()) { + return deserialized_map; + } + + if (!Strings::IsValidJson(entity_variables)) { + LogZoneState("Invalid JSON data for entity variables"); + return deserialized_map; + } + + try { + std::stringstream ss; + { + ss << entity_variables; + cereal::JSONInputArchive ar(ss); + ar(deserialized_map); + } + } catch (const std::exception &e) { + LogZoneState("Failed to load entity variables [{}]", e.what()); + } + + return deserialized_map; +} + +// MatchState compares the NPC to the state +// it should print what doesn't match when it returns false +inline bool MatchState(NPC *n, const ZoneStateSpawnsRepository::ZoneStateSpawns &state) +{ + if (n->GetNPCTypeID() != state.npc_id) { + std::cout << "NPC ID mismatch: " << n->GetNPCTypeID() << " != " << state.npc_id << std::endl; + return false; + } + + if (n->GetX() != state.x) { + std::cout << "X mismatch: " << n->GetX() << " != " << state.x << std::endl; + return false; + } + + if (n->GetY() != state.y) { + std::cout << "Y mismatch: " << n->GetY() << " != " << state.y << std::endl; + return false; + } + + if (n->GetZ() != state.z) { + std::cout << "Z mismatch: " << n->GetZ() << " != " << state.z << std::endl; + return false; + } + + if (n->GetHeading() != state.heading) { + std::cout << "Heading mismatch: " << n->GetHeading() << " != " << state.heading << std::endl; + return false; + } + + if (n->GetHP() != state.hp) { + std::cout << "HP mismatch: " << n->GetHP() << " != " << state.hp << std::endl; + return false; + } + + if (n->GetMana() != state.mana) { + std::cout << "Mana mismatch: " << n->GetMana() << " != " << state.mana << std::endl; + return false; + } + + if (n->GetEndurance() != state.endurance) { + std::cout << "Endurance mismatch: " << n->GetEndurance() << " != " << state.endurance << std::endl; + return false; + } + + auto entity_variables = GetVariablesDeserialized(state.entity_variables); + + for (const auto &[key, value]: entity_variables) { + if (n->GetEntityVariable(key) != value) { + std::cout << "Variable mismatch: " << key << " " << n->GetEntityVariable(key) << " != " << value + << std::endl; + return false; + } + } + + return true; +} + +// MatchFreshlySpawnedState compares the NPC to the state +// it should print what doesn't match when it returns false +inline bool MatchFreshlySpawnedState(NPC *n, const ZoneStateSpawnsRepository::ZoneStateSpawns &state) +{ + auto npc_type = content_db.LoadNPCTypesData(n->GetNPCTypeID()); + if (!npc_type) { + RunTest("NPC type does not exist", false, true); + } + + if (n->GetX() != state.x) { + std::cout << "X mismatch: " << n->GetX() << " != " << state.x << std::endl; + return false; + } + + if (n->GetY() != state.y) { + std::cout << "Y mismatch: " << n->GetY() << " != " << state.y << std::endl; + return false; + } + +// if (n->GetZ() != state.z) { +// std::cout << "Z mismatch: " << n->GetZ() << " != " << state.z << std::endl; +// return false; +// } + + if (n->GetHeading() != state.heading) { + std::cout << "Heading mismatch: " << n->GetHeading() << " != " << state.heading << std::endl; + return false; + } + + if (n->GetHP() != npc_type->max_hp) { + std::cout << "HP mismatch: " << n->GetHP() << " != " << npc_type->max_hp << std::endl; + return false; + } + + if (n->GetLoottableID() != npc_type->loottable_id) { + std::cout << "Loot table mismatch: " << n->GetLoottableID() << " != " << npc_type->loottable_id << std::endl; + return false; + } + + // not loaded from database +// if (n->GetMana() != npc_type->Mana) { +// std::cout << "Mana mismatch: " << n->GetMana() << " != " << npc_type->Mana << std::endl; +// return false; +// } + + // endurance not saved + + auto entity_variables = GetVariablesDeserialized(state.entity_variables); + + for (const auto &[key, value]: entity_variables) { + if (n->GetEntityVariable(key) != value) { + std::cout << "Variable mismatch: " << key << " " << n->GetEntityVariable(key) << " != " << value + << std::endl; + return false; + } + } + + return true; +} + +inline bool MatchCorpseState(Corpse *c, const ZoneStateSpawnsRepository::ZoneStateSpawns &state) +{ + if (c->GetNPCTypeID() != state.npc_id) { + std::cout << "Corpse NPC ID mismatch: " << c->GetNPCTypeID() << " != " << state.npc_id << std::endl; + return false; + } + + if (c->GetX() != state.x) { + std::cout << "Corpse X mismatch: " << c->GetX() << " != " << state.x << std::endl; + return false; + } + + if (c->GetY() != state.y) { + std::cout << "Corpse Y mismatch: " << c->GetY() << " != " << state.y << std::endl; + return false; + } + +// if (c->GetZ() != state.z) { +// std::cout << "Corpse Z mismatch: " << c->GetZ() << " != " << state.z << std::endl; +// return false; +// } + + if (c->GetHeading() != state.heading) { + std::cout << "Corpse Heading mismatch: " << c->GetHeading() << " != " << state.heading << std::endl; + return false; + } + + if (c->GetDecayTime() != state.decay_in_seconds * 1000) { + std::cout << "Corpse Decay Time mismatch: " << c->GetDecayTime() << " != " << state.decay_in_seconds * 1000 + << std::endl; + return false; + } + + auto entity_variables = GetVariablesDeserialized(state.entity_variables); + + for (const auto &[key, value]: entity_variables) { + if (c->GetEntityVariable(key) != value) { + std::cout << "Corpse Variable mismatch: " << key << " " << c->GetEntityVariable(key) << " != " << value + << std::endl; + return false; + } + } + + return true; +} + +inline void TestSpawns() +{ + zone->Shutdown(); + ClearState(); + SetupStateZone(); + zone->Repop(true); + entity_list.MobProcess(); + +// PrintZoneNpcs(); + + RunTest("Spawns > Ensure no state spawns exist before shutdown", 0, (int) GetStateSpawns().size()); + + zone->Shutdown(); + + auto entries = GetStateSpawns().size(); + RunTest(fmt::format("Spawns > State exists after shutdown, entries ({})", entries), true, entries > 0); + + SetupStateZone(); + + entries = GetStateSpawns().size(); + RunTest(fmt::format("Spawns > State exists after bootup, entries ({})", entries), true, entries > 0); + + zone->Shutdown(); + SetupStateZone(); + + entries = GetStateSpawns().size(); + RunTest( + fmt::format("Spawns > State is the same after shutdown/bootup (2nd time), entries ({})", entries), + true, + entries > 0 + ); + + // need to compare the state spawns to the actual spawns + bool all_exist = true; + + for (auto &state_spawn: GetStateSpawns()) { + auto npc = entity_list.GetNPCByNPCTypeID(state_spawn.npc_id); + if (!npc) { + all_exist = false; + break; + } + } + + RunTest("Spawns > All state spawns (Spawn2) exist in entity list", true, all_exist); + RunTest( + fmt::format("Spawns > All state spawns (Spawn2) exist in entity list, count [{}]", GetStateSpawns().size()), + true, + GetStateSpawns().size() == entity_list.GetNPCList().size() + ); + RunTest( + fmt::format( + "Spawns > Same count of state spawns and entity list | state spawns [{}] entity list [{}]", + GetStateSpawns().size(), + entity_list.GetNPCList().size()), + true, + GetStateSpawns().size() == entity_list.GetNPCList().size() + ); + + // kill all NPC's + std::vector npcs_to_kill; + + // Collect NPCs first + for (const auto &e: entity_list.GetNPCList()) { + if (e.second) { + npcs_to_kill.push_back(e.second); + } + } + + // Now safely process them + for (auto *npc: npcs_to_kill) { + npc->SetQueuedToCorpse(); + npc->Death(npc, npc->GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); + } + + bool condition = (int) entity_list.GetNPCList().size() == 0 && (int) entity_list.GetCorpseList().size() == 115; + RunTest("Spawns > All NPC's killed (0 NPCs) (115 Corpses)", true, condition); + + std::vector spawn2_ids = {}; + + for (auto &e: GetStateSpawns()) { + if (e.spawn2_id > 0) { + spawn2_ids.push_back(e.spawn2_id); + } + } + + auto times = RespawnTimesRepository::GetWhere( + database, + fmt::format("id IN ({})", Strings::Join(spawn2_ids, ",")) + ); + + for (auto &e: times) { + e.duration = 1; + e.start = std::time(nullptr) + 1; + } + RespawnTimesRepository::ReplaceMany(database, times); + + zone->Shutdown(); + SetupStateZone(); + + condition = (int) entity_list.GetNPCList().size() == 0 && (int) entity_list.GetCorpseList().size() == 115; + if (!condition) { + PrintEntityCounts(); + } + RunTest("Spawns > After restore (0 NPCs) (115 Corpses)", true, condition); + + for (auto &e: entity_list.GetCorpseList()) { + auto c = e.second; + + for (auto &s: GetStateSpawns()) { + bool is_same_corpse = + c->GetNPCTypeID() == s.npc_id && + c->GetX() == s.x && + c->GetY() == s.y && + c->GetHeading() == s.heading; + + if (is_same_corpse) { + if (!MatchCorpseState(c, s)) { + RunTest("Spawns > Corpse state matches state", true, false); + } + } + } + } + + Timer::RollForward(5); + + zone->Process(); + + condition = (int) entity_list.GetNPCList().size() == 115 && (int) entity_list.GetCorpseList().size() == 115; + if (!condition) { + PrintEntityCounts(); + } + RunTest("Spawns > After respawn (115 NPCs) (115 Corpses)", true, condition); + + for (auto &c: entity_list.GetCorpseList()) { + c.second->DepopNPCCorpse(); + } + + entity_list.CorpseProcess(); + + condition = (int) entity_list.GetNPCList().size() == 115 && (int) entity_list.GetCorpseList().size() == 0; + if (!condition) { + PrintEntityCounts(); + } + RunTest("Spawns > After respawn (115 NPCs) (0 Corpses)", true, condition); + + // lets set NPC's up with a predictable loottable for testing + uint32_t loottable_id = SeedLootTable(); + + for (auto &e: entity_list.GetNPCList()) { + auto n = e.second; + n->ClearLootItems(); + n->SetResumedFromZoneSuspend(false); + n->AddLootTable(loottable_id); + n->SetResumedFromZoneSuspend(true); + } + + RespawnTimesRepository::DeleteWhere(database, fmt::format("id IN ({})", Strings::Join(spawn2_ids, ","))); + + zone->Shutdown(); + SetupStateZone(); + + npcs_to_kill = {}; + + // kill only 10 NPCs + int i = 0; + + for (const auto &e: entity_list.GetNPCList()) { + if (e.second) { + npcs_to_kill.push_back(e.second); + i++; + } + if (i == 10) { + break; + } + } + + for (auto *npc: npcs_to_kill) { + if (!npc) { + continue; + } + npc->SetQueuedToCorpse(); + npc->Death(npc, npc->GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); + } + + condition = (int) entity_list.GetNPCList().size() == 105 && (int) entity_list.GetCorpseList().size() == 10; + if (!condition) { + PrintEntityCounts(); + } + RunTest("Spawns > Kill 10 NPC's before save/restore (105 NPCs) (10 Corpses)", true, condition); + + zone->Shutdown(); + SetupStateZone(); + + condition = (int) entity_list.GetNPCList().size() == 105 && (int) entity_list.GetCorpseList().size() == 10; + if (!condition) { + PrintEntityCounts(); + } + RunTest("Spawns > After restore (105 NPCs) (10 Corpses)", true, condition); + + // validate that all corpses and npc's have cloak of flames + bool test_failed = false; + for (auto &e: entity_list.GetCorpseList()) { + auto corpse = e.second; + if (!corpse) { + continue; + } + + bool has_item = corpse->HasItem(11621); + if (!has_item || corpse->CountItem(11621) != 1) { + std::cout << "Corpse does not have item" << std::endl; + std::cout << " -- CountItem(11621) " << corpse->CountItem(11621) << std::endl; + std::cout << " -- GetCleanName " << corpse->GetCleanName() << std::endl; + RunTest( + "Spawns > After restore > Corpse has item (cloak of flames) item count (1) (no dupes)", + true, + has_item + ); + } + } + + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + if (!npc) { + continue; + } + + bool has_item = npc->HasItem(11621); + if (!has_item && npc->CountItem(11621) == 1) { + std::cout << "NPC does not have item" << std::endl; + std::cout << " -- CountItem(11621) " << npc->CountItem(11621) << std::endl; + std::cout << " -- GetCleanName " << npc->GetCleanName() << std::endl; + RunTest( + "Spawns > After restore > NPC has item (cloak of flames) item count (1) (no dupes)", + true, + has_item + ); + } + } + + RunTest( + "Spawns > After restore > All npcs/corpses have item (cloak of flames) item count (1) and no dupes", + true, + true + ); + + // test to make sure the entity variables match what was on state + int npcs_matching_state = 0; + + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + for (auto &s: GetStateSpawns()) { + if (npc->GetSpawnGroupId() == s.spawngroup_id && npc->GetSpawn()->GetID() == s.spawn2_id) { + if (!MatchState(npc, s)) { + RunTest("Spawns > NPC state matches state", true, false); + } + npcs_matching_state++; + } + } + } + + RunTest( + fmt::format("Spawns > After restore > NPC(s) matching state | matching ({})", npcs_matching_state), + false, + false + ); + + // test to make sure the entity variables match what was on state + int corpses_matching_state = 0; + + for (auto &e: entity_list.GetCorpseList()) { + auto c = e.second; + + for (auto &s: GetStateSpawns()) { + bool is_same_corpse = + c->GetNPCTypeID() == s.npc_id && + c->GetX() == s.x && + c->GetY() == s.y && + c->GetHeading() == s.heading; + + if (is_same_corpse) { + if (!MatchCorpseState(c, s)) { + RunTest("Spawns > Corpse state matches state", true, false); + } + corpses_matching_state++; + } + } + } + + RunTest( + fmt::format("Spawns > After restore > Corpse(s) matching state | matching ({})", corpses_matching_state), + false, + false + ); + + entity_list.MobProcess(); + + zone->Process(); + zone->ClearSpawnTimers(); + entity_list.MobProcess(); + + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + if (npc->GetSpawnGroupId() == 0) { + continue; + } + + npc->SetEntityVariable("previously_spawned", "true"); + } + + Timer::RollForward(302401); // longest respawn time in zone + zone->Process(); + entity_list.MobProcess(); // processing depops + + condition = (int) entity_list.GetNPCList().size() == 115 && (int) entity_list.GetCorpseList().size() == 10; + if (!condition) { + PrintEntityCounts(); + PrintZoneNpcs(); + } + RunTest("Spawns > After respawn, ensure we have expected entity counts (115 NPCs) (10 Corpses)", true, condition); + + entity_list.MobProcess(); // processing depops + + npcs_matching_state = 0; + int dropped_items = 0; + + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + + if (npc->GetEntityVariable("previously_spawned") != "true") { + for (auto &s: GetStateSpawns()) { + if (npc->GetSpawnGroupId() == s.spawngroup_id && npc->GetSpawn()->GetID() == s.spawn2_id && + s.npc_id == 0) { + if (!MatchFreshlySpawnedState(npc, s)) { + RunTest("Spawns > NPC state matches state", true, false); + } + + dropped_items += npc->GetLootList().size(); + npcs_matching_state++; + } + } + } + } + + RunTest( + fmt::format( + "Spawns > After restore, after respawn (10 killed) > ensure NPC(s) match state | matching ({})", + npcs_matching_state + ), + npcs_matching_state == 10, + true + ); + + RunTest( + fmt::format( + "Spawns > After restore, after respawn (10 killed) ensure NPC's have loot assigned | dropped items ({})", + dropped_items + ), + dropped_items > 0, + true + ); +} + +inline void TestZoneVariables() +{ + std::vector> test_variables = { + {"test_variable", "test_value"}, + {"test_variable2", "test_value2"} + }; + + // Set variables + for (const auto &[key, value]: test_variables) { + zone->SetVariable(key, value); + RunTest("Zone variables > (" + key + ") set", value, zone->GetVariable(key)); + } + + // Simulate shutdown and restart twice + for (int i = 1; i <= 2; ++i) { + zone->Shutdown(); + SetupStateZone(); + + for (const auto &[key, value]: test_variables) { + RunTest( + "Zone variables > (" + key + ") persists after shutdown/bootup (" + std::to_string(i) + ")", + value, + zone->GetVariable(key) + ); + } + } + + // Delete one variable + zone->DeleteVariable("test_variable"); + + RunTest("Zone variables > (test_variable) delete is empty", "", zone->GetVariable("test_variable")); + RunTest( + "Zone variables > (test_variable2) delete second one still exists", + "test_value2", + zone->GetVariable("test_variable2") + ); + + // Final shutdown and restart check + zone->Shutdown(); + SetupStateZone(); + + for (const auto &[key, value]: test_variables) { + std::string expected_value = (key == "test_variable") ? "" : value; + RunTest("Zone variables > (" + key + ") after shutdown/bootup", expected_value, zone->GetVariable(key)); + } +} + +inline void TestHpManaEnd() +{ + std::vector ids = {}; + + for (auto &npc: entity_list.GetNPCList()) { + ids.push_back(npc.second->GetNPCTypeID()); + } + + auto npc_types = NpcTypesRepository::GetWhere(database, fmt::format(" id IN ({})", Strings::Join(ids, ","))); + + // validate that hp / mana / end equal that of what's on the npc types row for all rows + // dont run tests in the loop, just collect the data + std::vector> hp_mismatch = {}; + std::vector> mana_mismatch = {}; + + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + + for (auto &npc_type: npc_types) { + if (npc->GetNPCTypeID() != npc_type.id) { + continue; + } + + // we need to make sure the values are not 0 because of auto scaling + if (npc->GetHP() != npc_type.hp && npc_type.hp > 0) { + hp_mismatch.emplace_back(npc->GetNPCTypeID(), true); + } + if (npc->GetMana() != npc_type.mana && npc_type.mana > 0) { + mana_mismatch.emplace_back(npc->GetNPCTypeID(), true); + } + } + } + + RunTest( + "HP/Mana/End Save/Restore > Ensure default HP state matches data in npc_types row", + 0, + (int) hp_mismatch.size() + ); + + RunTest( + "HP/Mana/End Save/Restore > Ensure default Mana state matches data in npc_types row", + 0, + (int) mana_mismatch.size() + ); + + // do damage to NPC's and make sure they restore to their original values + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + npc->SetHP(1); + npc->SetMana(1); + } + + zone->Shutdown(); + SetupStateZone(); + + hp_mismatch.clear(); + mana_mismatch.clear(); + + // compare state values versus what's on the entity list NPC object + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + for (auto &state: GetStateSpawns()) { + if (npc->GetNPCTypeID() != state.npc_id) { + continue; + } + + // we need to make sure the values are not 0 because of auto scaling + if (npc->GetHP() != state.hp && state.hp > 0) { + hp_mismatch.emplace_back(npc->GetNPCTypeID(), true); + } + if (npc->GetMana() != state.mana && state.mana > 0) { + mana_mismatch.emplace_back(npc->GetNPCTypeID(), true); + } + } + } + + RunTest( + "HP/Mana/End Save/Restore > Ensure restored HP state matches data on the NPC object", + 0, + (int) hp_mismatch.size() + ); + RunTest( + "HP/Mana/End Save/Restore > Ensure restored Mana state matches data on the NPC object", + 0, + (int) mana_mismatch.size() + ); +} + +inline void TestBuffs() +{ + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + if (npc->GetNPCTypeID() == 0) { + continue; + } + + npc->CastSpell(6824, npc->GetID(), (EQ::spells::CastingSlot) 0, 0, 0); + } + + zone->Shutdown(); + SetupStateZone(); + + // Check buffs + bool missing_buffs = false; + + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + if (npc->GetNPCTypeID() == 0) { + continue; + } + + if (!npc->FindBuff(6824)) { + missing_buffs = true; + break; + } + } + + RunTest("Buffs > Persist after shutdown/bootup", false, missing_buffs); +} + +inline void TestZLocationDrift() +{ + zone->Shutdown(); + ClearState(); + SetupStateZone(); + + auto b = GetStateSpawns(); + + for (int i = 0; i < 10; ++i) { + zone->Shutdown(); + SetupStateZone(); + } + + auto a = GetStateSpawns(); + + // compare entries_before x/y/z to entries_after x/y/z + bool locations_different = false; + + for (size_t i = 0; i < b.size(); ++i) { + if (b[i].x != a[i].x || b[i].y != a[i].y || b[i].z != a[i].z) { + locations_different = true; + + std::cout << "Location drift detected for NPC ID: " << b[i].npc_id << std::endl; + std::cout << "Location drift detected for NPC ID: " << a[i].npc_id << std::endl; + std::cout << "Before - X: " << b[i].x << ", Y: " << b[i].y << ", Z: " << b[i].z << std::endl; + std::cout << "After - X: " << a[i].x << ", Y: " << a[i].y << ", Z: " << a[i].z << std::endl; + break; + } + } + + RunTest( + "Z Location Drift > Ensure Z location does not drift after multiple shutdowns/bootups", + false, + locations_different + ); +} + +inline void TestLocationChange() +{ + // Test that state spawns are where we moved them to if we moved them, move them slightly in a predictable way so + // we can test for it after restore + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + if (!npc) { + continue; + } + + // set all to -870 -1394 106.58 (nagafen area) + npc->SetPosition(-870, -1394, 106); + } + + zone->Shutdown(); + SetupStateZone(); + + bool all_moved = true; + + for (auto &e: entity_list.GetNPCList()) { + for (auto &state: GetStateSpawns()) { + if (e.second->GetNPCTypeID() != state.npc_id) { + continue; + } + + auto n = e.second; + if (n->GetSpawnGroupId() != state.spawngroup_id && n->GetSpawn()->GetID() != state.spawn2_id) { + continue; + } + + // z gets auto adjusted to the ground, so we dont need to check it + if (n->GetX() != state.x || n->GetY() != state.y) { + std::cout << "NPC ID: " << n->GetNPCTypeID() << " X: " << n->GetX() << " Y: " << n->GetY() << std::endl; + std::cout << "State ID " << state.npc_id << " X: " << state.x << " Y: " << state.y << std::endl; + std::cout << "-----------------------------------\n"; + all_moved = false; + break; + } + else { +// std::cout << "NPC ID: " << e.second->GetNPCTypeID() << " X: " << e.second->GetX() << " Y: " << e.second->GetY() << std::endl; +// std::cout << "State ID " << state.npc_id << " X: " << state.x << " Y: " << state.y << std::endl; +// std::cout << "-----------------------------------\n"; + } + } + } + + RunTest("Location > State spawns are where we moved them to after restore", true, all_moved); +} + +inline void TestEntityVariables() +{ + std::map test_entity_variables = { + {"test_entity_variable", "test_entity_value"}, + {"test_entity_variable2", "test_entity_value2"} + }; + + // Set entity variables + for (const auto &[key, value]: test_entity_variables) { + for (auto &e: entity_list.GetNPCList()) { + e.second->SetEntityVariable(key, value); + } + } + + zone->Shutdown(); + SetupStateZone(); + + // Check entity variables + bool missing_entity_variables = false; + for (const auto &[key, value]: test_entity_variables) { + for (auto &e: entity_list.GetNPCList()) { + if (e.second->GetEntityVariable(key) != value) { + missing_entity_variables = true; + break; + } + } + } + + RunTest("Entity Variables > Persist after shutdown/bootup", false, missing_entity_variables); +} + +inline void TestLoot() +{ + uint32_t table_id = SeedLootTable(); + + for (auto &e: entity_list.GetNPCList()) { + e.second->ClearLootItems(); + e.second->SetResumedFromZoneSuspend(false); + e.second->AddLootTable(table_id); + e.second->SetResumedFromZoneSuspend(true); + } + + bool missing_loot = false; + + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + if (npc->GetNPCTypeID() == 0) { + continue; + } + + // cloak of flames + if (npc->CountItem(11621) == 0) { + missing_loot = true; + break; + } + } + + RunTest("Loot > Cloak of Flames added to all NPC's via Loottable before shutdown", false, missing_loot); + + zone->Shutdown(); + SetupStateZone(); + + missing_loot = false; + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + if (npc->GetNPCTypeID() == 0) { + continue; + } + + // cloak of flames + if (npc->CountItem(11621) == 0) { + missing_loot = true; + break; + } + } + + RunTest("Loot > Cloak of Flames added to all NPC's via Loottable after shutdown/bootup", false, missing_loot); + + // make sure no duplicates are added + bool duplicates = false; + + for (auto &e: entity_list.GetNPCList()) { + auto npc = e.second; + if (npc->GetNPCTypeID() == 0) { + continue; + } + + if (npc->CountItem(11621) > 1) { + duplicates = true; + break; + } + } + + RunTest("Loot > No duplicates added when adding item to NPC", false, duplicates); + + // kill all NPC's + std::vector npcs_to_kill; + + // Collect NPCs first + for (const auto &e: entity_list.GetNPCList()) { + if (e.second) { + npcs_to_kill.push_back(e.second); + } + } + + // Now safely process them + for (auto *npc: npcs_to_kill) { + npc->SetQueuedToCorpse(); + npc->Death(npc, npc->GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); + } + + // make sure all of the corpses have "Cloak of Flames" + bool missing_loot_corpse = false; + + for (auto &e: entity_list.GetCorpseList()) { + auto corpse = e.second; + if (corpse->GetNPCTypeID() == 0) { + continue; + } + + if (corpse->CountItem(11621) == 0) { + missing_loot_corpse = true; + break; + } + } + + RunTest( + "Loot > Cloak of Flames added to all Corpse's via Loottable before shutdown/bootup", + false, + missing_loot_corpse + ); + + zone->Shutdown(); + SetupStateZone(); + + missing_loot_corpse = false; + for (auto &e: entity_list.GetCorpseList()) { + auto corpse = e.second; + if (corpse->GetNPCTypeID() == 0) { + continue; + } + + if (corpse->CountItem(11621) == 0) { + missing_loot_corpse = true; + break; + } + } + + RunTest( + "Loot > Cloak of Flames added to all Corpse's via Loottable after shutdown/bootup", + false, + missing_loot_corpse + ); + + // make sure no duplicates are added + bool duplicates_corpse = false; + + for (auto &e: entity_list.GetCorpseList()) { + auto corpse = e.second; + if (corpse->GetNPCTypeID() == 0) { + continue; + } + + if (corpse->CountItem(11621) > 1) { + duplicates_corpse = true; + break; + } + } + + RunTest("Loot > No duplicates added when adding item to Corpse", false, duplicates_corpse); +} + +void ZoneCLI::TestZoneState(int argc, char **argv, argh::parser &cmd, std::string &description) +{ + if (cmd[{"-h", "--help"}]) { + return; + } + + ClearState(); // clean slate + SetupStateZone(); + zone->Repop(true); + + std::cout << "===========================================\n"; + std::cout << "⚙\uFE0F> Running Zone State Tests... (soldungb)\n"; + std::cout << "===========================================\n\n"; + + TestZoneVariables(); + TestHpManaEnd(); + TestBuffs(); + TestLocationChange(); + TestEntityVariables(); + TestLoot(); + TestSpawns(); + TestZLocationDrift(); + + std::cout << "\n===========================================\n"; + std::cout << "✅ All Zone State Tests Completed!\n"; + std::cout << "===========================================\n"; +} diff --git a/zone/entity.cpp b/zone/entity.cpp index 72209dee7..58f33d9fc 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -5991,3 +5991,14 @@ void EntityList::SendMerchantInventory(Mob* m, int32 slot_id, bool is_delete) return; } + +void EntityList::RestoreCorpse(NPC *npc, uint32_t decay_time) +{ + uint16 corpse_id = npc->GetID(); + npc->Death(npc, npc->GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); + auto c = entity_list.GetCorpseByID(corpse_id); + if (c) { + c->UnLock(); + c->SetDecayTimer(decay_time); + } +} diff --git a/zone/entity.h b/zone/entity.h index 2250a5a71..226a306e8 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -580,6 +580,7 @@ public: void SendMerchantEnd(Mob* merchant); void SendMerchantInventory(Mob* m, int32 slot_id = -1, bool is_delete = false); + void RestoreCorpse(NPC* npc, uint32_t decay_time); protected: friend class Zone; diff --git a/zone/mob.h b/zone/mob.h index 070372222..01798720a 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -1125,7 +1125,7 @@ public: virtual void SetAttackTimer(); inline void SetInvul(bool invul) { invulnerable=invul; } - inline bool GetInvul(void) { return invulnerable; } + inline bool GetInvul() { return invulnerable; } void SetExtraHaste(int haste, bool need_to_save = true); inline int GetExtraHaste() { return extra_haste; } virtual int GetHaste(); diff --git a/zone/npc.cpp b/zone/npc.cpp index d3c31ce29..1360d89a1 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -132,8 +132,6 @@ NPC::NPC(const NPCType *npc_type_data, Spawn2 *in_respawn, const glm::vec4 &posi ), attacked_timer(CombatEventTimer_expire), swarm_timer(100), - m_corpse_queue_timer(1000), - m_corpse_queue_shutoff_timer(30000), m_resumed_from_zone_suspend_shutoff_timer(10000), classattack_timer(1000), monkattack_timer(1000), @@ -624,28 +622,6 @@ bool NPC::Process() // zone state corpse creation timer if (RuleB(Zone, StateSavingOnShutdown)) { - // creates a corpse if the NPC is queued for corpse creation - if (m_corpse_queue_timer.Check()) { - if (IsQueuedForCorpse()) { - auto decay_timer = m_corpse_decay_time; - uint16 corpse_id = GetID(); - Death(this, GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); - auto c = entity_list.GetCorpseByID(corpse_id); - if (c) { - c->UnLock(); - c->SetDecayTimer(decay_timer); - } - } - m_corpse_queue_timer.Disable(); - m_corpse_queue_shutoff_timer.Disable(); - } - - // shuts off the corpse queue timer if it is still running - if (m_corpse_queue_shutoff_timer.Check()) { - m_corpse_queue_timer.Disable(); - m_corpse_queue_shutoff_timer.Disable(); - } - // shuts off the temporary spawn protected state of the NPC if (m_resumed_from_zone_suspend_shutoff_timer.Check()) { m_resumed_from_zone_suspend_shutoff_timer.Disable(); @@ -654,17 +630,6 @@ bool NPC::Process() } if (tic_timer.Check()) { - if (RuleB(Zone, StateSavingOnShutdown) && IsQueuedForCorpse()) { - auto decay_timer = m_corpse_decay_time; - uint16 corpse_id = GetID(); - Death(this, GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); - auto c = entity_list.GetCorpseByID(corpse_id); - if (c) { - c->UnLock(); - c->SetDecayTimer(decay_timer); - } - } - if (parse->HasQuestSub(GetNPCTypeID(), EVENT_TICK)) { parse->EventNPC(EVENT_TICK, this, nullptr, "", 0); } diff --git a/zone/npc.h b/zone/npc.h index 57da2830b..31a4e09f9 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -603,10 +603,9 @@ public: // zone state save inline void SetQueuedToCorpse() { m_queued_for_corpse = true; } - inline bool IsQueuedForCorpse() { return m_queued_for_corpse; } - inline uint32_t SetCorpseDecayTime(uint32_t decay_time) { return m_corpse_decay_time = decay_time; } + inline bool IsQueuedForCorpse() const { return m_queued_for_corpse; } inline void SetResumedFromZoneSuspend(bool state = true) { m_resumed_from_zone_suspend = state; } - inline bool IsResumedFromZoneSuspend() { return m_resumed_from_zone_suspend; } + inline bool IsResumedFromZoneSuspend() const { return m_resumed_from_zone_suspend; } inline void LoadBuffsFromState(std::vector in_buffs) { int i = 0; @@ -658,15 +657,12 @@ protected: LootItems m_loot_items; // zone state - bool m_resumed_from_zone_suspend = false; - bool m_queued_for_corpse = false; // this is to check for corpse creation on zone state restore - uint32_t m_corpse_decay_time = 0; // decay time set on zone state restore - Timer m_corpse_queue_timer = {}; // this is to check for corpse creation on zone state restore - Timer m_corpse_queue_shutoff_timer = {}; + bool m_resumed_from_zone_suspend = false; + bool m_queued_for_corpse = false; // this is to check for corpse creation on zone state restore - // this is a 30-second timer that protects a NPC from having double assignment of loot + // this is a timer that protects a NPC from having double assignment of loot // this is to prevent a player from killing a NPC and then zoning out and back in to get loot again - // if loot was to be assigned via script again, this protects double assignment for 30 seconds + // if loot was to be assigned via script again, this protects double assignment for a short time Timer m_resumed_from_zone_suspend_shutoff_timer = {}; std::list faction_list; diff --git a/zone/spawn2.cpp b/zone/spawn2.cpp index aa3d63d84..455064ebc 100644 --- a/zone/spawn2.cpp +++ b/zone/spawn2.cpp @@ -191,16 +191,20 @@ bool Spawn2::Process() { return false; } - uint16 condition_value=1; - + uint16 condition_value = 1; if (condition_id > 0) { - condition_value = zone->spawn_conditions.GetCondition(zone->GetShortName(), zone->GetInstanceID(), condition_id); + condition_value = zone->spawn_conditions.GetCondition( + zone->GetShortName(), + zone->GetInstanceID(), + condition_id + ); } //have the spawn group pick an NPC for us uint32 npcid = 0; - if (RuleB(Zone, StateSavingOnShutdown) && currentnpcid && currentnpcid > 0) { - npcid = currentnpcid; + if (m_resumed_npc_id > 0) { + npcid = m_resumed_npc_id; + m_resumed_npc_id = 0; } else { npcid = spawn_group->GetNPCType(condition_value); } diff --git a/zone/spawn2.h b/zone/spawn2.h index f021b120d..0cb2572ce 100644 --- a/zone/spawn2.h +++ b/zone/spawn2.h @@ -78,6 +78,7 @@ public: inline bool IsResumedFromZoneSuspend() const { return m_resumed_from_zone_suspend; } inline void SetResumedFromZoneSuspend(bool resumed) { m_resumed_from_zone_suspend = resumed; } inline void SetEntityVariables(std::map vars) { m_entity_variables = vars; } + inline void SetResumedNPCID(uint32 npc_id) { m_resumed_npc_id = npc_id; } protected: friend class Zone; @@ -105,6 +106,7 @@ private: bool IsDespawned; uint32 killcount; bool m_resumed_from_zone_suspend = false; + uint32 m_resumed_npc_id = 0; std::map m_entity_variables = {}; }; diff --git a/zone/zone.cpp b/zone/zone.cpp index 45d7003c2..c448bf418 100644 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -887,10 +887,7 @@ void Zone::Shutdown(bool quiet) c.second->WorldKick(); } - bool does_zone_have_entities = - zone && zone->IsLoaded() && - (!entity_list.GetNPCList().empty() || !entity_list.GetCorpseList().empty()); - if (RuleB(Zone, StateSavingOnShutdown) && does_zone_have_entities) { + if (RuleB(Zone, StateSavingOnShutdown) && zone && zone->IsLoaded()) { SaveZoneState(); } @@ -1537,7 +1534,6 @@ bool Zone::Process() { spawn_conditions.Process(); if (spawn2_timer.Check()) { - LinkedListIterator iterator(spawn2_list); EQ::InventoryProfile::CleanDirty(); @@ -3295,5 +3291,4 @@ void Zone::ReloadMaps() pathing = IPathfinder::Load(map_name); } -#include "zone_save_state.cpp" #include "zone_loot.cpp" diff --git a/zone/zone_cli.cpp b/zone/zone_cli.cpp index e7ce5579e..33177e8f6 100644 --- a/zone/zone_cli.cpp +++ b/zone/zone_cli.cpp @@ -29,17 +29,23 @@ void ZoneCLI::CommandHandler(int argc, char **argv) auto function_map = EQEmuCommand::function_map; // Register commands - function_map["benchmark:databuckets"] = &ZoneCLI::BenchmarkDatabuckets; - function_map["sidecar:serve-http"] = &ZoneCLI::SidecarServeHttp; - function_map["tests:databuckets"] = &ZoneCLI::DataBuckets; - function_map["tests:npc-handins"] = &ZoneCLI::NpcHandins; - function_map["tests:npc-handins-multiquest"] = &ZoneCLI::NpcHandinsMultiQuest; + function_map["benchmark:databuckets"] = &ZoneCLI::BenchmarkDatabuckets; + function_map["sidecar:serve-http"] = &ZoneCLI::SidecarServeHttp; + function_map["tests:databuckets"] = &ZoneCLI::TestDataBuckets; + function_map["tests:npc-handins"] = &ZoneCLI::TestNpcHandins; + function_map["tests:npc-handins-multiquest"] = &ZoneCLI::TestNpcHandinsMultiQuest; + function_map["tests:zone-state"] = &ZoneCLI::TestZoneState; EQEmuCommand::HandleMenu(function_map, cmd, argc, argv); } -#include "cli/databuckets.cpp" +// cli #include "cli/benchmark_databuckets.cpp" #include "cli/sidecar_serve_http.cpp" -#include "cli/npc_handins.cpp" -#include "cli/npc_handins_multiquest.cpp" + +// tests +#include "cli/tests/_test_util.cpp" +#include "cli/tests/databuckets.cpp" +#include "cli/tests/npc_handins.cpp" +#include "cli/tests/npc_handins_multiquest.cpp" +#include "cli/tests/zone_state.cpp" diff --git a/zone/zone_cli.h b/zone/zone_cli.h index bbee36f41..7e5e6e81c 100644 --- a/zone/zone_cli.h +++ b/zone/zone_cli.h @@ -1,6 +1,7 @@ #ifndef EQEMU_ZONE_CLI_H #define EQEMU_ZONE_CLI_H +#include #include "../common/cli/argh.h" class ZoneCLI { @@ -11,10 +12,10 @@ public: static bool RanConsoleCommand(int argc, char **argv); static bool RanSidecarCommand(int argc, char **argv); static bool RanTestCommand(int argc, char **argv); - static void DataBuckets(int argc, char **argv, argh::parser &cmd, std::string &description); - static void NpcHandins(int argc, char **argv, argh::parser &cmd, std::string &description); - static void NpcHandinsMultiQuest(int argc, char **argv, argh::parser &cmd, std::string &description); + static void TestDataBuckets(int argc, char **argv, argh::parser &cmd, std::string &description); + static void TestNpcHandins(int argc, char **argv, argh::parser &cmd, std::string &description); + static void TestNpcHandinsMultiQuest(int argc, char **argv, argh::parser &cmd, std::string &description); + static void TestZoneState(int argc, char **argv, argh::parser &cmd, std::string &description); }; - #endif //EQEMU_ZONE_CLI_H diff --git a/zone/zone_loot.cpp b/zone/zone_loot.cpp index b6457d4b6..fb26dd7ec 100644 --- a/zone/zone_loot.cpp +++ b/zone/zone_loot.cpp @@ -368,18 +368,22 @@ void Zone::LoadLootDrops(const std::vector in_lootdrop_ids) m_lootdrops.emplace_back(e); // add lootdrop entries - for (const auto &f: lootdrop_entries) { - if (e.id == f.lootdrop_id) { + // add lootdrop entries + for (const auto &h: lootdrop_entries) { + if (e.id == h.lootdrop_id) { // check if lootdrop entry already exists in memory has_entry = false; - for (const auto &g: m_lootdrop_entries) { - if (f.lootdrop_id == g.lootdrop_id && f.item_id == g.item_id && f.multiplier == g.multiplier) { + for (const auto &i: m_lootdrop_entries) { + if (h.lootdrop_id == i.lootdrop_id && h.item_id == i.item_id) { has_entry = true; break; } } + if (!has_entry) { + m_lootdrop_entries.emplace_back(h); + } } } } diff --git a/zone/zone_save_state.cpp b/zone/zone_save_state.cpp index 4e765fcac..c0adaaf12 100644 --- a/zone/zone_save_state.cpp +++ b/zone/zone_save_state.cpp @@ -4,46 +4,7 @@ #include "npc.h" #include "corpse.h" #include "zone.h" -#include "../common/repositories/zone_state_spawns_repository.h" -#include "../common/repositories/spawn2_disabled_repository.h" - -struct LootEntryStateData { - uint32 item_id; - uint32_t lootdrop_id; - uint16 charges = 0; // used in dynamically added loot (AddItem) - - // cereal - template - void serialize(Archive &ar) - { - ar( - CEREAL_NVP(item_id), - CEREAL_NVP(lootdrop_id), - CEREAL_NVP(charges) - ); - } -}; - -struct LootStateData { - uint32 copper = 0; - uint32 silver = 0; - uint32 gold = 0; - uint32 platinum = 0; - std::vector entries = {}; - - // cereal - template - void serialize(Archive &ar) - { - ar( - CEREAL_NVP(copper), - CEREAL_NVP(silver), - CEREAL_NVP(gold), - CEREAL_NVP(platinum), - CEREAL_NVP(entries) - ); - } -}; +#include "zone_save_state.h" // IsZoneStateValid checks if the zone state is valid // if these fields are all empty or zero value for an entire zone state, it's considered invalid @@ -359,7 +320,7 @@ inline void LoadNPCState(Zone *zone, NPC *n, ZoneStateSpawnsRepository::ZoneStat auto decay_time = s.decay_in_seconds * 1000; if (decay_time > 0) { n->SetQueuedToCorpse(); - n->SetCorpseDecayTime(decay_time); + entity_list.RestoreCorpse(n, decay_time); } else { n->Depop(); @@ -454,6 +415,7 @@ bool Zone::LoadZoneState( zone->Process(); // load zone variables first + int count = 0; for (auto &s: spawn_states) { if (s.is_zone) { LoadZoneVariables(zone, s.entity_variables); @@ -502,11 +464,13 @@ bool Zone::LoadZoneState( ); if (spawn_time_left == 0) { - new_spawn->SetCurrentNPCID(s.npc_id); + new_spawn->SetResumedNPCID(s.npc_id); new_spawn->SetResumedFromZoneSuspend(true); new_spawn->SetEntityVariables(GetVariablesDeserialized(s.entity_variables)); } + count++; + spawn2_list.Insert(new_spawn); new_spawn->Process(); auto n = new_spawn->GetNPC(); @@ -548,24 +512,6 @@ bool Zone::LoadZoneState( LoadNPCState(zone, npc, s); } - // any NPC that is spawned by the spawn system - for (auto &e: entity_list.GetNPCList()) { - auto npc = e.second; - if (npc->GetSpawnGroupId() == 0) { - continue; - } - - for (auto &s: spawn_states) { - bool is_same_npc = - s.npc_id == npc->GetNPCTypeID() && - s.spawn2_id == npc->GetSpawnPointID() && - s.spawngroup_id == npc->GetSpawnGroupId(); - if (is_same_npc) { - LoadNPCState(zone, npc, s); - } - } - } - return !spawn_states.empty(); } @@ -638,6 +584,7 @@ void Zone::SaveZoneState() std::vector spawns = {}; LinkedListIterator iterator(spawn2_list); iterator.Reset(); + int count = 0; while (iterator.MoreElements()) { Spawn2 *sp = iterator.GetData(); auto s = ZoneStateSpawnsRepository::NewEntity(); @@ -667,6 +614,7 @@ void Zone::SaveZoneState() spawns.emplace_back(s); iterator.Advance(); + count++; } // npc's that are not in the spawn2 list @@ -742,6 +690,11 @@ void Zone::SaveZoneState() return; } + if (spawns.empty()) { + LogInfo("No zone state data to save"); + return; + } + ZoneStateSpawnsRepository::InsertMany(database, spawns); LogInfo("Saved [{}] zone state spawns", Strings::Commify(spawns.size())); diff --git a/zone/zone_save_state.h b/zone/zone_save_state.h new file mode 100644 index 000000000..8d124240b --- /dev/null +++ b/zone/zone_save_state.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include "npc.h" +#include "corpse.h" +#include "zone.h" +#include "../common/repositories/zone_state_spawns_repository.h" +#include "../common/repositories/spawn2_disabled_repository.h" + +struct LootEntryStateData { + uint32 item_id = 0; + uint32_t lootdrop_id = 0; + uint16 charges = 0; // used in dynamically added loot (AddItem) + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(item_id), + CEREAL_NVP(lootdrop_id), + CEREAL_NVP(charges) + ); + } +}; + +struct LootStateData { + uint32 copper = 0; + uint32 silver = 0; + uint32 gold = 0; + uint32 platinum = 0; + std::vector entries = {}; + + // cereal + template + void serialize(Archive &ar) + { + ar( + CEREAL_NVP(copper), + CEREAL_NVP(silver), + CEREAL_NVP(gold), + CEREAL_NVP(platinum), + CEREAL_NVP(entries) + ); + } +};