From 2f7ca2cdc8d459e962bd6f799a528e3a082c206b Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 28 Feb 2025 15:31:06 -0600 Subject: [PATCH] [Zone] Implement Zone State Saving on Shutdown (#4715) * Save spawns * Update base_zone_state_spawns_repository.h * Zone state save work * Code cleanup * More cleanup * Database migration * Update database_update_manifest.cpp * Revert decay at storage model * Code cleanup * More cleanup * More cleanup * More cleanup * Entity variables * Add entity variables to the schema * Post rebase * Checkpoint * Serialize / deserialize buffs * Current hp / mana / end save / load * Save / load current_waypoint * Add zone spawn protection * Finishing touches * Cleanup * Update zone_save_state.cpp * Cleanup * Update zone_save_state.cpp * Update npc.cpp * Update npc.cpp * More * Update perl_npc.cpp * Update zone_loot.cpp --- common/database/database_update_manifest.cpp | 41 + common/database_schema.h | 1 + common/eqemu_logsys.h | 4 +- common/eqemu_logsys_log_aliases.h | 10 + common/loot.h | 1 + .../base/base_zone_state_spawns_repository.h | 703 ++++++++++++++++++ .../zone_state_spawns_repository.h | 14 + common/ruletypes.h | 1 + common/version.h | 2 +- zone/attack.cpp | 39 +- zone/common.h | 42 ++ zone/corpse.h | 1 + zone/loot.cpp | 6 + zone/lua_npc.cpp | 7 + zone/lua_npc.h | 1 + zone/main.cpp | 6 +- zone/npc.cpp | 46 ++ zone/npc.h | 19 + zone/perl_npc.cpp | 6 + zone/spawn2.cpp | 25 +- zone/spawn2.h | 16 +- zone/worldserver.cpp | 2 +- zone/zone.cpp | 61 +- zone/zone.h | 13 +- zone/zone_loot.cpp | 89 +++ zone/zone_save_state.cpp | 540 ++++++++++++++ 26 files changed, 1637 insertions(+), 59 deletions(-) create mode 100644 common/repositories/base/base_zone_state_spawns_repository.h create mode 100644 common/repositories/zone_state_spawns_repository.h create mode 100644 zone/zone_save_state.cpp diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index 6aa398b49..cbbba58d0 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -6868,6 +6868,47 @@ CREATE UNIQUE INDEX `keys` ON data_buckets (`key`, character_id, npc_id, bot_id, -- ✅ Create indexes for just instance_id (instance deletion) CREATE INDEX idx_instance_id ON data_buckets (instance_id); +)", + .content_schema_update = false + }, + ManifestEntry{ + .version = 9307, + .description = "2025_02_17_zone_state_spawns.sql", + .check = "SHOW TABLES LIKE 'zone_state_spawns'", + .condition = "empty", + .match = "", + .sql = R"( +CREATE TABLE `zone_state_spawns` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `zone_id` int(11) unsigned DEFAULT NULL, + `instance_id` int(11) unsigned DEFAULT NULL, + `is_corpse` tinyint(11) DEFAULT 0, + `decay_in_seconds` int(11) DEFAULT 0, + `npc_id` int(10) unsigned DEFAULT NULL, + `spawn2_id` int(10) unsigned NOT NULL, + `spawngroup_id` int(10) unsigned NOT NULL, + `x` float NOT NULL, + `y` float NOT NULL, + `z` float NOT NULL, + `heading` float NOT NULL, + `respawn_time` int(10) unsigned NOT NULL, + `variance` int(10) unsigned NOT NULL, + `grid` int(10) unsigned DEFAULT 0, + `current_waypoint` int(11) DEFAULT 0, + `path_when_zone_idle` smallint(6) DEFAULT 0, + `condition_id` smallint(5) unsigned DEFAULT 0, + `condition_min_value` smallint(6) DEFAULT 0, + `enabled` smallint(6) DEFAULT 1, + `anim` smallint(5) unsigned DEFAULT 0, + `loot_data` text DEFAULT NULL, + `entity_variables` text DEFAULT NULL, + `buffs` text DEFAULT NULL, + `hp` bigint(20) DEFAULT 0, + `mana` bigint(20) DEFAULT 0, + `endurance` bigint(20) DEFAULT 0, + `created_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 )", .content_schema_update = false }, diff --git a/common/database_schema.h b/common/database_schema.h index b929c5e1b..dfaeacbbd 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -350,6 +350,7 @@ namespace DatabaseSchema { "shared_task_dynamic_zones", "shared_task_members", "shared_tasks", + "zone_state_spawns", }; } diff --git a/common/eqemu_logsys.h b/common/eqemu_logsys.h index 6df671dba..02bd89018 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -149,6 +149,7 @@ namespace Logs { BotSpellChecks, BotSpellTypeChecks, NpcHandin, + ZoneState, MaxCategoryID /* Don't Remove this */ }; @@ -256,7 +257,8 @@ namespace Logs { "Bot Settings", "Bot Spell Checks", "Bot Spell Type Checks", - "NpcHandin" + "NpcHandin", + "ZoneState" }; } diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index eaf66f8cc..1047f6c0d 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -914,6 +914,16 @@ OutF(LogSys, Logs::Detail, Logs::NpcHandin, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ } while (0) +#define LogZoneState(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::General, Logs::ZoneState))\ + OutF(LogSys, Logs::General, Logs::ZoneState, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogZoneStateDetail(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::Detail, Logs::ZoneState))\ + OutF(LogSys, Logs::Detail, Logs::ZoneState, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ if (LogSys.IsLogEnabled(debug_level, log_category))\ LogSys.Out(debug_level, log_category, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ diff --git a/common/loot.h b/common/loot.h index ba36c364f..62baf1425 100644 --- a/common/loot.h +++ b/common/loot.h @@ -25,6 +25,7 @@ struct LootItem { uint16 trivial_max_level; uint16 npc_min_level; uint16 npc_max_level; + uint32 lootdrop_id; // required for zone state referencing }; typedef std::list LootItems; diff --git a/common/repositories/base/base_zone_state_spawns_repository.h b/common/repositories/base/base_zone_state_spawns_repository.h new file mode 100644 index 000000000..067d2fea8 --- /dev/null +++ b/common/repositories/base/base_zone_state_spawns_repository.h @@ -0,0 +1,703 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://docs.eqemu.io/developer/repositories + */ + +#ifndef EQEMU_BASE_ZONE_STATE_SPAWNS_REPOSITORY_H +#define EQEMU_BASE_ZONE_STATE_SPAWNS_REPOSITORY_H + +#include "../../database.h" +#include "../../strings.h" +#include + +class BaseZoneStateSpawnsRepository { +public: + struct ZoneStateSpawns { + int64_t id; + uint32_t zone_id; + uint32_t instance_id; + int8_t is_corpse; + int32_t decay_in_seconds; + uint32_t npc_id; + uint32_t spawn2_id; + uint32_t spawngroup_id; + float x; + float y; + float z; + float heading; + uint32_t respawn_time; + uint32_t variance; + uint32_t grid; + int32_t current_waypoint; + int16_t path_when_zone_idle; + uint16_t condition_id; + int16_t condition_min_value; + int16_t enabled; + uint16_t anim; + std::string loot_data; + std::string entity_variables; + std::string buffs; + int64_t hp; + int64_t mana; + int64_t endurance; + time_t created_at; + }; + + static std::string PrimaryKey() + { + return std::string("id"); + } + + static std::vector Columns() + { + return { + "id", + "zone_id", + "instance_id", + "is_corpse", + "decay_in_seconds", + "npc_id", + "spawn2_id", + "spawngroup_id", + "x", + "y", + "z", + "heading", + "respawn_time", + "variance", + "grid", + "current_waypoint", + "path_when_zone_idle", + "condition_id", + "condition_min_value", + "enabled", + "anim", + "loot_data", + "entity_variables", + "buffs", + "hp", + "mana", + "endurance", + "created_at", + }; + } + + static std::vector SelectColumns() + { + return { + "id", + "zone_id", + "instance_id", + "is_corpse", + "decay_in_seconds", + "npc_id", + "spawn2_id", + "spawngroup_id", + "x", + "y", + "z", + "heading", + "respawn_time", + "variance", + "grid", + "current_waypoint", + "path_when_zone_idle", + "condition_id", + "condition_min_value", + "enabled", + "anim", + "loot_data", + "entity_variables", + "buffs", + "hp", + "mana", + "endurance", + "UNIX_TIMESTAMP(created_at)", + }; + } + + static std::string ColumnsRaw() + { + return std::string(Strings::Implode(", ", Columns())); + } + + static std::string SelectColumnsRaw() + { + return std::string(Strings::Implode(", ", SelectColumns())); + } + + static std::string TableName() + { + return std::string("zone_state_spawns"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static ZoneStateSpawns NewEntity() + { + ZoneStateSpawns e{}; + + e.id = 0; + e.zone_id = 0; + e.instance_id = 0; + e.is_corpse = 0; + e.decay_in_seconds = 0; + e.npc_id = 0; + e.spawn2_id = 0; + e.spawngroup_id = 0; + e.x = 0; + e.y = 0; + e.z = 0; + e.heading = 0; + e.respawn_time = 0; + e.variance = 0; + e.grid = 0; + e.current_waypoint = 0; + e.path_when_zone_idle = 0; + e.condition_id = 0; + e.condition_min_value = 0; + e.enabled = 1; + e.anim = 0; + e.loot_data = ""; + e.entity_variables = ""; + e.buffs = ""; + e.hp = 0; + e.mana = 0; + e.endurance = 0; + e.created_at = 0; + + return e; + } + + static ZoneStateSpawns GetZoneStateSpawns( + const std::vector &zone_state_spawnss, + int zone_state_spawns_id + ) + { + for (auto &zone_state_spawns : zone_state_spawnss) { + if (zone_state_spawns.id == zone_state_spawns_id) { + return zone_state_spawns; + } + } + + return NewEntity(); + } + + static ZoneStateSpawns FindOne( + Database& db, + int zone_state_spawns_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {} = {} LIMIT 1", + BaseSelect(), + PrimaryKey(), + zone_state_spawns_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + ZoneStateSpawns e{}; + + e.id = row[0] ? strtoll(row[0], nullptr, 10) : 0; + e.zone_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.instance_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.is_corpse = row[3] ? static_cast(atoi(row[3])) : 0; + e.decay_in_seconds = row[4] ? static_cast(atoi(row[4])) : 0; + e.npc_id = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.spawn2_id = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.spawngroup_id = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.x = row[8] ? strtof(row[8], nullptr) : 0; + e.y = row[9] ? strtof(row[9], nullptr) : 0; + e.z = row[10] ? strtof(row[10], nullptr) : 0; + e.heading = row[11] ? strtof(row[11], nullptr) : 0; + e.respawn_time = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; + e.variance = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; + e.grid = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; + e.current_waypoint = row[15] ? static_cast(atoi(row[15])) : 0; + e.path_when_zone_idle = row[16] ? static_cast(atoi(row[16])) : 0; + e.condition_id = row[17] ? static_cast(strtoul(row[17], nullptr, 10)) : 0; + e.condition_min_value = row[18] ? static_cast(atoi(row[18])) : 0; + e.enabled = row[19] ? static_cast(atoi(row[19])) : 1; + e.anim = row[20] ? static_cast(strtoul(row[20], nullptr, 10)) : 0; + e.loot_data = row[21] ? row[21] : ""; + e.entity_variables = row[22] ? row[22] : ""; + e.buffs = row[23] ? row[23] : ""; + e.hp = row[24] ? strtoll(row[24], nullptr, 10) : 0; + e.mana = row[25] ? strtoll(row[25], nullptr, 10) : 0; + e.endurance = row[26] ? strtoll(row[26], nullptr, 10) : 0; + e.created_at = strtoll(row[27] ? row[27] : "-1", nullptr, 10); + + return e; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int zone_state_spawns_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + zone_state_spawns_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + const ZoneStateSpawns &e + ) + { + std::vector v; + + auto columns = Columns(); + + v.push_back(columns[1] + " = " + std::to_string(e.zone_id)); + v.push_back(columns[2] + " = " + std::to_string(e.instance_id)); + v.push_back(columns[3] + " = " + std::to_string(e.is_corpse)); + v.push_back(columns[4] + " = " + std::to_string(e.decay_in_seconds)); + v.push_back(columns[5] + " = " + std::to_string(e.npc_id)); + v.push_back(columns[6] + " = " + std::to_string(e.spawn2_id)); + v.push_back(columns[7] + " = " + std::to_string(e.spawngroup_id)); + v.push_back(columns[8] + " = " + std::to_string(e.x)); + v.push_back(columns[9] + " = " + std::to_string(e.y)); + v.push_back(columns[10] + " = " + std::to_string(e.z)); + v.push_back(columns[11] + " = " + std::to_string(e.heading)); + v.push_back(columns[12] + " = " + std::to_string(e.respawn_time)); + v.push_back(columns[13] + " = " + std::to_string(e.variance)); + v.push_back(columns[14] + " = " + std::to_string(e.grid)); + v.push_back(columns[15] + " = " + std::to_string(e.current_waypoint)); + v.push_back(columns[16] + " = " + std::to_string(e.path_when_zone_idle)); + v.push_back(columns[17] + " = " + std::to_string(e.condition_id)); + v.push_back(columns[18] + " = " + std::to_string(e.condition_min_value)); + v.push_back(columns[19] + " = " + std::to_string(e.enabled)); + v.push_back(columns[20] + " = " + std::to_string(e.anim)); + v.push_back(columns[21] + " = '" + Strings::Escape(e.loot_data) + "'"); + v.push_back(columns[22] + " = '" + Strings::Escape(e.entity_variables) + "'"); + v.push_back(columns[23] + " = '" + Strings::Escape(e.buffs) + "'"); + v.push_back(columns[24] + " = " + std::to_string(e.hp)); + v.push_back(columns[25] + " = " + std::to_string(e.mana)); + v.push_back(columns[26] + " = " + std::to_string(e.endurance)); + v.push_back(columns[27] + " = FROM_UNIXTIME(" + (e.created_at > 0 ? std::to_string(e.created_at) : "null") + ")"); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + Strings::Implode(", ", v), + PrimaryKey(), + e.id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static ZoneStateSpawns InsertOne( + Database& db, + ZoneStateSpawns e + ) + { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.zone_id)); + v.push_back(std::to_string(e.instance_id)); + v.push_back(std::to_string(e.is_corpse)); + v.push_back(std::to_string(e.decay_in_seconds)); + v.push_back(std::to_string(e.npc_id)); + v.push_back(std::to_string(e.spawn2_id)); + v.push_back(std::to_string(e.spawngroup_id)); + v.push_back(std::to_string(e.x)); + v.push_back(std::to_string(e.y)); + v.push_back(std::to_string(e.z)); + v.push_back(std::to_string(e.heading)); + v.push_back(std::to_string(e.respawn_time)); + v.push_back(std::to_string(e.variance)); + v.push_back(std::to_string(e.grid)); + v.push_back(std::to_string(e.current_waypoint)); + v.push_back(std::to_string(e.path_when_zone_idle)); + v.push_back(std::to_string(e.condition_id)); + v.push_back(std::to_string(e.condition_min_value)); + v.push_back(std::to_string(e.enabled)); + v.push_back(std::to_string(e.anim)); + v.push_back("'" + Strings::Escape(e.loot_data) + "'"); + v.push_back("'" + Strings::Escape(e.entity_variables) + "'"); + v.push_back("'" + Strings::Escape(e.buffs) + "'"); + v.push_back(std::to_string(e.hp)); + v.push_back(std::to_string(e.mana)); + v.push_back(std::to_string(e.endurance)); + v.push_back("FROM_UNIXTIME(" + (e.created_at > 0 ? std::to_string(e.created_at) : "null") + ")"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + Strings::Implode(",", v) + ) + ); + + if (results.Success()) { + e.id = results.LastInsertedID(); + return e; + } + + e = NewEntity(); + + return e; + } + + static int InsertMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.zone_id)); + v.push_back(std::to_string(e.instance_id)); + v.push_back(std::to_string(e.is_corpse)); + v.push_back(std::to_string(e.decay_in_seconds)); + v.push_back(std::to_string(e.npc_id)); + v.push_back(std::to_string(e.spawn2_id)); + v.push_back(std::to_string(e.spawngroup_id)); + v.push_back(std::to_string(e.x)); + v.push_back(std::to_string(e.y)); + v.push_back(std::to_string(e.z)); + v.push_back(std::to_string(e.heading)); + v.push_back(std::to_string(e.respawn_time)); + v.push_back(std::to_string(e.variance)); + v.push_back(std::to_string(e.grid)); + v.push_back(std::to_string(e.current_waypoint)); + v.push_back(std::to_string(e.path_when_zone_idle)); + v.push_back(std::to_string(e.condition_id)); + v.push_back(std::to_string(e.condition_min_value)); + v.push_back(std::to_string(e.enabled)); + v.push_back(std::to_string(e.anim)); + v.push_back("'" + Strings::Escape(e.loot_data) + "'"); + v.push_back("'" + Strings::Escape(e.entity_variables) + "'"); + v.push_back("'" + Strings::Escape(e.buffs) + "'"); + v.push_back(std::to_string(e.hp)); + v.push_back(std::to_string(e.mana)); + v.push_back(std::to_string(e.endurance)); + v.push_back("FROM_UNIXTIME(" + (e.created_at > 0 ? std::to_string(e.created_at) : "null") + ")"); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + ZoneStateSpawns e{}; + + e.id = row[0] ? strtoll(row[0], nullptr, 10) : 0; + e.zone_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.instance_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.is_corpse = row[3] ? static_cast(atoi(row[3])) : 0; + e.decay_in_seconds = row[4] ? static_cast(atoi(row[4])) : 0; + e.npc_id = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.spawn2_id = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.spawngroup_id = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.x = row[8] ? strtof(row[8], nullptr) : 0; + e.y = row[9] ? strtof(row[9], nullptr) : 0; + e.z = row[10] ? strtof(row[10], nullptr) : 0; + e.heading = row[11] ? strtof(row[11], nullptr) : 0; + e.respawn_time = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; + e.variance = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; + e.grid = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; + e.current_waypoint = row[15] ? static_cast(atoi(row[15])) : 0; + e.path_when_zone_idle = row[16] ? static_cast(atoi(row[16])) : 0; + e.condition_id = row[17] ? static_cast(strtoul(row[17], nullptr, 10)) : 0; + e.condition_min_value = row[18] ? static_cast(atoi(row[18])) : 0; + e.enabled = row[19] ? static_cast(atoi(row[19])) : 1; + e.anim = row[20] ? static_cast(strtoul(row[20], nullptr, 10)) : 0; + e.loot_data = row[21] ? row[21] : ""; + e.entity_variables = row[22] ? row[22] : ""; + e.buffs = row[23] ? row[23] : ""; + e.hp = row[24] ? strtoll(row[24], nullptr, 10) : 0; + e.mana = row[25] ? strtoll(row[25], nullptr, 10) : 0; + e.endurance = row[26] ? strtoll(row[26], nullptr, 10) : 0; + e.created_at = strtoll(row[27] ? row[27] : "-1", nullptr, 10); + + all_entries.push_back(e); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, const std::string &where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + ZoneStateSpawns e{}; + + e.id = row[0] ? strtoll(row[0], nullptr, 10) : 0; + e.zone_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.instance_id = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.is_corpse = row[3] ? static_cast(atoi(row[3])) : 0; + e.decay_in_seconds = row[4] ? static_cast(atoi(row[4])) : 0; + e.npc_id = row[5] ? static_cast(strtoul(row[5], nullptr, 10)) : 0; + e.spawn2_id = row[6] ? static_cast(strtoul(row[6], nullptr, 10)) : 0; + e.spawngroup_id = row[7] ? static_cast(strtoul(row[7], nullptr, 10)) : 0; + e.x = row[8] ? strtof(row[8], nullptr) : 0; + e.y = row[9] ? strtof(row[9], nullptr) : 0; + e.z = row[10] ? strtof(row[10], nullptr) : 0; + e.heading = row[11] ? strtof(row[11], nullptr) : 0; + e.respawn_time = row[12] ? static_cast(strtoul(row[12], nullptr, 10)) : 0; + e.variance = row[13] ? static_cast(strtoul(row[13], nullptr, 10)) : 0; + e.grid = row[14] ? static_cast(strtoul(row[14], nullptr, 10)) : 0; + e.current_waypoint = row[15] ? static_cast(atoi(row[15])) : 0; + e.path_when_zone_idle = row[16] ? static_cast(atoi(row[16])) : 0; + e.condition_id = row[17] ? static_cast(strtoul(row[17], nullptr, 10)) : 0; + e.condition_min_value = row[18] ? static_cast(atoi(row[18])) : 0; + e.enabled = row[19] ? static_cast(atoi(row[19])) : 1; + e.anim = row[20] ? static_cast(strtoul(row[20], nullptr, 10)) : 0; + e.loot_data = row[21] ? row[21] : ""; + e.entity_variables = row[22] ? row[22] : ""; + e.buffs = row[23] ? row[23] : ""; + e.hp = row[24] ? strtoll(row[24], nullptr, 10) : 0; + e.mana = row[25] ? strtoll(row[25], nullptr, 10) : 0; + e.endurance = row[26] ? strtoll(row[26], nullptr, 10) : 0; + e.created_at = strtoll(row[27] ? row[27] : "-1", nullptr, 10); + + all_entries.push_back(e); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, const std::string &where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int64 GetMaxId(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COALESCE(MAX({}), 0) FROM {}", + PrimaryKey(), + TableName() + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static int64 Count(Database& db, const std::string &where_filter = "") + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COUNT(*) FROM {} {}", + TableName(), + (where_filter.empty() ? "" : "WHERE " + where_filter) + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static std::string BaseReplace() + { + return fmt::format( + "REPLACE INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static int ReplaceOne( + Database& db, + const ZoneStateSpawns &e + ) + { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.zone_id)); + v.push_back(std::to_string(e.instance_id)); + v.push_back(std::to_string(e.is_corpse)); + v.push_back(std::to_string(e.decay_in_seconds)); + v.push_back(std::to_string(e.npc_id)); + v.push_back(std::to_string(e.spawn2_id)); + v.push_back(std::to_string(e.spawngroup_id)); + v.push_back(std::to_string(e.x)); + v.push_back(std::to_string(e.y)); + v.push_back(std::to_string(e.z)); + v.push_back(std::to_string(e.heading)); + v.push_back(std::to_string(e.respawn_time)); + v.push_back(std::to_string(e.variance)); + v.push_back(std::to_string(e.grid)); + v.push_back(std::to_string(e.current_waypoint)); + v.push_back(std::to_string(e.path_when_zone_idle)); + v.push_back(std::to_string(e.condition_id)); + v.push_back(std::to_string(e.condition_min_value)); + v.push_back(std::to_string(e.enabled)); + v.push_back(std::to_string(e.anim)); + v.push_back("'" + Strings::Escape(e.loot_data) + "'"); + v.push_back("'" + Strings::Escape(e.entity_variables) + "'"); + v.push_back("'" + Strings::Escape(e.buffs) + "'"); + v.push_back(std::to_string(e.hp)); + v.push_back(std::to_string(e.mana)); + v.push_back(std::to_string(e.endurance)); + v.push_back("FROM_UNIXTIME(" + (e.created_at > 0 ? std::to_string(e.created_at) : "null") + ")"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseReplace(), + Strings::Implode(",", v) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int ReplaceMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.zone_id)); + v.push_back(std::to_string(e.instance_id)); + v.push_back(std::to_string(e.is_corpse)); + v.push_back(std::to_string(e.decay_in_seconds)); + v.push_back(std::to_string(e.npc_id)); + v.push_back(std::to_string(e.spawn2_id)); + v.push_back(std::to_string(e.spawngroup_id)); + v.push_back(std::to_string(e.x)); + v.push_back(std::to_string(e.y)); + v.push_back(std::to_string(e.z)); + v.push_back(std::to_string(e.heading)); + v.push_back(std::to_string(e.respawn_time)); + v.push_back(std::to_string(e.variance)); + v.push_back(std::to_string(e.grid)); + v.push_back(std::to_string(e.current_waypoint)); + v.push_back(std::to_string(e.path_when_zone_idle)); + v.push_back(std::to_string(e.condition_id)); + v.push_back(std::to_string(e.condition_min_value)); + v.push_back(std::to_string(e.enabled)); + v.push_back(std::to_string(e.anim)); + v.push_back("'" + Strings::Escape(e.loot_data) + "'"); + v.push_back("'" + Strings::Escape(e.entity_variables) + "'"); + v.push_back("'" + Strings::Escape(e.buffs) + "'"); + v.push_back(std::to_string(e.hp)); + v.push_back(std::to_string(e.mana)); + v.push_back(std::to_string(e.endurance)); + v.push_back("FROM_UNIXTIME(" + (e.created_at > 0 ? std::to_string(e.created_at) : "null") + ")"); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseReplace(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } +}; + +#endif //EQEMU_BASE_ZONE_STATE_SPAWNS_REPOSITORY_H diff --git a/common/repositories/zone_state_spawns_repository.h b/common/repositories/zone_state_spawns_repository.h new file mode 100644 index 000000000..0166622b0 --- /dev/null +++ b/common/repositories/zone_state_spawns_repository.h @@ -0,0 +1,14 @@ +#ifndef EQEMU_ZONE_STATE_SPAWNS_REPOSITORY_H +#define EQEMU_ZONE_STATE_SPAWNS_REPOSITORY_H + +#include "../database.h" +#include "../strings.h" +#include "base/base_zone_state_spawns_repository.h" + +class ZoneStateSpawnsRepository: public BaseZoneStateSpawnsRepository { +public: + // Custom extended repository methods here + +}; + +#endif //EQEMU_ZONE_STATE_SPAWNS_REPOSITORY_H diff --git a/common/ruletypes.h b/common/ruletypes.h index 77d3f0560..6c834c580 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -374,6 +374,7 @@ RULE_BOOL(Zone, AllowCrossZoneSpellsOnBots, false, "Set to true to allow cross z RULE_BOOL(Zone, AllowCrossZoneSpellsOnMercs, false, "Set to true to allow cross zone spells (cast/remove) to affect mercenaries") RULE_BOOL(Zone, AllowCrossZoneSpellsOnPets, false, "Set to true to allow cross zone spells (cast/remove) to affect pets") RULE_BOOL(Zone, ZoneShardQuestMenuOnly, false, "Set to true if you only want quests to show the zone shard menu") +RULE_BOOL(Zone, StateSavingOnShutdown, true, "Set to true if you want zones to save state on shutdown (npcs, corpses, loot, entity variables, buffs etc.)") RULE_CATEGORY_END() RULE_CATEGORY(Map) diff --git a/common/version.h b/common/version.h index bf0a8add2..4c9344b79 100644 --- a/common/version.h +++ b/common/version.h @@ -42,7 +42,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9306 +#define CURRENT_BINARY_DATABASE_VERSION 9307 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054 #endif diff --git a/zone/attack.cpp b/zone/attack.cpp index 8f6aaf02d..e32941fec 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -2789,32 +2789,35 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy const uint16 entity_id = GetID(); if ( - !HasOwner() && - !IsMerc() && - !GetSwarmInfo() && - (!is_merchant || allow_merchant_corpse) && ( + !HasOwner() && + !IsMerc() && + !GetSwarmInfo() && + (!is_merchant || allow_merchant_corpse) && ( - killer && ( - killer->IsClient() || + killer && ( - killer->HasOwner() && - killer->GetUltimateOwner()->IsClient() - ) || - ( - killer->IsNPC() && - killer->CastToNPC()->GetSwarmInfo() && - killer->CastToNPC()->GetSwarmInfo()->GetOwner() && - killer->CastToNPC()->GetSwarmInfo()->GetOwner()->IsClient() + killer->IsClient() || + ( + killer->HasOwner() && + killer->GetUltimateOwner()->IsClient() + ) || + ( + killer->IsNPC() && + killer->CastToNPC()->GetSwarmInfo() && + killer->CastToNPC()->GetSwarmInfo()->GetOwner() && + killer->CastToNPC()->GetSwarmInfo()->GetOwner()->IsClient() + ) ) + ) || + ( + killer_mob && is_ldon_treasure ) - ) || - ( - killer_mob && is_ldon_treasure ) ) - ) { + || IsQueuedForCorpse() + ) { if (killer) { if (killer->GetOwner() != 0 && killer->GetOwner()->IsClient()) { killer = killer->GetOwner(); diff --git a/zone/common.h b/zone/common.h index a900b9738..10c17b7a6 100644 --- a/zone/common.h +++ b/zone/common.h @@ -4,6 +4,8 @@ #include "../common/types.h" #include "../common/spdat.h" +#include + #define HIGHEST_RESIST 9 //Max resist type value #define MAX_SPELL_PROJECTILE 10 //Max amount of spell projectiles that can be active by a single mob. @@ -272,6 +274,46 @@ struct Buffs_Struct { bool persistant_buff; bool client; //True if the caster is a client bool UpdateClient; + + // cereal + template + void serialize(Archive &ar) + { + + std::string caster_name_str(caster_name); + if (Archive::is_saving::value) { + caster_name_str = std::string(caster_name); + } + + ar( + CEREAL_NVP(spellid), + CEREAL_NVP(casterlevel), + CEREAL_NVP(casterid), + CEREAL_NVP(caster_name_str), + CEREAL_NVP(ticsremaining), + CEREAL_NVP(counters), + CEREAL_NVP(hit_number), + CEREAL_NVP(melee_rune), + CEREAL_NVP(magic_rune), + CEREAL_NVP(dot_rune), + CEREAL_NVP(caston_x), + CEREAL_NVP(caston_y), + CEREAL_NVP(caston_z), + CEREAL_NVP(ExtraDIChance), + CEREAL_NVP(RootBreakChance), + CEREAL_NVP(instrument_mod), + CEREAL_NVP(virus_spread_time), + CEREAL_NVP(persistant_buff), + CEREAL_NVP(client), + CEREAL_NVP(UpdateClient) + ); + + // Copy back into caster_name after deserialization + if (Archive::is_loading::value) { + strncpy(caster_name, caster_name_str.c_str(), sizeof(caster_name)); + caster_name[sizeof(caster_name) - 1] = '\0'; // Ensure null termination + } + } }; struct StatBonuses { diff --git a/zone/corpse.h b/zone/corpse.h index c1925c325..7b647ed8d 100644 --- a/zone/corpse.h +++ b/zone/corpse.h @@ -200,6 +200,7 @@ public: uint32 GetItemIDBySlot(uint16 loot_slot); uint16 GetFirstLootSlotByItemID(uint32 item_id); std::vector GetLootList(); + inline const LootItems &GetLootItems() { return m_item_list; } void LootCorpseItem(Client *c, const EQApplicationPacket *app); void EndLoot(Client *c, const EQApplicationPacket *app); void MakeLootRequestPackets(Client *c, const EQApplicationPacket *app); diff --git a/zone/loot.cpp b/zone/loot.cpp index bb53ddc55..8d412bfd6 100644 --- a/zone/loot.cpp +++ b/zone/loot.cpp @@ -276,6 +276,11 @@ void NPC::AddLootDrop( uint32 augment_six ) { + if (m_resumed_from_zone_suspend) { + LogZoneState("NPC [{}] is resuming from zone suspend, skipping AddItem", GetCleanName()); + return; + } + if (!item2) { return; } @@ -500,6 +505,7 @@ void NPC::AddLootDrop( parse->EventNPC(EVENT_LOOT_ADDED, this, nullptr, "", 0, &args); } + item->lootdrop_id = loot_drop.lootdrop_id; m_loot_items.push_back(item); if (found) { diff --git a/zone/lua_npc.cpp b/zone/lua_npc.cpp index b1c3d8d25..99123936d 100644 --- a/zone/lua_npc.cpp +++ b/zone/lua_npc.cpp @@ -939,6 +939,12 @@ Lua_Spawn Lua_NPC::GetSpawn(lua_State* L) return Lua_Spawn(self->GetSpawn()); } +bool Lua_NPC::IsResumedFromZoneSuspend() +{ + Lua_Safe_Call_Bool(); + return self->IsResumedFromZoneSuspend(); +} + luabind::scope lua_register_npc() { return luabind::class_("NPC") .def(luabind::constructor<>()) @@ -1040,6 +1046,7 @@ luabind::scope lua_register_npc() { .def("IsOnHatelist", (bool(Lua_NPC::*)(Lua_Mob))&Lua_NPC::IsOnHatelist) .def("IsRaidTarget", (bool(Lua_NPC::*)(void))&Lua_NPC::IsRaidTarget) .def("IsRareSpawn", (bool(Lua_NPC::*)(void))&Lua_NPC::IsRareSpawn) + .def("IsResumedFromZoneSuspend",(bool(Lua_NPC::*)(void))&Lua_NPC::IsResumedFromZoneSuspend) .def("IsTaunting", (bool(Lua_NPC::*)(void))&Lua_NPC::IsTaunting) .def("IsUnderwaterOnly", (bool(Lua_NPC::*)(void))&Lua_NPC::IsUnderwaterOnly) .def("MerchantCloseShop", (void(Lua_NPC::*)(void))&Lua_NPC::MerchantCloseShop) diff --git a/zone/lua_npc.h b/zone/lua_npc.h index 93a0b9451..3e7ede475 100644 --- a/zone/lua_npc.h +++ b/zone/lua_npc.h @@ -198,6 +198,7 @@ public: ); void ReturnHandinItems(Lua_Client c); Lua_Spawn GetSpawn(lua_State* L); + bool IsResumedFromZoneSuspend(); }; #endif diff --git a/zone/main.cpp b/zone/main.cpp index 9361c8695..1da10f5e7 100644 --- a/zone/main.cpp +++ b/zone/main.cpp @@ -631,7 +631,7 @@ int main(int argc, char **argv) if (zone) { if (!zone->Process()) { - Zone::Shutdown(); + zone->Shutdown(); } } @@ -668,7 +668,7 @@ int main(int argc, char **argv) safe_delete(Config); if (zone != 0) { - Zone::Shutdown(true); + zone->Shutdown(true); } //Fix for Linux world server problem. safe_delete(task_manager); @@ -687,7 +687,7 @@ int main(int argc, char **argv) void Shutdown() { - Zone::Shutdown(true); + zone->Shutdown(true); LogInfo("Shutting down..."); LogSys.CloseFileLogs(); EQ::EventLoop::Get().Shutdown(); diff --git a/zone/npc.cpp b/zone/npc.cpp index 9541fda46..5e7992484 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -62,6 +62,7 @@ #else #include #include + #endif extern Zone* zone; @@ -131,6 +132,9 @@ 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(30000), classattack_timer(1000), monkattack_timer(1000), knightattack_timer(1000), @@ -618,7 +622,49 @@ 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(); + SetResumedFromZoneSuspend(false); + } + } + 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 84022ef13..9c9c31976 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -601,6 +601,13 @@ public: bool HasProcessedHandinReturn() { return m_has_processed_handin_return; } bool HandinStarted() { return m_handin_started; } + // 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 void SetResumedFromZoneSuspend(bool state = true) { m_resumed_from_zone_suspend = state; } + inline bool IsResumedFromZoneSuspend() { return m_resumed_from_zone_suspend; } + protected: void HandleRoambox(); @@ -622,6 +629,18 @@ protected: uint32 m_loot_platinum; 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 = {}; + + // this is a 30-second 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 + Timer m_resumed_from_zone_suspend_shutoff_timer = {}; + std::list faction_list; int32 npc_faction_id; diff --git a/zone/perl_npc.cpp b/zone/perl_npc.cpp index bea29a037..8498f4a6e 100644 --- a/zone/perl_npc.cpp +++ b/zone/perl_npc.cpp @@ -806,6 +806,11 @@ void Perl_NPC_MultiQuestEnable(NPC* self) self->MultiQuestEnable(); } +bool Perl_NPC_IsResumedFromZoneSuspend(NPC* self) +{ + return self->IsResumedFromZoneSuspend(); +} + bool Perl_NPC_CheckHandin( NPC* self, Client* c, @@ -983,6 +988,7 @@ void perl_register_npc() package.add("IsOnHatelist", &Perl_NPC_IsOnHatelist); package.add("IsRaidTarget", &Perl_NPC_IsRaidTarget); package.add("IsRareSpawn", &Perl_NPC_IsRareSpawn); + package.add("IsResumedFromZoneSuspend", &Perl_NPC_IsResumedFromZoneSuspend); package.add("IsTaunting", &Perl_NPC_IsTaunting); package.add("IsUnderwaterOnly", (bool(*)(NPC*))&Perl_NPC_IsUnderwaterOnly); package.add("MerchantCloseShop", &Perl_NPC_MerchantCloseShop); diff --git a/zone/spawn2.cpp b/zone/spawn2.cpp index e8752eb40..b500f25fc 100644 --- a/zone/spawn2.cpp +++ b/zone/spawn2.cpp @@ -16,6 +16,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ +#include #include "../common/global_define.h" #include "../common/strings.h" @@ -33,6 +34,7 @@ #include "../common/repositories/spawn2_repository.h" #include "../common/repositories/spawn2_disabled_repository.h" #include "../common/repositories/respawn_times_repository.h" +#include "../common/repositories/zone_state_spawns_repository.h" extern EntityList entity_list; extern Zone* zone; @@ -85,9 +87,9 @@ Spawn2::Spawn2(uint32 in_spawn2_id, uint32 spawngroup_id, x = in_x; y = in_y; z = in_z; - heading = in_heading; - respawn_ = respawn; - variance_ = variance; + heading = in_heading; + m_respawn_time = respawn; + variance_ = variance; grid_ = grid; path_when_zone_idle = in_path_when_zone_idle; condition_id = in_cond_id; @@ -95,6 +97,7 @@ Spawn2::Spawn2(uint32 in_spawn2_id, uint32 spawngroup_id, npcthis = nullptr; enabled = in_enabled; this->anim = anim; + currentnpcid = 0; if(timeleft == 0xFFFFFFFF) { //special disable timeleft @@ -115,7 +118,7 @@ Spawn2::~Spawn2() uint32 Spawn2::resetTimer() { - uint32 rspawn = respawn_ * 1000; + uint32 rspawn = m_respawn_time * 1000; if (variance_ != 0) { int var_over_2 = (variance_ * 1000) / 2; @@ -150,12 +153,12 @@ uint32 Spawn2::despawnTimer(uint32 despawn_timer) bool Spawn2::Process() { IsDespawned = false; - if (!Enabled()) + if (!Enabled()) { return true; + } //grab our spawn group SpawnGroup *spawn_group = zone->spawn_group_list.GetSpawnGroup(spawngroup_id_); - if (NPCPointerValid() && (spawn_group && spawn_group->despawn == 0 || condition_id != 0)) { return true; } @@ -195,7 +198,7 @@ bool Spawn2::Process() { } //have the spawn group pick an NPC for us - uint32 npcid = spawn_group->GetNPCType(condition_value); + uint32 npcid = currentnpcid && currentnpcid > 0 ? currentnpcid : spawn_group->GetNPCType(condition_value); if (npcid == 0) { LogSpawns("Spawn2 [{}]: Spawn group [{}] did not yeild an NPC! not spawning", spawn2_id, spawngroup_id_); @@ -267,10 +270,12 @@ bool Spawn2::Process() { NPC *npc = new NPC(tmp, this, glm::vec4(x, y, z, heading), GravityBehavior::Water); npcthis = npc; + npc->AddLootTable(); if (npc->DropsGlobalLoot()) { npc->CheckGlobalLootTables(); } + npc->SetSpawnGroupId(spawngroup_id_); npc->SaveGuardPointAnim(anim); npc->SetAppearance((EmuAppearance) anim); @@ -500,6 +505,12 @@ bool ZoneDatabase::PopulateZoneSpawnList(uint32 zoneid, LinkedList &spa NPC::SpawnZoneController(); + if (RuleB(Zone, StateSavingOnShutdown) && zone->LoadZoneState(spawn_times, disabled_spawns)) { + LogZoneState("Loaded zone state for zone [{}] instance_id [{}]", zone_name, zone->GetInstanceID()); + return true; + } + + // normal spawn2 loading for (auto &s: spawns) { uint32 spawn_time_left = 0; if (spawn_times.count(s.id) != 0) { diff --git a/zone/spawn2.h b/zone/spawn2.h index e16fb4a72..f39184757 100644 --- a/zone/spawn2.h +++ b/zone/spawn2.h @@ -55,10 +55,10 @@ public: float GetZ() { return z; } float GetHeading() { return heading; } bool PathWhenZoneIdle() { return path_when_zone_idle; } - void SetRespawnTimer(uint32 newrespawntime) { respawn_ = newrespawntime; }; + void SetRespawnTimer(uint32 newrespawntime) { m_respawn_time = newrespawntime; }; void SetVariance(uint32 newvariance) { variance_ = newvariance; } const uint32 GetVariance() const { return variance_; } - uint32 RespawnTimer() { return respawn_; } + uint32 RespawnTimer() { return m_respawn_time; } uint32 SpawnGroupID() { return spawngroup_id_; } uint32 CurrentNPCID() { return currentnpcid; } void SetCurrentNPCID(uint32 nid) { currentnpcid = nid; } @@ -69,13 +69,19 @@ public: void SetNPCPointerNull() { npcthis = nullptr; } Timer GetTimer() { return timer; } void SetTimer(uint32 duration) { timer.Start(duration); } - uint32 GetKillCount() { return killcount; } + uint32 GetKillCount() { return killcount; } + uint32 GetGrid() const { return grid_; } + bool GetPathWhenZoneIdle() const { return path_when_zone_idle; } + int16 GetConditionMinValue() const { return condition_min_value; } + int16 GetAnimation () { return anim; } + inline NPC *GetNPC() const { return npcthis; } + protected: friend class Zone; Timer timer; private: - uint32 spawn2_id; - uint32 respawn_; + uint32 spawn2_id; + uint32 m_respawn_time; uint32 resetTimer(); uint32 despawnTimer(uint32 despawn_timer); diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index d181d8db4..816e4e7ef 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -591,7 +591,7 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) auto *s = (ServerZoneStateChange_Struct *) pack->pBuffer; LogInfo("Zone shutdown by {}.", s->admin_name); - Zone::Shutdown(); + zone->Shutdown(); } break; } diff --git a/zone/zone.cpp b/zone/zone.cpp index c3a595229..a62473266 100644 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -64,6 +64,7 @@ #include "../common/repositories/ldon_trap_templates_repository.h" #include "../common/repositories/respawn_times_repository.h" #include "../common/repositories/npc_emotes_repository.h" +#include "../common/repositories/zone_state_spawns_repository.h" #include "../common/serverinfo.h" #include "../common/repositories/merc_stance_entries_repository.h" #include "../common/repositories/alternate_currency_repository.h" @@ -880,57 +881,66 @@ void Zone::Shutdown(bool quiet) } DataBucket::DeleteCachedBuckets(DataBucketLoadType::Zone, zone->GetZoneID(), zone->GetInstanceID()); + // save and kick all clients + for (auto c : entity_list.GetClientList()) { + c.second->Save(); + c.second->WorldKick(); + } + + if (RuleB(Zone, StateSavingOnShutdown)) { + SaveZoneState(); + } entity_list.StopMobAI(); std::map::iterator itr; - while (!zone->npctable.empty()) { - itr = zone->npctable.begin(); + while (!npctable.empty()) { + itr = npctable.begin(); delete itr->second; itr->second = nullptr; - zone->npctable.erase(itr); + npctable.erase(itr); } - while (!zone->merctable.empty()) { - itr = zone->merctable.begin(); + while (!merctable.empty()) { + itr = merctable.begin(); delete itr->second; itr->second = nullptr; - zone->merctable.erase(itr); + merctable.erase(itr); } - zone->adventure_entry_list_flavor.clear(); + adventure_entry_list_flavor.clear(); std::map::iterator itr4; - while (!zone->ldon_trap_list.empty()) { - itr4 = zone->ldon_trap_list.begin(); + while (!ldon_trap_list.empty()) { + itr4 = ldon_trap_list.begin(); delete itr4->second; itr4->second = nullptr; - zone->ldon_trap_list.erase(itr4); + ldon_trap_list.erase(itr4); } - zone->ldon_trap_entry_list.clear(); + ldon_trap_entry_list.clear(); LogInfo( "Zone [{}] zone_id [{}] version [{}] instance_id [{}]", - zone->GetShortName(), - zone->GetZoneID(), - zone->GetInstanceVersion(), - zone->GetInstanceID() + GetShortName(), + GetZoneID(), + GetInstanceVersion(), + GetInstanceID() ); petition_list.ClearPetitions(); - zone->SetZoneHasCurrentTime(false); + SetZoneHasCurrentTime(false); if (!quiet) { LogInfo( "Zone [{}] zone_id [{}] version [{}] instance_id [{}] Going to sleep", - zone->GetShortName(), - zone->GetZoneID(), - zone->GetInstanceVersion(), - zone->GetInstanceID() + GetShortName(), + GetZoneID(), + GetInstanceVersion(), + GetInstanceID() ); } is_zone_loaded = false; - zone->ResetAuth(); + ResetAuth(); safe_delete(zone); entity_list.ClearAreas(); parse->ReloadQuests(true); @@ -1099,6 +1109,8 @@ Zone::Zone(uint32 in_zoneid, uint32 in_instanceid, const char* in_short_name) } Zone::~Zone() { + LogInfo("Zone destructor called for zone [{}]", short_name); + spawn2_list.Clear(); if (worldserver.Connected()) { worldserver.SetZoneData(0); @@ -1926,6 +1938,10 @@ void Zone::Repop(bool is_forced) spawn_conditions.LoadSpawnConditions(short_name, instanceid); + if (RuleB(Zone, StateSavingOnShutdown)) { + ClearZoneState(zoneid, instanceid); + } + if (!content_db.PopulateZoneSpawnList(zoneid, spawn2_list, GetInstanceVersion())) { LogDebug("Error in Zone::Repop: database.PopulateZoneSpawnList failed"); } @@ -3192,7 +3208,7 @@ std::string Zone::GetBucketRemaining(const std::string& bucket_name) void Zone::DisableRespawnTimers() { - LinkedListIterator e(spawn2_list); + LinkedListIterator e(spawn2_list); e.Reset(); @@ -3202,4 +3218,5 @@ void Zone::DisableRespawnTimers() } } +#include "zone_save_state.cpp" #include "zone_loot.cpp" diff --git a/zone/zone.h b/zone/zone.h index d11799387..2e2bee4b7 100755 --- a/zone/zone.h +++ b/zone/zone.h @@ -47,6 +47,8 @@ #include "../common/repositories/lootdrop_entries_repository.h" #include "../common/repositories/base_data_repository.h" #include "../common/repositories/skill_caps_repository.h" +#include "../common/repositories/zone_state_spawns_repository.h" +#include "../common/repositories/spawn2_disabled_repository.h" struct EXPModifier { @@ -104,7 +106,7 @@ class MobMovementManager; class Zone { public: static bool Bootup(uint32 iZoneID, uint32 iInstanceID, bool is_static = false); - static void Shutdown(bool quiet = false); + void Shutdown(bool quiet = false); Zone(uint32 in_zoneid, uint32 in_instanceid, const char *in_short_name); ~Zone(); @@ -438,6 +440,7 @@ public: // loot void LoadLootTable(const uint32 loottable_id); void LoadLootTables(const std::vector in_loottable_ids); + void LoadLootDrops(const std::vector in_lootdrop_ids); void ClearLootTables(); void ReloadLootTables(); LoottableRepository::Loottable *GetLootTable(const uint32 loottable_id); @@ -460,6 +463,14 @@ public: inline void SetZoneServerId(uint32 id) { m_zone_server_id = id; } inline uint32 GetZoneServerId() const { return m_zone_server_id; } + // zone state + bool LoadZoneState( + std::unordered_map spawn_times, + std::vector disabled_spawns + ); + void SaveZoneState(); + static void ClearZoneState(uint32 zone_id, uint32 instance_id); + private: bool allow_mercs; bool can_bind; diff --git a/zone/zone_loot.cpp b/zone/zone_loot.cpp index 697a2634f..b6457d4b6 100644 --- a/zone/zone_loot.cpp +++ b/zone/zone_loot.cpp @@ -300,3 +300,92 @@ std::vector Zone::GetLootdropEntries return entries; } +void Zone::LoadLootDrops(const std::vector in_lootdrop_ids) +{ + BenchTimer timer; + + // copy lootdrop_ids + std::vector lootdrop_ids = in_lootdrop_ids; + + // check if lootdrop is already loaded + std::vector loaded_drops = {}; + for (const auto &e: lootdrop_ids) { + for (const auto &f: m_lootdrops) { + if (e == f.id) { + LogLootDetail("Lootdrop [{}] already loaded", e); + loaded_drops.push_back(e); + } + } + } + + // remove loaded drops from lootdrop_ids + for (const auto &e: loaded_drops) { + lootdrop_ids.erase( + std::remove( + lootdrop_ids.begin(), + lootdrop_ids.end(), + e + ), + lootdrop_ids.end() + ); + } + + if (lootdrop_ids.empty()) { + LogLootDetail("No lootdrops to load"); + return; + } + + auto lootdrops = LootdropRepository::GetWhere( + content_db, + fmt::format( + "id IN ({})", + Strings::Join(lootdrop_ids, ",") + ) + ); + + auto lootdrop_entries = LootdropEntriesRepository::GetWhere( + content_db, + fmt::format( + "lootdrop_id IN ({})", + Strings::Join(lootdrop_ids, ",") + ) + ); + + // emplace back drops to m_lootdrops if not exists + for (const auto &e: lootdrops) { + bool has_drop = false; + + for (const auto &l: m_lootdrops) { + if (e.id == l.id) { + has_drop = true; + break; + } + } + + bool has_entry = false; + if (!has_drop) { + // add lootdrop + m_lootdrops.emplace_back(e); + + // add lootdrop entries + for (const auto &f: lootdrop_entries) { + if (e.id == f.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) { + has_entry = true; + break; + } + } + + } + } + } + } + + if (!lootdrop_ids.empty()) { + LogInfo("Loaded [{}] lootdrops ({}s)", m_lootdrops.size(), std::to_string(timer.elapsed())); + } +} diff --git a/zone/zone_save_state.cpp b/zone/zone_save_state.cpp new file mode 100644 index 000000000..18b4a2429 --- /dev/null +++ b/zone/zone_save_state.cpp @@ -0,0 +1,540 @@ +#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; + 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) + ); + } +}; + +inline void LoadLootStateData(Zone *zone, NPC *npc, const std::string &loot_data) +{ + LootStateData l{}; + std::stringstream ss; + { + ss << loot_data; + cereal::JSONInputArchive ar(ss); + l.serialize(ar); + } + + npc->AddLootCash(l.copper, l.silver, l.gold, l.platinum); + + for (auto &e: l.entries) { + const auto *db_item = database.GetItem(e.item_id); + if (!db_item) { + continue; + } + + // dynamically added via AddItem + if (e.lootdrop_id == 0) { + npc->AddItem(e.item_id, e.charges); + continue; + } + + const auto entries = zone->GetLootdropEntries(e.lootdrop_id); + if (entries.empty()) { + continue; + } + + LootdropEntriesRepository::LootdropEntries lootdrop_entry; + for (auto &le: entries) { + if (e.item_id == le.item_id) { + lootdrop_entry = le; + break; + } + } + + npc->AddLootDrop(db_item, lootdrop_entry); + } +} + +inline std::string GetLootSerialized(NPC *npc) +{ + LootStateData ls = {}; + auto loot_items = npc->GetLootItems(); // Assuming this returns a list of loot items + ls.copper = npc->GetCopper(); + ls.silver = npc->GetSilver(); + ls.gold = npc->GetGold(); + ls.platinum = npc->GetPlatinum(); + ls.entries.reserve(loot_items.size()); + + for (auto &l: loot_items) { + ls.entries.emplace_back( + LootEntryStateData{ + .item_id = l->item_id, + .lootdrop_id = l->lootdrop_id, + .charges = l->charges, + } + ); + } + + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + ls.serialize(ar); + } + + return ss.str(); +} + +inline std::string GetLootSerialized(Corpse *c) +{ + LootStateData ls = {}; + auto loot_items = c->GetLootItems(); // Assuming this returns a list of loot items + ls.copper = c->GetCopper(); + ls.silver = c->GetSilver(); + ls.gold = c->GetGold(); + ls.platinum = c->GetPlatinum(); + ls.entries.reserve(loot_items.size()); + + for (auto &l: loot_items) { + ls.entries.emplace_back( + LootEntryStateData{ + .item_id = l->item_id, + .lootdrop_id = l->lootdrop_id, + } + ); + } + + std::stringstream ss; + { + cereal::JSONOutputArchiveSingleLine ar(ss); + ls.serialize(ar); + } + + return ss.str(); +} + +inline void LoadNPCEntityVariables(NPC *n, const std::string &entity_variables) +{ + std::map deserialized_map; + try { + std::istringstream is(entity_variables); + { + cereal::JSONInputArchive archive(is); + archive(deserialized_map); + } + } + catch (const std::exception &e) { + LogZoneState("Failed to load entity variables for NPC [{}] [{}]", n->GetNPCTypeID(), e.what()); + return; + } + + for (const auto &[key, value]: deserialized_map) { + n->SetEntityVariable(key, value); + } +} + +inline void LoadNPCBuffs(NPC *n, const std::string &buffs) +{ + std::vector valid_buffs; + try { + std::istringstream is(buffs); + { + cereal::JSONInputArchive archive(is); + archive(cereal::make_nvp("buffs", valid_buffs)); + } + } + catch (const std::exception &e) { + LogZoneState("Failed to load entity variables for NPC [{}] [{}]", n->GetNPCTypeID(), e.what()); + return; + } + + for (const auto &b: valid_buffs) { + // int AddBuff(Mob *caster, const uint16 spell_id, int duration = 0, int32 level_override = -1, bool disable_buff_overwrite = false); + n->AddBuff(n, b.spellid, b.ticsremaining, b.casterlevel, false); + } +} + +inline std::vector GetLootdropIds(const std::vector &spawn_states) +{ + LogInfo("Loading lootdrop ids for zone state spawns"); + + std::vector lootdrop_ids; + for (auto &s: spawn_states) { + if (s.loot_data.empty()) { + continue; + } + + LootStateData l{}; + try { + std::stringstream ss; + { + ss << s.loot_data; + cereal::JSONInputArchive ar(ss); + l.serialize(ar); + } + } + catch (const std::exception &e) { + LogZoneState("Failed to load loot state data for spawn2 [{}] [{}]", s.id, e.what()); + continue; + } + + for (auto &e: l.entries) { + // make sure it isn't already in the list + if (std::find(lootdrop_ids.begin(), lootdrop_ids.end(), e.lootdrop_id) == lootdrop_ids.end()) { + lootdrop_ids.push_back(e.lootdrop_id); + } + } + } + + LogInfo("Loaded [{}] lootdrop id(s)", lootdrop_ids.size()); + + return lootdrop_ids; +} + +inline void LoadNPCState(Zone *zone, NPC *n, ZoneStateSpawnsRepository::ZoneStateSpawns &s) +{ + n->SetHP(s.hp); + n->SetMana(s.mana); + n->SetEndurance(s.endurance); + + if (s.grid) { + n->AssignWaypoints(s.grid, s.current_waypoint); + } + + LoadLootStateData(zone, n, s.loot_data); + LoadNPCEntityVariables(n, s.entity_variables); + LoadNPCBuffs(n, s.buffs); + + if (s.is_corpse) { + auto decay_time = s.decay_in_seconds * 1000; + if (decay_time > 0) { + n->SetQueuedToCorpse(); + n->SetCorpseDecayTime(decay_time); + } + else { + n->Depop(); + } + } + + n->SetResumedFromZoneSuspend(true); +} + +bool Zone::LoadZoneState( + std::unordered_map spawn_times, + std::vector disabled_spawns +) +{ + auto spawn_states = ZoneStateSpawnsRepository::GetWhere( + database, + fmt::format( + "zone_id = {} AND instance_id = {}", + zoneid, + zone->GetInstanceID() + ) + ); + + LogInfo("Loading zone state spawns for zone [{}] spawns [{}]", GetShortName(), spawn_states.size()); + + std::vector lootdrop_ids = GetLootdropIds(spawn_states); + zone->LoadLootDrops(lootdrop_ids); + + // we have to load grids first otherwise setting grid/wp will not work + zone->initgrids_timer.Trigger(); + zone->Process(); + + for (auto &s: spawn_states) { + if (s.spawngroup_id == 0) { + continue; + } + + if (s.is_corpse) { + continue; + } + + uint32 spawn_time_left = 0; + if (spawn_times.count(s.spawn2_id) != 0) { + spawn_time_left = spawn_times[s.spawn2_id]; + LogInfo("Spawn2 [{}] Respawn time left [{}]", s.spawn2_id, spawn_time_left); + } + + // load from spawn2_disabled + bool spawn_enabled = true; + + // check if spawn is disabled + for (auto &ds: disabled_spawns) { + if (ds.spawn2_id == s.spawn2_id) { + spawn_enabled = !ds.disabled; + } + } + + auto new_spawn = new Spawn2( + s.spawn2_id, + s.spawngroup_id, + s.x, + s.y, + s.z, + s.heading, + s.respawn_time, + s.variance, + spawn_time_left, + s.grid, + (bool) s.path_when_zone_idle, + s.condition_id, + s.condition_min_value, + spawn_enabled, + (EmuAppearance) s.anim + ); + + if (spawn_time_left == 0) { + new_spawn->SetCurrentNPCID(s.npc_id); + } + + spawn2_list.Insert(new_spawn); + new_spawn->Process(); + auto n = new_spawn->GetNPC(); + if (n) { + n->ClearLootItems(); + if (s.grid > 0) { + n->AssignWaypoints(s.grid, s.current_waypoint); + } + } + } + + // dynamic spawns, quest spawns, triggers etc. + for (auto &s: spawn_states) { + if (s.spawngroup_id > 0) { + continue; + } + + auto npc_type = content_db.LoadNPCTypesData(s.npc_id); + if (!npc_type) { + LogZoneState("Failed to load NPC type data for npc_id [{}]", s.npc_id); + continue; + } + + auto npc = new NPC( + npc_type, + nullptr, + glm::vec4(s.x, s.y, s.z, s.heading), + GravityBehavior::Water + ); + + entity_list.AddNPC(npc, true, true); + + 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(); +} + +inline void SaveNPCState(NPC *n, ZoneStateSpawnsRepository::ZoneStateSpawns &s) +{ + // entity variables + std::map variables; + for (const auto &k: n->GetEntityVariables()) { + variables[k] = n->GetEntityVariable(k); + } + + std::ostringstream os; + { + cereal::JSONOutputArchiveSingleLine archive(os); + archive(variables); + } + + s.entity_variables = os.str(); + + // buffs + auto buffs = n->GetBuffs(); + if (!buffs) { + return; + } + + std::vector valid_buffs; + + for (int index = 0; index < n->GetMaxBuffSlots(); index++) { + if (buffs[index].spellid != 0 && buffs[index].spellid != 65535) { + valid_buffs.push_back(buffs[index]); + } + } + + try { + os = std::ostringstream(); + { + cereal::JSONOutputArchiveSingleLine archive(os); + archive(cereal::make_nvp("buffs", valid_buffs)); + } + } + catch (const std::exception &e) { + LogZoneState("Failed to serialize buffs for NPC [{}] [{}]", n->GetNPCTypeID(), e.what()); + return; + } + + s.buffs = os.str(); + + // rest + s.npc_id = n->GetNPCTypeID(); + s.loot_data = GetLootSerialized(n); + s.hp = n->GetHP(); + s.mana = n->GetMana(); + s.endurance = n->GetEndurance(); + s.grid = n->GetGrid(); + s.current_waypoint = n->GetGrid() > 0 ? n->GetCWP() : 0; + s.x = n->GetX(); + s.y = n->GetY(); + s.z = n->GetZ(); + s.heading = n->GetHeading(); + s.created_at = std::time(nullptr); +} + +void Zone::SaveZoneState() +{ + // spawns + std::vector spawns = {}; + LinkedListIterator iterator(spawn2_list); + iterator.Reset(); + while (iterator.MoreElements()) { + Spawn2 *sp = iterator.GetData(); + auto s = ZoneStateSpawnsRepository::NewEntity(); + s.zone_id = GetZoneID(); + s.instance_id = GetInstanceID(); + s.npc_id = sp->CurrentNPCID(); + s.spawn2_id = sp->GetID(); + s.spawngroup_id = sp->SpawnGroupID(); + s.x = sp->GetX(); + s.y = sp->GetY(); + s.z = sp->GetZ(); + s.heading = sp->GetHeading(); + s.respawn_time = sp->RespawnTimer(); + s.variance = sp->GetVariance(); + s.grid = sp->GetGrid(); + s.path_when_zone_idle = sp->GetPathWhenZoneIdle() ? 1 : 0; + s.condition_id = sp->GetSpawnCondition(); + s.condition_min_value = sp->GetConditionMinValue(); + s.enabled = sp->Enabled() ? 1 : 0; + s.anim = sp->GetAnimation(); + s.created_at = std::time(nullptr); + + auto n = sp->GetNPC(); + if (n) { + SaveNPCState(n, s); + } + + spawns.emplace_back(s); + iterator.Advance(); + } + + // npcs that are not in the spawn2 list + for (auto &n: entity_list.GetNPCList()) { + // everything below here is dynamically spawned + bool ignore_npcs = + n.second->GetSpawnGroupId() > 0 || + n.second->GetNPCTypeID() < 100 || + n.second->HasOwner(); + if (ignore_npcs) { + continue; + } + + auto s = ZoneStateSpawnsRepository::NewEntity(); + s.zone_id = GetZoneID(); + s.instance_id = GetInstanceID(); + + SaveNPCState(n.second, s); + + spawns.emplace_back(s); + } + + // corpses + for (auto &n: entity_list.GetCorpseList()) { + if (!n.second->IsNPCCorpse()) { + continue; + } + + auto s = ZoneStateSpawnsRepository::NewEntity(); + s.zone_id = GetZoneID(); + s.instance_id = GetInstanceID(); + s.npc_id = n.second->GetNPCTypeID(); + s.is_corpse = 1; + s.x = n.second->GetX(); + s.y = n.second->GetY(); + s.z = n.second->GetZ(); + s.heading = n.second->GetHeading(); + s.created_at = std::time(nullptr); + s.loot_data = GetLootSerialized(n.second); + s.decay_in_seconds = (int) (n.second->GetDecayTime() / 1000); + + spawns.emplace_back(s); + } + + ZoneStateSpawnsRepository::DeleteWhere( + database, + fmt::format( + "`zone_id` = {} AND `instance_id` = {}", + GetZoneID(), + GetInstanceID() + ) + ); + + ZoneStateSpawnsRepository::InsertMany(database, spawns); + + LogInfo("Saved [{}] zone state spawns", Strings::Commify(spawns.size())); +} + +void Zone::ClearZoneState(uint32 zone_id, uint32 instance_id) +{ + ZoneStateSpawnsRepository::DeleteWhere( + database, + fmt::format( + "`zone_id` = {} AND `instance_id` = {}", + zone_id, + instance_id + ) + ); +}