Compare commits

...

10 Commits

Author SHA1 Message Date
Kitzunu
c276e0b530
Merge 591b5fc61f9c00230c5f5fd4c3a73b409f4ea94e into 835283bf26b02bd3c6e88505b123ad65aa9d0e98 2025-02-24 10:57:11 +01:00
Andrew
835283bf26
feat(Core/Scripting): Implement ScheduleEnrageTimer() helper (#21597) 2025-02-24 09:59:18 +01:00
Andrew
8f6d651471
fix(Scripts/SunwellPlateau): Felmyst should cast Noxious Cloud only a… (#21596) 2025-02-24 02:36:22 -03:00
sudlud
591b5fc61f
Merge branch 'master' into mail_server_template_items 2025-02-24 06:27:30 +01:00
github-actions[bot]
f6c29614d5 chore(DB): import pending files
Referenced commit(s): 96e7a20bd9325618b7adbd1381aa4e0e50bdba23
2025-02-24 05:19:56 +00:00
Andrew
96e7a20bd9
fix(DB/Creature): Fix Felmyst flying animation in p1 and add despawn on evade (#21396) 2025-02-24 06:18:55 +01:00
Kitzunu
cd8761796f
fix(CI/Codestyle): skip SQL keyword 'NOT' (#21591) 2025-02-24 06:16:20 +01:00
Kitzunu
2ed3a480b7 add check for no item count 2025-02-23 14:13:56 +01:00
Kitzunu
cc5950f068 fix sql error 2025-02-23 14:03:16 +01:00
Kitzunu
38149673de refactor(Core/Mail): Allow ServerMail to send multiple items
* Allow ServerMail to send multiple items by introducing new DB table mail_server_template_items

* Merge existing entries from mail_server_template to mail_server_template_items before dropping columns

* Add foreign keys to mail_server_character.mailId and mail_server_template_items.templateID to remove entries if the parent mail_server_template.id is removed

* Clean up and add early return for ObjectMgr::SendServerMail

* closes https://github.com/azerothcore/azerothcore-wotlk/issues/11446
2025-02-23 13:54:43 +01:00
11 changed files with 196 additions and 83 deletions

View File

@ -234,7 +234,7 @@ def backtick_check(file: io, file_path: str) -> None:
# Skip SQL keywords
if word.upper() in {"SELECT", "FROM", "JOIN", "WHERE", "GROUP", "BY", "ORDER",
"DELETE", "UPDATE", "INSERT", "INTO", "SET", "VALUES", "AND",
"IN", "OR", "REPLACE"}:
"IN", "OR", "REPLACE", "NOT"}:
continue
# Make sure the word is enclosed in backticks

View File

@ -0,0 +1,3 @@
-- DB update 2025_02_22_00 -> 2025_02_24_00
--
UPDATE `creature_template` SET `flags_extra` = `flags_extra`|512|2147483648 WHERE `entry` = 25038;

View File

@ -0,0 +1,36 @@
--
DROP TABLE IF EXISTS `mail_server_template_items`;
CREATE TABLE `mail_server_template_items` (
`id` INT UNSIGNED AUTO_INCREMENT,
`templateID` INT UNSIGNED NOT NULL,
`faction` ENUM('Alliance', 'Horde') NOT NULL,
`item` INT UNSIGNED NOT NULL,
`itemCount` INT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_mail_template`
FOREIGN KEY (`templateID`) REFERENCES `mail_server_template`(`id`)
ON DELETE CASCADE
) ENGINE=InnoDB COLLATE='utf8mb4_unicode_ci';
INSERT INTO `mail_server_template_items` (`templateID`, `faction`, `item`, `itemCount`)
SELECT `id`, 'Alliance', `itemA`, `itemCountA` FROM `mail_server_template` WHERE `itemA` > 0;
INSERT INTO `mail_server_template_items` (`templateID`, `faction`, `item`, `itemCount`)
SELECT `id`, 'Horde', `itemH`, `itemCountH` FROM `mail_server_template` WHERE `itemH` > 0;
ALTER TABLE `mail_server_template`
DROP COLUMN `itemA`,
DROP COLUMN `itemCountA`,
DROP COLUMN `itemH`,
DROP COLUMN `itemCountH`;
-- Make sure we dont have invalid instances in mail_server_character.mailId before we add the foregin key to avoid SQL errors
DELETE FROM `mail_server_character` WHERE `mailId` NOT IN (SELECT `id` FROM `mail_server_template`);
-- Add foreign key for mail_server_character.mailId
ALTER TABLE `mail_server_character`
DROP PRIMARY KEY,
ADD PRIMARY KEY (`guid`, `mailId`),
ADD CONSTRAINT `fk_mail_template_character`
FOREIGN KEY (`mailId`) REFERENCES `mail_server_template`(`id`)
ON DELETE CASCADE;

View File

@ -633,6 +633,7 @@ void BossAI::_Reset()
me->ResetLootMode();
events.Reset();
scheduler.CancelAll();
me->m_Events.KillAllEvents(false);
summons.DespawnAll();
ClearUniqueTimedEventsDone();
_healthCheckEvents.clear();
@ -787,6 +788,20 @@ void BossAI::ScheduleHealthCheckEvent(std::initializer_list<uint8> healthPct, st
_nextHealthCheck = _healthCheckEvents.front();
}
void BossAI::ScheduleEnrageTimer(uint32 spellId, Milliseconds timer, uint8 textId /*= 0*/)
{
me->m_Events.AddEventAtOffset([this, spellId, textId]
{
if (!me->IsAlive())
return;
if (textId)
Talk(textId);
DoCastSelf(spellId, true);
}, timer);
}
// WorldBossAI - for non-instanced bosses
WorldBossAI::WorldBossAI(Creature* creature) :

View File

@ -485,6 +485,12 @@ public:
void ScheduleHealthCheckEvent(uint32 healthPct, std::function<void()> exec);
void ScheduleHealthCheckEvent(std::initializer_list<uint8> healthPct, std::function<void()> exec);
// @brief Casts the spell after the fixed time and says the text id if provided. Timer will run even if the creature is casting or out of combat.
// @param spellId The spell to cast.
// @param timer The time to wait before casting the spell.
// @param textId The text id to say.
void ScheduleEnrageTimer(uint32 spellId, Milliseconds timer, uint8 textId = 0);
// Hook used to execute events scheduled into EventMap without the need
// to override UpdateAI
// note: You must re-schedule the event within this method if the event

View File

@ -10298,38 +10298,45 @@ uint32 ObjectMgr::GetQuestMoneyReward(uint8 level, uint32 questMoneyDifficulty)
return 0;
}
void ObjectMgr::SendServerMail(Player* player, uint32 id, uint32 reqLevel, uint32 reqPlayTime, uint32 rewardMoneyA, uint32 rewardMoneyH, uint32 rewardItemA, uint32 rewardItemCountA, uint32 rewardItemH, uint32 rewardItemCountH, std::string subject, std::string body, uint8 active) const
void ObjectMgr::SendServerMail(Player* player, uint32 id, uint32 reqLevel, uint32 reqPlayTime, uint32 rewardMoneyA, uint32 rewardMoneyH, std::vector<ServerMailItems> const& items, std::string subject, std::string body, uint8 active) const
{
if (active)
if (!active)
return;
if (player->GetLevel() < reqLevel)
return;
if (player->GetTotalPlayedTime() < reqPlayTime)
return;
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
MailSender sender(MAIL_NORMAL, player->GetGUID().GetCounter(), MAIL_STATIONERY_GM);
MailDraft draft(subject, body);
draft.AddMoney(player->GetTeamId() == TEAM_ALLIANCE ? rewardMoneyA : rewardMoneyH);
// Loop through all items and attach them to the mail
for (auto const& mailItem : items)
{
if (player->GetLevel() < reqLevel)
return;
if (!mailItem.item || !mailItem.itemCount)
continue;
if (player->GetTotalPlayedTime() < reqPlayTime)
return;
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
MailSender sender(MAIL_NORMAL, player->GetGUID().GetCounter(), MAIL_STATIONERY_GM);
MailDraft draft(subject, body);
draft.AddMoney(player->GetTeamId() == TEAM_ALLIANCE ? rewardMoneyA : rewardMoneyH);
if (Item* mailItem = Item::CreateItem(player->GetTeamId() == TEAM_ALLIANCE ? rewardItemA : rewardItemH, player->GetTeamId() == TEAM_ALLIANCE ? rewardItemCountA : rewardItemCountH))
if (Item* newItem = Item::CreateItem(mailItem.item, mailItem.itemCount))
{
mailItem->SaveToDB(trans);
draft.AddItem(mailItem);
newItem->SaveToDB(trans);
draft.AddItem(newItem);
}
draft.SendMailTo(trans, MailReceiver(player), sender);
CharacterDatabase.CommitTransaction(trans);
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_MAIL_SERVER_CHARACTER);
stmt->SetData(0, player->GetGUID().GetCounter());
stmt->SetData(1, id);
CharacterDatabase.Execute(stmt);
LOG_DEBUG("entities.player", "ObjectMgr::SendServerMail() Sent mail id {} to {}", id, player->GetGUID().ToString());
}
draft.SendMailTo(trans, MailReceiver(player), sender);
CharacterDatabase.CommitTransaction(trans);
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_MAIL_SERVER_CHARACTER);
stmt->SetData(0, player->GetGUID().GetCounter());
stmt->SetData(1, id);
CharacterDatabase.Execute(stmt);
LOG_DEBUG("entities.player", "ObjectMgr::SendServerMail() Sent mail id {} to {}", id, player->GetGUID().ToString());
}
void ObjectMgr::LoadMailServerTemplates()
@ -10338,8 +10345,8 @@ void ObjectMgr::LoadMailServerTemplates()
_serverMailStore.clear(); // for reload case
// 0 1 2 3 4 5 6 7 8 9 10 11
QueryResult result = CharacterDatabase.Query("SELECT `id`, `reqLevel`, `reqPlayTime`, `moneyA`, `moneyH`, `itemA`, `itemCountA`, `itemH`,`itemCountH`, `subject`, `body`, `active` FROM `mail_server_template`");
// 0 1 2 3 4 5 6 7
QueryResult result = CharacterDatabase.Query("SELECT `id`, `reqLevel`, `reqPlayTime`, `moneyA`, `moneyH`, `subject`, `body`, `active` FROM `mail_server_template`");
if (!result)
{
LOG_INFO("sql.sql", ">> Loaded 0 server mail rewards. DB table `mail_server_template` is empty.");
@ -10362,13 +10369,9 @@ void ObjectMgr::LoadMailServerTemplates()
servMail.reqPlayTime = fields[2].Get<uint32>();
servMail.moneyA = fields[3].Get<uint32>();
servMail.moneyH = fields[4].Get<uint32>();
servMail.itemA = fields[5].Get<uint32>();
servMail.itemCountA = fields[6].Get<uint32>();
servMail.itemH = fields[7].Get<uint32>();
servMail.itemCountH = fields[8].Get<uint32>();
servMail.subject = fields[9].Get<std::string>();
servMail.body = fields[10].Get<std::string>();
servMail.active = fields[11].Get<uint8>();
servMail.subject = fields[5].Get<std::string>();
servMail.body = fields[6].Get<std::string>();
servMail.active = fields[7].Get<uint8>();
if (servMail.reqLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL))
{
@ -10381,32 +10384,79 @@ void ObjectMgr::LoadMailServerTemplates()
LOG_ERROR("sql.sql", "Table `mail_server_template` has moneyA {} or moneyH {} larger than MAX_MONEY_AMOUNT {} for id {}, skipped.", servMail.moneyA, servMail.moneyH, MAX_MONEY_AMOUNT, servMail.id);
return;
}
ItemTemplate const* itemTemplateA = sObjectMgr->GetItemTemplate(servMail.itemA);
if (!itemTemplateA && servMail.itemA)
{
LOG_ERROR("sql.sql", "Table `mail_server_template` has invalid item in itemA {} for id {}, skipped.", servMail.itemA, servMail.id);
return;
}
ItemTemplate const* itemTemplateH = sObjectMgr->GetItemTemplate(servMail.itemH);
if (!itemTemplateH && servMail.itemH)
{
LOG_ERROR("sql.sql", "Table `mail_server_template` has invalid item in itemH {} for id {}, skipped.", servMail.itemH, servMail.id);
return;
}
if (!servMail.itemA && servMail.itemCountA)
{
LOG_ERROR("sql.sql", "Table `mail_server_template` has itemCountA {} with no ItemA, set to 0", servMail.itemCountA);
servMail.itemCountA = 0;
}
if (!servMail.itemH && servMail.itemCountH)
{
LOG_ERROR("sql.sql", "Table `mail_server_template` has itemCountH {} with no ItemH, set to 0", servMail.itemCountH);
servMail.itemCountH = 0;
}
} while (result->NextRow());
LoadMailServerTemplatesItems();
LOG_INFO("server.loading", ">> Loaded {} Mail Server Template in {} ms", _serverMailStore.size(), GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " ");
}
void ObjectMgr::LoadMailServerTemplatesItems()
{
QueryResult result = CharacterDatabase.Query("SELECT `templateID`, `faction`, `item`, `itemCount` FROM `mail_server_template_items`");
if (!result)
{
LOG_INFO("sql.sql", ">> Loaded 0 server mail items. DB table `mail_server_template_items` is empty.");
LOG_INFO("server.loading", " ");
return;
}
do
{
Field* fields = result->Fetch();
uint32 templateID = fields[0].Get<uint32>();
std::string faction = fields[1].Get<std::string>();
uint32 item = fields[2].Get<uint32>();
uint32 itemCount = fields[3].Get<uint32>();
if (_serverMailStore.find(templateID) == _serverMailStore.end())
{
LOG_ERROR("sql.sql", "Table `mail_server_template_items` has an invalid templateID {}, skipped.", templateID);
continue;
}
ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(item);
if (!itemTemplate)
{
LOG_ERROR("sql.sql", "Table `mail_server_template_items` has an invalid item {} for templateID {}, skipped.", item, templateID);
continue;
}
if (!itemCount)
{
LOG_ERROR("sql.sql", "Table `mail_server_template_items` has itemCount 0 for item {}, skipped.", item);
continue;
}
uint32 stackable = itemTemplate->Stackable;
if (itemCount > stackable)
{
LOG_ERROR("sql.sql", "Table `mail_server_template_items` has itemCount {} exceeding item_template.Stackable {} for item {}, skipped.", itemCount, stackable, item);
continue;
}
uint32 maxCount = itemTemplate->MaxCount;
if (maxCount && itemCount > maxCount)
{
LOG_ERROR("sql.sql", "Table `mail_server_template_items` has itemCount {} exceeding item_template.MaxCount {} for item {}, skipped", itemCount, maxCount, item);
continue;
}
ServerMailItems mailItem;
mailItem.item = item;
mailItem.itemCount = itemCount;
if (faction == "Alliance")
_serverMailStore[templateID].itemsA.push_back(mailItem);
else if (faction == "Horde")
_serverMailStore[templateID].itemsH.push_back(mailItem);
else
{
LOG_ERROR("sql.sql", "Table `mail_server_template_items` has invalid faction value '{}' for id {}, skipped.", faction, templateID);
continue;
}
} while (result->NextRow());
}

View File

@ -1056,6 +1056,7 @@ public:
void LoadInstanceEncounters();
void LoadMailLevelRewards();
void LoadMailServerTemplates();
void LoadMailServerTemplatesItems();
void LoadVehicleTemplateAccessories();
void LoadVehicleAccessories();
void LoadVehicleSeatAddon();
@ -1449,7 +1450,7 @@ public:
}
[[nodiscard]] uint32 GetQuestMoneyReward(uint8 level, uint32 questMoneyDifficulty) const;
void SendServerMail(Player* player, uint32 id, uint32 reqLevel, uint32 reqPlayTime, uint32 rewardMoneyA, uint32 rewardMoneyH, uint32 rewardItemA, uint32 rewardItemCountA, uint32 rewardItemH, uint32 rewardItemCountH, std::string subject, std::string body, uint8 active) const;
void SendServerMail(Player* player, uint32 id, uint32 reqLevel, uint32 reqPlayTime, uint32 rewardMoneyA, uint32 rewardMoneyH, std::vector<ServerMailItems> const& items, std::string subject, std::string body, uint8 active) const;
private:
// first free id for selected id type
uint32 _auctionId; // pussywizard: accessed by a single thread

View File

@ -210,6 +210,13 @@ struct Mail
[[nodiscard]] bool IsReturnedMail() const { return checked & MAIL_CHECK_MASK_RETURNED; }
};
struct ServerMailItems
{
ServerMailItems() = default;
uint32 item{ 0 };
uint32 itemCount{ 0 };
};
struct ServerMail
{
ServerMail() = default;
@ -218,13 +225,13 @@ struct ServerMail
uint32 reqPlayTime{ 0 };
uint32 moneyA{ 0 };
uint32 moneyH{ 0 };
uint32 itemA{ 0 };
uint32 itemCountA{ 0 };
uint32 itemH{ 0 };
uint32 itemCountH{ 0 };
std::string subject;
std::string body;
uint8 active{ 0 };
// Items from mail_server_template_items
std::vector<ServerMailItems> itemsA;
std::vector<ServerMailItems> itemsH;
};
#endif

View File

@ -83,7 +83,6 @@ struct boss_sacrolash : public BossAI
_isSisterDead = false;
BossAI::Reset();
me->SetLootMode(0);
me->m_Events.KillAllEvents(false);
}
void DoAction(int32 param) override
@ -124,10 +123,7 @@ struct boss_sacrolash : public BossAI
if (alythess->IsAlive() && !alythess->IsInCombat())
alythess->AI()->AttackStart(who);
me->m_Events.AddEventAtOffset([&] {
Talk(YELL_BERSERK);
DoCastSelf(SPELL_ENRAGE, true);
}, 6min);
ScheduleEnrageTimer(SPELL_ENRAGE, 6min, YELL_BERSERK);
ScheduleTimedEvent(10s, [&] {
DoCastSelf(SPELL_SHADOW_BLADES);
@ -195,7 +191,6 @@ struct boss_alythess : public BossAI
_isSisterDead = false;
BossAI::Reset();
me->SetLootMode(0);
me->m_Events.KillAllEvents(false);
}
void DoAction(int32 param) override
@ -236,10 +231,7 @@ struct boss_alythess : public BossAI
if (sacrolash->IsAlive() && !sacrolash->IsInCombat())
sacrolash->AI()->AttackStart(who);
me->m_Events.AddEventAtOffset([&] {
Talk(YELL_BERSERK);
DoCastSelf(SPELL_ENRAGE, true);
}, 6min);
ScheduleEnrageTimer(SPELL_ENRAGE, 6min, YELL_BERSERK);
ScheduleTimedEvent(1s, [&] {
DoCastVictim(SPELL_BLAZE);

View File

@ -134,7 +134,7 @@ struct boss_felmyst : public BossAI
void JustEngagedWith(Unit* who) override
{
BossAI::JustEngagedWith(who);
me->CastSpell(me, SPELL_NOXIOUS_FUMES, true);
me->m_Events.AddEventAtOffset([&] {
Talk(YELL_BERSERK);
DoCastSelf(SPELL_BERSERK, true);
@ -144,7 +144,7 @@ struct boss_felmyst : public BossAI
Position landPos = who->GetPosition();
me->m_Events.AddEventAtOffset([&, landPos] {
me->GetMotionMaster()->MovePoint(POINT_GROUND, landPos, false, true);
me->GetMotionMaster()->MoveLand(POINT_GROUND, landPos);
}, 2s);
}
@ -168,11 +168,14 @@ struct boss_felmyst : public BossAI
void MovementInform(uint32 type, uint32 point) override
{
if (type != POINT_MOTION_TYPE)
if (type != EFFECT_MOTION_TYPE && type != POINT_MOTION_TYPE)
return;
if (point == POINT_GROUND)
{
if (!me->HasAura(SPELL_NOXIOUS_FUMES))
DoCastSelf(SPELL_NOXIOUS_FUMES, true);
me->HandleEmoteCommand(EMOTE_ONESHOT_LAND);
me->SetCanFly(false);
me->SetDisableGravity(false);

View File

@ -37,6 +37,7 @@ public:
return;
uint32 playerGUID = player->GetGUID().GetCounter();
bool isAlliance = player->GetTeamId() == TEAM_ALLIANCE;
for (auto const& [mailId, servMail] : serverMailStore)
{
@ -45,12 +46,14 @@ public:
stmt->SetData(1, mailId);
// Capture servMail by value
auto callback = [session, servMailWrapper = std::reference_wrapper<ServerMail const>(servMail)](PreparedQueryResult result)
auto callback = [session, servMailWrapper = std::reference_wrapper<ServerMail const>(servMail), isAlliance](PreparedQueryResult result)
{
ServerMail const& servMail = servMailWrapper.get(); // Dereference the wrapper to get the original object
if (!result)
{
std::vector<ServerMailItems> const& items = isAlliance ? servMail.itemsA : servMail.itemsH;
sObjectMgr->SendServerMail(
session->GetPlayer(),
servMail.id,
@ -58,10 +61,7 @@ public:
servMail.reqPlayTime,
servMail.moneyA,
servMail.moneyH,
servMail.itemA,
servMail.itemCountA,
servMail.itemH,
servMail.itemCountH,
items,
servMail.subject,
servMail.body,
servMail.active