From 83ff1a840d28f22140055ba5a8febed6114e04d9 Mon Sep 17 00:00:00 2001 From: Vayle Date: Mon, 9 Mar 2026 12:14:37 -0400 Subject: [PATCH 1/5] Preserve suppressed pet buffs across zoning --- common/database/database_update_manifest.h | 12 ++ .../character_pet_buffs_repository.h | 118 +++++++++++++++++- common/version.h | 2 +- ..._add_suppressed_to_character_pet_buffs.sql | 3 + zone/pets.cpp | 43 +++++-- zone/zonedb.cpp | 33 +++-- 6 files changed, 191 insertions(+), 20 deletions(-) create mode 100644 utils/sql/git/optional/2026_03_09_add_suppressed_to_character_pet_buffs.sql diff --git a/common/database/database_update_manifest.h b/common/database/database_update_manifest.h index 1cce442f0..9897ab203 100644 --- a/common/database/database_update_manifest.h +++ b/common/database/database_update_manifest.h @@ -7199,6 +7199,18 @@ ALTER TABLE `character_parcels_containers` .sql = R"( ALTER TABLE `character_buffs` ADD COLUMN `suppressed` tinyint(1) unsigned NOT NULL DEFAULT 0 AFTER `instrument_mod`; +)", + .content_schema_update = false + }, + ManifestEntry{ + .version = 9330, + .description = "2026_03_09_add_suppressed_to_character_pet_buffs.sql", + .check = "SHOW COLUMNS FROM `character_pet_buffs` LIKE 'suppressed'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `character_pet_buffs` + ADD COLUMN `suppressed` tinyint(1) unsigned NOT NULL DEFAULT 0 AFTER `instrument_mod`; )", .content_schema_update = false }, diff --git a/common/repositories/character_pet_buffs_repository.h b/common/repositories/character_pet_buffs_repository.h index a0fcd4b4c..5e6fc609e 100644 --- a/common/repositories/character_pet_buffs_repository.h +++ b/common/repositories/character_pet_buffs_repository.h @@ -7,6 +7,20 @@ class CharacterPetBuffsRepository: public BaseCharacterPetBuffsRepository { public: + struct CharacterPetBuffsWithSuppressed { + int32_t char_id; + int32_t pet; + int32_t slot; + int32_t spell_id; + int8_t caster_level; + std::string castername; + int32_t ticsremaining; + int32_t counters; + int32_t numhits; + int32_t rune; + uint8_t instrument_mod; + uint8_t suppressed; + }; /** * This file was auto generated and can be modified and extended upon @@ -41,8 +55,110 @@ public: * find yourself re-using logic for other parts of the code, its best to just make a * method that can be re-used easily elsewhere especially if it can use a base repository * method and encapsulate filters there - */ + */ // Custom extended repository methods here + static CharacterPetBuffsWithSuppressed NewEntityWithSuppressed() + { + return CharacterPetBuffsWithSuppressed{ + .char_id = 0, + .pet = 0, + .slot = 0, + .spell_id = 0, + .caster_level = 0, + .castername = "", + .ticsremaining = 0, + .counters = 0, + .numhits = 0, + .rune = 0, + .instrument_mod = 10, + .suppressed = 0, + }; + } + + static std::vector GetWhereWithSuppressed( + Database& db, + const std::string& where_filter + ) + { + std::vector entries; + + auto results = db.QueryDatabase( + fmt::format( + "SELECT {}, `suppressed` FROM {} WHERE {}", + SelectColumnsRaw(), + TableName(), + where_filter + ) + ); + + entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + auto e = NewEntityWithSuppressed(); + + e.char_id = row[0] ? static_cast(atoi(row[0])) : 0; + e.pet = row[1] ? static_cast(atoi(row[1])) : 0; + e.slot = row[2] ? static_cast(atoi(row[2])) : 0; + e.spell_id = row[3] ? static_cast(atoi(row[3])) : 0; + e.caster_level = row[4] ? static_cast(atoi(row[4])) : 0; + e.castername = row[5] ? row[5] : ""; + e.ticsremaining = row[6] ? static_cast(atoi(row[6])) : 0; + e.counters = row[7] ? static_cast(atoi(row[7])) : 0; + e.numhits = row[8] ? static_cast(atoi(row[8])) : 0; + e.rune = row[9] ? static_cast(atoi(row[9])) : 0; + e.instrument_mod = row[10] ? static_cast(strtoul(row[10], nullptr, 10)) : 10; + e.suppressed = row[11] ? static_cast(strtoul(row[11], nullptr, 10)) : 0; + + entries.emplace_back(e); + } + + return entries; + } + + static int InsertManyWithSuppressed( + Database& db, + const std::vector& entries + ) + { + if (entries.empty()) { + return 0; + } + + std::vector insert_chunks; + insert_chunks.reserve(entries.size()); + + for (const auto& e : entries) { + std::vector values; + values.reserve(12); + + values.push_back(std::to_string(e.char_id)); + values.push_back(std::to_string(e.pet)); + values.push_back(std::to_string(e.slot)); + values.push_back(std::to_string(e.spell_id)); + values.push_back(std::to_string(e.caster_level)); + values.push_back("'" + Strings::Escape(e.castername) + "'"); + values.push_back(std::to_string(e.ticsremaining)); + values.push_back(std::to_string(e.counters)); + values.push_back(std::to_string(e.numhits)); + values.push_back(std::to_string(e.rune)); + values.push_back(std::to_string(e.instrument_mod)); + values.push_back(std::to_string(e.suppressed)); + + insert_chunks.push_back("(" + Strings::Implode(",", values) + ")"); + } + + auto results = db.QueryDatabase( + fmt::format( + "INSERT INTO {} ({}, `suppressed`) VALUES {}", + TableName(), + ColumnsRaw(), + Strings::Implode(",", insert_chunks) + ) + ); + + return results.Success() ? results.RowsAffected() : 0; + } + }; diff --git a/common/version.h b/common/version.h index 34958bcef..a611351d5 100644 --- a/common/version.h +++ b/common/version.h @@ -41,6 +41,6 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9328 +#define CURRENT_BINARY_DATABASE_VERSION 9330 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054 #define CUSTOM_BINARY_DATABASE_VERSION 0 diff --git a/utils/sql/git/optional/2026_03_09_add_suppressed_to_character_pet_buffs.sql b/utils/sql/git/optional/2026_03_09_add_suppressed_to_character_pet_buffs.sql new file mode 100644 index 000000000..488e152ed --- /dev/null +++ b/utils/sql/git/optional/2026_03_09_add_suppressed_to_character_pet_buffs.sql @@ -0,0 +1,3 @@ +-- Add suppressed column to character_pet_buffs to persist pet buff suppression state across zones +ALTER TABLE `character_pet_buffs` + ADD COLUMN `suppressed` tinyint(1) unsigned NOT NULL DEFAULT 0 AFTER `instrument_mod`; diff --git a/zone/pets.cpp b/zone/pets.cpp index d4eec4dbe..18d6c55f8 100644 --- a/zone/pets.cpp +++ b/zone/pets.cpp @@ -481,14 +481,17 @@ void NPC::GetPetState(SpellBuff_Struct *pet_buffs, uint32 *items, char *name) { //save their buffs. for (int i=EQ::invslot::EQUIPMENT_BEGIN; i < GetPetMaxTotalSlots(); i++) { - if (IsValidSpell(buffs[i].spellid)) { - pet_buffs[i].spellid = buffs[i].spellid; + if (IsValidOrSuppressedSpell(buffs[i].spellid)) { + const bool suppressed = buffs[i].spellid == SPELL_SUPPRESSED; + + pet_buffs[i].spellid = suppressed ? SPELL_SUPPRESSED : buffs[i].spellid; pet_buffs[i].effect_type = i+1; - pet_buffs[i].duration = buffs[i].ticsremaining; + pet_buffs[i].duration = suppressed ? buffs[i].suppressedticsremaining : buffs[i].ticsremaining; pet_buffs[i].level = buffs[i].casterlevel; pet_buffs[i].bard_modifier = 10; pet_buffs[i].counters = buffs[i].counters; pet_buffs[i].bard_modifier = buffs[i].instrument_mod; + pet_buffs[i].player_id = suppressed ? buffs[i].suppressedid : 0; } else { pet_buffs[i].spellid = SPELL_UNKNOWN; @@ -496,6 +499,7 @@ void NPC::GetPetState(SpellBuff_Struct *pet_buffs, uint32 *items, char *name) { pet_buffs[i].level = 0; pet_buffs[i].bard_modifier = 10; pet_buffs[i].counters = 0; + pet_buffs[i].player_id = 0; } } } @@ -505,32 +509,53 @@ void NPC::SetPetState(SpellBuff_Struct *pet_buffs, uint32 *items) { int i; for (i = 0; i < GetPetMaxTotalSlots(); i++) { + const bool suppressed = pet_buffs[i].spellid == SPELL_SUPPRESSED; + uint32 restored_spell_id = suppressed ? pet_buffs[i].player_id : pet_buffs[i].spellid; + for(int z = 0; z < GetPetMaxTotalSlots(); z++) { - // check for duplicates - if(IsValidSpell(buffs[z].spellid) && buffs[z].spellid == pet_buffs[i].spellid) { + // check for duplicates + if (!IsValidOrSuppressedSpell(buffs[z].spellid)) { + continue; + } + + const uint32 existing_spell_id = buffs[z].spellid == SPELL_SUPPRESSED ? buffs[z].suppressedid : buffs[z].spellid; + if (existing_spell_id == restored_spell_id) { buffs[z].spellid = SPELL_UNKNOWN; + buffs[z].suppressedid = 0; + buffs[z].suppressedticsremaining = -1; pet_buffs[i].spellid = 0xFFFFFFFF; + pet_buffs[i].player_id = 0; + restored_spell_id = 0xFFFFFFFF; } } - if (pet_buffs[i].spellid <= (uint32)SPDAT_RECORDS && pet_buffs[i].spellid != 0 && (pet_buffs[i].duration > 0 || pet_buffs[i].duration == -1)) { + if ( + IsValidSpell(restored_spell_id) && + (pet_buffs[i].duration > 0 || pet_buffs[i].duration == -1) + ) { if(pet_buffs[i].level == 0 || pet_buffs[i].level > 100) pet_buffs[i].level = 1; - buffs[i].spellid = pet_buffs[i].spellid; - buffs[i].ticsremaining = pet_buffs[i].duration; + + buffs[i].spellid = suppressed ? SPELL_SUPPRESSED : restored_spell_id; + buffs[i].ticsremaining = suppressed ? 0 : pet_buffs[i].duration; buffs[i].casterlevel = pet_buffs[i].level; buffs[i].casterid = 0; buffs[i].counters = pet_buffs[i].counters; - buffs[i].hit_number = spells[pet_buffs[i].spellid].hit_number; + buffs[i].hit_number = spells[restored_spell_id].hit_number; buffs[i].instrument_mod = pet_buffs[i].bard_modifier; + buffs[i].suppressedid = suppressed ? restored_spell_id : 0; + buffs[i].suppressedticsremaining = suppressed ? pet_buffs[i].duration : -1; } else { buffs[i].spellid = SPELL_UNKNOWN; + buffs[i].suppressedid = 0; + buffs[i].suppressedticsremaining = -1; pet_buffs[i].spellid = 0xFFFFFFFF; pet_buffs[i].effect_type = 0; pet_buffs[i].level = 0; pet_buffs[i].duration = 0; pet_buffs[i].bard_modifier = 0; + pet_buffs[i].player_id = 0; } } for (int j1=0; j1 < GetPetMaxTotalSlots(); j1++) { diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index b39203ce2..efb6ce92d 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -3128,8 +3128,8 @@ void ZoneDatabase::SavePetInfo(Client *client) std::vector pet_infos; auto pet_info = CharacterPetInfoRepository::NewEntity(); - std::vector pet_buffs; - auto pet_buff = CharacterPetBuffsRepository::NewEntity(); + std::vector pet_buffs; + auto pet_buff = CharacterPetBuffsRepository::NewEntityWithSuppressed(); std::vector inventory; auto item = CharacterPetInventoryRepository::NewEntity(); @@ -3161,7 +3161,7 @@ void ZoneDatabase::SavePetInfo(Client *client) ); for (int slot_id = 0; slot_id < max_slots; slot_id++) { - if (!IsValidSpell(p->Buffs[slot_id].spellid)) { + if (!IsValidOrSuppressedSpell(p->Buffs[slot_id].spellid)) { continue; } @@ -3171,18 +3171,22 @@ void ZoneDatabase::SavePetInfo(Client *client) pet_buffs.reserve(pet_buff_count); for (int slot_id = 0; slot_id < max_slots; slot_id++) { - if (!IsValidSpell(p->Buffs[slot_id].spellid)) { + if (!IsValidOrSuppressedSpell(p->Buffs[slot_id].spellid)) { continue; } + const bool suppressed = p->Buffs[slot_id].spellid == SPELL_SUPPRESSED; + pet_buff.char_id = client->CharacterID(); pet_buff.pet = pet_info_type; pet_buff.slot = slot_id; - pet_buff.spell_id = p->Buffs[slot_id].spellid; + pet_buff.spell_id = suppressed ? p->Buffs[slot_id].player_id : p->Buffs[slot_id].spellid; pet_buff.caster_level = p->Buffs[slot_id].level; pet_buff.ticsremaining = p->Buffs[slot_id].duration; pet_buff.counters = p->Buffs[slot_id].counters; + pet_buff.numhits = 0; pet_buff.instrument_mod = p->Buffs[slot_id].bard_modifier; + pet_buff.suppressed = suppressed ? 1 : 0; pet_buffs.push_back(pet_buff); } @@ -3242,7 +3246,16 @@ void ZoneDatabase::SavePetInfo(Client *client) ); if (!pet_buffs.empty()) { - CharacterPetBuffsRepository::InsertMany(database, pet_buffs); + const auto saved_count = CharacterPetBuffsRepository::InsertManyWithSuppressed(database, pet_buffs); + if (saved_count != static_cast(pet_buffs.size())) { + LogError( + "Failed to save all pet buffs for character [{}] [{}]. Expected [{}] rows, saved [{}]. Verify the `character_pet_buffs` schema is up to date.", + client->GetCleanName(), + client->CharacterID(), + pet_buffs.size(), + saved_count + ); + } } CharacterPetInventoryRepository::DeleteWhere( @@ -3332,7 +3345,7 @@ void ZoneDatabase::LoadPetInfo(Client *client) p->taunting = e.taunting; } - const auto& buffs = CharacterPetBuffsRepository::GetWhere( + const auto& buffs = CharacterPetBuffsRepository::GetWhereWithSuppressed( database, fmt::format( "`char_id` = {}", @@ -3358,9 +3371,11 @@ void ZoneDatabase::LoadPetInfo(Client *client) continue; } - p->Buffs[e.slot].spellid = e.spell_id; + const bool suppressed = e.suppressed != 0; + + p->Buffs[e.slot].spellid = suppressed ? SPELL_SUPPRESSED : e.spell_id; p->Buffs[e.slot].level = e.caster_level; - p->Buffs[e.slot].player_id = 0; + p->Buffs[e.slot].player_id = suppressed ? e.spell_id : 0; p->Buffs[e.slot].effect_type = BuffEffectType::Buff; p->Buffs[e.slot].duration = e.ticsremaining; p->Buffs[e.slot].counters = e.counters; From 1b4cc695e0afb6891fafab828a282ea78bc4ec70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:53:02 +0000 Subject: [PATCH 3/5] Fix pet_buffs.reserve to use cumulative size to avoid reallocations Co-authored-by: Valorith <76063792+Valorith@users.noreply.github.com> --- zone/zonedb.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index efb6ce92d..937f14adc 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -3168,7 +3168,7 @@ void ZoneDatabase::SavePetInfo(Client *client) pet_buff_count++; } - pet_buffs.reserve(pet_buff_count); + pet_buffs.reserve(pet_buffs.size() + pet_buff_count); for (int slot_id = 0; slot_id < max_slots; slot_id++) { if (!IsValidOrSuppressedSpell(p->Buffs[slot_id].spellid)) { From 80bd422922790ce2278ded14760e31087eabf39f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:11:24 +0000 Subject: [PATCH 5/5] Add PET_BUFF_COUNT guard on pet buff load to prevent out-of-bounds write Co-authored-by: Valorith <76063792+Valorith@users.noreply.github.com> --- zone/zonedb.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index efb6ce92d..f12f29425 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -3363,7 +3363,7 @@ void ZoneDatabase::LoadPetInfo(Client *client) continue; } - if (e.slot >= RuleI(Spells, MaxTotalSlotsPET)) { + if (e.slot >= RuleI(Spells, MaxTotalSlotsPET) || e.slot >= PET_BUFF_COUNT) { continue; }