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 auto controller = entity_list.GetNPCByNPCTypeID(ZONE_CONTROLLER_NPC_ID); if (controller != nullptr) { controller->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() == entries; RunTest( fmt::format("Spawns > All NPC's killed (0 NPCs) ([{}] Corpses)", entries), 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() == entries; if (!condition) { PrintEntityCounts(); } RunTest( fmt::format("Spawns > After restore (0 NPCs) ([{}] Corpses)", entries), 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() == entries && (int) entity_list.GetCorpseList().size() == entries; if (!condition) { PrintEntityCounts(); } RunTest( fmt::format("Spawns > After respawn ([{}] NPCs) ([{}] Corpses)", entries, entries), true, condition ); for (auto &c: entity_list.GetCorpseList()) { c.second->DepopNPCCorpse(); } entity_list.CorpseProcess(); condition = (int) entity_list.GetNPCList().size() == entries && (int) entity_list.GetCorpseList().size() == 0; if (!condition) { PrintEntityCounts(); } RunTest( fmt::format("Spawns > After respawn ([{}] NPCs) (0 Corpses)", entries), 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() == (entries - 10) && (int) entity_list.GetCorpseList().size() == 10; if (!condition) { PrintEntityCounts(); } RunTest( fmt::format("Spawns > Kill 10 NPC's before save/restore ([{}] NPCs) (10 Corpses)", (entries - 10)), true, condition ); zone->Shutdown(); SetupStateZone(); condition = (int) entity_list.GetNPCList().size() == (entries - 10) && (int) entity_list.GetCorpseList().size() == 10; if (!condition) { PrintEntityCounts(); } RunTest( fmt::format("Spawns > After restore ([{}] NPCs) (10 Corpses)", (entries - 10)), 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 ); int max_respawn = 0; const auto& l = RespawnTimesRepository::All(database); for (const auto& e : l) { if (e.duration > max_respawn) { max_respawn = e.duration; } } 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(max_respawn); // longest respawn time in zone zone->Process(); entity_list.MobProcess(); // processing depops condition = (int) entity_list.GetNPCList().size() == entries && (int) entity_list.GetCorpseList().size() == 10; if (!condition) { PrintEntityCounts(); PrintZoneNpcs(); } RunTest( fmt::format("Spawns > After respawn, ensure we have expected entity counts ([{}] NPCs) (10 Corpses)", entries), 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"; }