diff --git a/src/game/Object/ObjectMgr.h b/src/game/Object/ObjectMgr.h index f785aebb4..8d322c9d7 100644 --- a/src/game/Object/ObjectMgr.h +++ b/src/game/Object/ObjectMgr.h @@ -1366,6 +1366,15 @@ class ObjectMgr return m_CreatureQuestRelations.equal_range(entry); } + /// Quest-completion relations keyed by quest instead of by creature. + /// SMSG_QUEST_NPC_QUERY_RESPONSE answers with the quest's ENDER + /// creatures; retail 18414 captures confirm that rule (quest 28508 + /// returns its ender 44452, never its giver 42898). + QuestRelationsMapBounds GetQuestEnderCreaturesMapBounds(uint32 questId) const + { + return m_QuestEnderCreatures.equal_range(questId); + } + QuestRelationsMapBounds GetCreatureQuestInvolvedRelationsMapBounds(uint32 entry) const { return m_CreatureQuestInvolvedRelations.equal_range(entry); @@ -1497,6 +1506,7 @@ class ObjectMgr QuestRelationsMap m_CreatureQuestRelations; QuestRelationsMap m_CreatureQuestInvolvedRelations; + QuestRelationsMap m_QuestEnderCreatures; QuestRelationsMap m_GOQuestRelations; QuestRelationsMap m_GOQuestInvolvedRelations; diff --git a/src/game/Object/ObjectMgrQuestRelations.cpp b/src/game/Object/ObjectMgrQuestRelations.cpp index 156a1d1f5..d0e2ab67d 100644 --- a/src/game/Object/ObjectMgrQuestRelations.cpp +++ b/src/game/Object/ObjectMgrQuestRelations.cpp @@ -161,6 +161,16 @@ void ObjectMgr::LoadCreatureInvolvedRelations() { LoadQuestRelationsHelper(m_CreatureQuestInvolvedRelations, QA_CREATURE, QR_END); + // Inverse view for CMSG_QUEST_NPC_QUERY, which asks by quest rather than + // by creature. Built from the same rows, so it cannot drift from them and + // needs no additional query or table-name coupling. + m_QuestEnderCreatures.clear(); + for (QuestRelationsMap::const_iterator itr = m_CreatureQuestInvolvedRelations.begin(); + itr != m_CreatureQuestInvolvedRelations.end(); ++itr) + { + m_QuestEnderCreatures.insert(QuestRelationsMap::value_type(itr->second, itr->first)); + } + for (QuestRelationsMap::iterator itr = m_CreatureQuestInvolvedRelations.begin(); itr != m_CreatureQuestInvolvedRelations.end(); ++itr) { CreatureInfo const* cInfo = GetCreatureTemplate(itr->first); diff --git a/src/game/Object/ObjectUpdate.cpp b/src/game/Object/ObjectUpdate.cpp index 5e2bb18a6..4cec8b10a 100644 --- a/src/game/Object/ObjectUpdate.cpp +++ b/src/game/Object/ObjectUpdate.cpp @@ -110,6 +110,15 @@ namespace PLAYER_FIELD_BUYBACK_TIMESTAMP_1 + 12 == MopUpdateObject::SelfBuybackSourceStart + MopUpdateObject::SelfBuybackFieldCount, "self buyback translation must cover the legacy price and timestamp arrays"); + static_assert(PLAYER_QUEST_LOG_1_1 == MopUpdateObject::SelfQuestLogSourceStart && + PLAYER_QUEST_LOG_50_5 + 1 == + MopUpdateObject::SelfQuestLogSourceStart + MopUpdateObject::SelfQuestLogFieldCount, + "self quest-log translation must cover all fifty legacy five-word slots"); + static_assert(MAX_QUEST_OFFSET == MopUpdateObject::SelfQuestLogSourceStride, + "legacy quest slot stride must match the projection's source stride"); + static_assert(PLAYER_QUEST_LOG_2_1 - PLAYER_QUEST_LOG_1_1 == + MopUpdateObject::SelfQuestLogSourceStride, + "legacy quest slots must remain contiguous at the projected stride"); bool CanBuildMopInventoryObject(Object const& object, Player* target) { @@ -576,7 +585,8 @@ void Object::BuildValuesUpdateBlockForPlayer(UpdateData* data, Player* target) c fields.reserve(25 + MopUpdateObject::ObserverVisibleItemFieldCount + MopUpdateObject::SelfInventoryFieldCount + MopUpdateObject::SelfSkillFieldCount + - MopUpdateObject::SelfBuybackFieldCount); + MopUpdateObject::SelfBuybackFieldCount + + MopUpdateObject::SelfQuestLogFieldCount); auto addIfChanged = [this, &fields](uint16 sourceIndex) { if (m_changedValues[sourceIndex]) @@ -605,6 +615,17 @@ void Object::BuildValuesUpdateBlockForPlayer(UpdateData* data, Player* target) c addIfChanged(UNIT_FIELD_DISPLAYID); addIfChanged(UNIT_FIELD_NATIVEDISPLAYID); + // QuestLogFrame renders a slot only once the client holds its + // quest id, so an accepted quest never appears until these private + // fields are projected. Ordered ahead of the visible-item feed + // because the serializer requires ascending legacy indices. + for (uint16 i = MopUpdateObject::SelfQuestLogSourceStart; + i < MopUpdateObject::SelfQuestLogSourceStart + + MopUpdateObject::SelfQuestLogFieldCount; ++i) + { + addIfChanged(i); + } + // Local equipment changes use the same public 18414 visible-item // projection as updates sent to nearby observers. for (uint16 i = MopUpdateObject::ObserverVisibleItemSourceStart; diff --git a/src/game/Object/Player.cpp b/src/game/Object/Player.cpp index 08a395838..5b8d9a741 100644 --- a/src/game/Object/Player.cpp +++ b/src/game/Object/Player.cpp @@ -2339,16 +2339,15 @@ void Player::SetGMVisible(bool on) */ void Player::SendLogXPGain(uint32 GivenXP, Unit* victim, uint32 RestXP) { - WorldPacket data(SMSG_LOG_XPGAIN, 21); - data << (victim ? victim->GetObjectGuid() : ObjectGuid());// guid - data << uint32(GivenXP + RestXP); // given experience - data << uint8(victim ? 0 : 1); // 00-kill_xp type, 01-non_kill_xp type - if (victim) - { - data << uint32(GivenXP); // experience without rested bonus - data << float(1); // 1 - none 0 - 100% group bonus output - } - data << uint8(0); // new 2.4.0 + MopProgressionPackets::ExperienceGain packetInfo; + packetInfo.sourceGuid = victim ? victim->GetObjectGuid() : ObjectGuid(); + packetInfo.totalExperience = GivenXP + RestXP; + packetInfo.type = victim ? 0 : 1; + packetInfo.hasBaseExperience = victim != nullptr; + packetInfo.baseExperience = GivenXP; + + WorldPacket data; + MopProgressionPackets::BuildExperienceGain(data, packetInfo); GetSession()->SendPacket(&data); } @@ -2453,22 +2452,17 @@ void Player::GiveLevel(uint32 level) uint32 basehp = 0, basemana = 0; sObjectMgr.GetPlayerClassLevelInfo(getClass(), level, basehp, basemana); - // send levelup info to client - WorldPacket data(SMSG_LEVELUP_INFO, (4 + 4 + MAX_STORED_POWERS * 4 + MAX_STATS * 4)); - data << uint32(level); - data << uint32(int32(basehp) - int32(GetCreateHealth())); - // for(int i = 0; i < MAX_POWERS; ++i) // Powers loop (0-4) - data << uint32(int32(basemana) - int32(GetCreateMana())); - data << uint32(0); - data << uint32(0); - data << uint32(0); - data << uint32(0); - // end for - for (int i = STAT_STRENGTH; i < MAX_STATS; ++i) // Stats loop (0-4) - { - data << uint32(int32(info.stats[i]) - GetCreateStat(Stats(i))); - } + MopProgressionPackets::LevelUpInfo packetInfo; + packetInfo.level = level; + packetInfo.healthDelta = uint32(int32(basehp) - int32(GetCreateHealth())); + packetInfo.powerDeltas[POWER_MANA] = + uint32(int32(basemana) - int32(GetCreateMana())); + for (int i = STAT_STRENGTH; i < MAX_STATS; ++i) + packetInfo.statDeltas[i] = + uint32(int32(info.stats[i]) - GetCreateStat(Stats(i))); + WorldPacket data; + MopProgressionPackets::BuildLevelUpInfo(data, packetInfo); GetSession()->SendPacket(&data); SetUInt32Value(PLAYER_NEXT_LEVEL_XP, sObjectMgr.GetXPForLevel(level)); @@ -4118,9 +4112,8 @@ void Player::Customize(ObjectGuid guid, uint8 gender, uint8 skin, uint8 face, ui */ void Player::SendExplorationExperience(uint32 Area, uint32 Experience) { - WorldPacket data(SMSG_EXPLORATION_EXPERIENCE, 8); - data << uint32(Area); - data << uint32(Experience); + WorldPacket data; + MopAreaTriggerPackets::BuildExplorationExperience(data, Area, Experience); GetSession()->SendPacket(&data); } @@ -4584,9 +4577,8 @@ void Player::ApplyEquipCooldown(Item* pItem) AddSpellCooldown(spellData.SpellId, pItem->GetEntry(), time(NULL) + 30); - WorldPacket data(SMSG_ITEM_COOLDOWN, 12); - data << pItem->GetObjectGuid(); - data << uint32(spellData.SpellId); + WorldPacket data; + MopSpellPackets::BuildItemCooldown(data, pItem->GetObjectGuid(), spellData.SpellId); GetSession()->SendPacket(&data); } } diff --git a/src/game/Object/Player.h b/src/game/Object/Player.h index 050faf499..da77cc33d 100644 --- a/src/game/Object/Player.h +++ b/src/game/Object/Player.h @@ -635,6 +635,13 @@ namespace MopDeathPackets { static size_t const CEMETERY_LIST_MAX = 16; + inline void BuildDurabilityDamageDeath(WorldPacket& out) + { + // Wow.exe 18414 route 0x1E3E reaches sub_CE083A without consuming + // payload bytes and raises the retained DURABILITYDAMAGE_DEATH text. + out.Initialize(SMSG_DURABILITY_DAMAGE_DEATH, 0); + } + inline void BuildDeathReleaseLocation(WorldPacket& out, uint32 mapId, float x, float y, float z) { @@ -701,6 +708,187 @@ namespace MopReputationPackets } } +namespace MopProgressionPackets +{ + struct ExperienceGain + { + ObjectGuid sourceGuid; + uint32 totalExperience = 0; + uint8 type = 0; + bool hasBaseExperience = false; + uint32 baseExperience = 0; + }; + + inline void BuildExperienceGain(WorldPacket& out, + ExperienceGain const& info) + { + out.Initialize(SMSG_LOG_XPGAIN, 19); + + // Wow.exe 18414 reader sub_6F7E25 uses an inverted presence bit for + // base XP. The current backend has no group-rate or recruit-a-friend + // inputs, so their directly verified neutral bits are emitted here. + out.WriteBit(!info.hasBaseExperience); + out.WriteGuidMask<1, 2, 7, 4, 3>(info.sourceGuid); + out.WriteBit(false); // no recruit-a-friend bonus + out.WriteGuidMask<0, 5, 6>(info.sourceGuid); + out.WriteBit(true); // default group rate 1.0; no float follows + out.FlushBits(); + + out.WriteGuidBytes<4, 2>(info.sourceGuid); + out << info.type; + out.WriteGuidBytes<7, 1, 3, 6>(info.sourceGuid); + out << info.totalExperience; + if (info.hasBaseExperience) + out << info.baseExperience; + out.WriteGuidBytes<0, 5>(info.sourceGuid); + } + + struct LevelUpInfo + { + uint32 talentDelta = 0; + uint32 healthDelta = 0; + std::array statDeltas = {{}}; + uint32 level = 0; + std::array powerDeltas = {{}}; + }; + + inline void BuildLevelUpInfo(WorldPacket& out, LevelUpInfo const& info) + { + out.Initialize(SMSG_LEVELUP_INFO, 13 * sizeof(uint32)); + + // Wow.exe 18414 reader sub_6BAC39 consumes thirteen uint32 values. + // Terminal sub_7B12E9 maps these slots to PLAYER_LEVEL_UP arguments. + out << info.talentDelta << info.healthDelta; + for (uint32 delta : info.statDeltas) + out << delta; + out << info.level; + for (uint32 delta : info.powerDeltas) + out << delta; + } +} + +namespace MopComboPointPackets +{ + inline void BuildUpdate(WorldPacket& out, ObjectGuid target, uint8 points) + { + out.Initialize(SMSG_UPDATE_COMBO_POINTS, 10); + + // Wow.exe 18414 reader sub_6E2BC4 consumes six target-GUID bytes, + // the raw combo-point byte, then the final two target-GUID bytes. + // Terminal sub_CCA14B publishes the recovered GUID/value pair. + out.WriteGuidMask<0, 5, 6, 3, 7, 4, 1, 2>(target); + out.WriteGuidBytes<5, 6, 4, 7, 3, 0>(target); + out << points; + out.WriteGuidBytes<2, 1>(target); + } +} + +namespace MopDuelPackets +{ + inline void BuildRequested(WorldPacket& out, ObjectGuid arbiter, + ObjectGuid initiator) + { + out.Initialize(SMSG_DUEL_REQUESTED, 18); + + // Reader sub_6CA34C interleaves the duel flag and initiator masks. + out.WriteGuidMask<5>(arbiter); + out.WriteGuidMask<4, 2, 7>(initiator); + out.WriteGuidMask<0>(arbiter); + out.WriteGuidMask<5>(initiator); + out.WriteGuidMask<4, 6>(arbiter); + out.WriteGuidMask<1, 3, 6>(initiator); + out.WriteGuidMask<7, 3, 2, 1>(arbiter); + out.WriteGuidMask<0>(initiator); + out.FlushBits(); + + out.WriteGuidBytes<5, 3>(arbiter); + out.WriteGuidBytes<7, 4>(initiator); + out.WriteGuidBytes<7>(arbiter); + out.WriteGuidBytes<3, 6, 0>(initiator); + out.WriteGuidBytes<4>(arbiter); + out.WriteGuidBytes<2, 1>(initiator); + out.WriteGuidBytes<0, 2, 6, 1>(arbiter); + out.WriteGuidBytes<5>(initiator); + } + + inline void BuildOutOfBounds(WorldPacket& out) + { + // Wow.exe 18414 routes 0x001A through the fieldless reader sub_6BC12D. + out.Initialize(SMSG_DUEL_OUTOFBOUNDS, 0); + } + + inline void BuildInBounds(WorldPacket& out) + { + // Wow.exe 18414 routes 0x163A through the same fieldless reader. + out.Initialize(SMSG_DUEL_INBOUNDS, 0); + } + + inline void BuildComplete(WorldPacket& out, bool completed) + { + out.Initialize(SMSG_DUEL_COMPLETE, 1); + + // Reader sub_6D3E7C consumes exactly one MSB-first completion bit. + out.WriteBit(completed); + out.FlushBits(); + } + + inline void BuildCountdown(WorldPacket& out, uint32 milliseconds) + { + out.Initialize(SMSG_DUEL_COUNTDOWN, sizeof(milliseconds)); + + // Reader sub_6D9F28 consumes one uint32; terminal sub_9BFFD4 + // divides it by 1000 before updating the duel countdown UI. + out << milliseconds; + } + + inline bool BuildWinner(WorldPacket& out, bool retreat, + std::string const& winnerName, uint32 winnerRealmAddress, + std::string const& loserName, uint32 loserRealmAddress) + { + if (winnerName.size() > 63 || loserName.size() > 63) + return false; + + out.Initialize(SMSG_DUEL_WINNER, + 2 + 2 * sizeof(uint32) + winnerName.size() + loserName.size()); + + // Reader sub_6CFDCC consumes the outcome followed by two six-bit + // lengths. Its realm-address fields are crossed around the names; + // terminal sub_9C0069 pairs them back as winner then loser. + out.WriteBit(retreat); + out.WriteBits(uint32(winnerName.size()), 6); + out.WriteBits(uint32(loserName.size()), 6); + out.FlushBits(); + + out << loserRealmAddress; + out.append(winnerName.c_str(), winnerName.size()); + out << winnerRealmAddress; + out.append(loserName.c_str(), loserName.size()); + return true; + } +} + +namespace MopMirrorTimerPackets +{ + inline void BuildStart(WorldPacket& out, uint32 type, uint32 maxValue, + uint32 currentValue, int32 regeneration, uint32 spellId, bool paused) + { + out.Initialize(SMSG_START_MIRROR_TIMER, 21); + + // Wow.exe 18414 reader sub_6F16F9 consumes five uint32 values in + // max/spell/current/regen/type order, then one MSB-first pause bit. + out << maxValue << spellId << currentValue << uint32(regeneration) << type; + out.WriteBit(paused); + out.FlushBits(); + } + + inline void BuildStop(WorldPacket& out, uint32 type) + { + // Wow.exe 18414 reader sub_6D9F28 consumes only the timer type. + out.Initialize(SMSG_STOP_MIRROR_TIMER, sizeof(type)); + out << type; + } +} + namespace MopAreaTriggerPackets { struct Request @@ -749,6 +937,16 @@ namespace MopAreaTriggerPackets // ERR_CORPSE_IS_NOT_IN_INSTANCE. out.Initialize(SMSG_AREA_TRIGGER_NO_CORPSE, 0); } + + inline void BuildExplorationExperience(WorldPacket& out, uint32 areaId, + uint32 experience) + { + out.Initialize(SMSG_EXPLORATION_EXPERIENCE, 8); + + // Wow.exe 18414 reader sub_6BB9C1 consumes the area-table key first; + // terminal sub_7B1384 then displays the second uint32 as awarded XP. + out << areaId << experience; + } } namespace MopQuestPackets diff --git a/src/game/Object/PlayerCombo.cpp b/src/game/Object/PlayerCombo.cpp index 8efbf1898..3f9e62303 100644 --- a/src/game/Object/PlayerCombo.cpp +++ b/src/game/Object/PlayerCombo.cpp @@ -88,9 +88,8 @@ void Player::SendComboPoints() Unit* combotarget = sObjectAccessor.GetUnit(*this, m_comboTargetGuid); if (combotarget) { - WorldPacket data(SMSG_UPDATE_COMBO_POINTS, combotarget->GetPackGUID().size() + 1); - data << combotarget->GetPackGUID(); - data << uint8(m_comboPoints); + WorldPacket data; + MopComboPointPackets::BuildUpdate(data, combotarget->GetObjectGuid(), uint8(m_comboPoints)); GetSession()->SendPacket(&data); } /*else diff --git a/src/game/Object/PlayerDeath.cpp b/src/game/Object/PlayerDeath.cpp index d64be36ea..499c2926c 100644 --- a/src/game/Object/PlayerDeath.cpp +++ b/src/game/Object/PlayerDeath.cpp @@ -93,8 +93,8 @@ static const uint32 corpseReclaimDelay[MAX_DEATH_COUNT] = {30, 60, 120}; */ void Player::BuildPlayerRepop() { - WorldPacket data(SMSG_PRE_RESURRECT, GetPackGUID().size()); - data << GetPackGUID(); + WorldPacket data; + MopCompactPackets::BuildPreResurrect(data, GetObjectGuid()); GetSession()->SendPacket(&data); if (getRace() == RACE_NIGHTELF) diff --git a/src/game/Object/PlayerDuel.cpp b/src/game/Object/PlayerDuel.cpp index 99b494f0c..39520dec5 100644 --- a/src/game/Object/PlayerDuel.cpp +++ b/src/game/Object/PlayerDuel.cpp @@ -102,7 +102,8 @@ void Player::CheckDuelDistance(time_t currTime) { duel->outOfBound = currTime; - WorldPacket data(SMSG_DUEL_OUTOFBOUNDS, 0); + WorldPacket data; + MopDuelPackets::BuildOutOfBounds(data); GetSession()->SendPacket(&data); } } @@ -112,7 +113,8 @@ void Player::CheckDuelDistance(time_t currTime) { duel->outOfBound = 0; - WorldPacket data(SMSG_DUEL_INBOUNDS, 0); + WorldPacket data; + MopDuelPackets::BuildInBounds(data); GetSession()->SendPacket(&data); } else if (currTime >= (duel->outOfBound + 10)) @@ -153,18 +155,16 @@ void Player::DuelComplete(DuelCompleteType type) return; } - WorldPacket data(SMSG_DUEL_COMPLETE, (1)); - data << (uint8)((type != DUEL_INTERRUPTED) ? 1 : 0); + WorldPacket data; + MopDuelPackets::BuildComplete(data, type != DUEL_INTERRUPTED); GetSession()->SendPacket(&data); opponent->GetSession()->SendPacket(&data); if (type != DUEL_INTERRUPTED) { - data.Initialize(SMSG_DUEL_WINNER, (1 + 20)); // we guess size - data << (uint8)((type == DUEL_WON) ? 0 : 1); // 0 = just won; 1 = fled - data << opponent->GetName(); - data << GetName(); - SendMessageToSet(&data, true); + if (MopDuelPackets::BuildWinner(data, type != DUEL_WON, + opponent->GetName(), realmID, GetName(), realmID)) + SendMessageToSet(&data, true); } // Used by Eluna @@ -253,7 +253,7 @@ void Player::DuelComplete(DuelCompleteType type) void Player::SendDuelCountdown(uint32 counter) { - WorldPacket data(SMSG_DUEL_COUNTDOWN, 4); - data << uint32(counter); // seconds + WorldPacket data; + MopDuelPackets::BuildCountdown(data, counter); GetSession()->SendPacket(&data); } diff --git a/src/game/Object/PlayerMirror.cpp b/src/game/Object/PlayerMirror.cpp index 88b57d5e0..006129b2f 100644 --- a/src/game/Object/PlayerMirror.cpp +++ b/src/game/Object/PlayerMirror.cpp @@ -99,13 +99,9 @@ void Player::SendMirrorTimer(MirrorTimerType Type, uint32 MaxValue, uint32 Curre } return; } - WorldPacket data(SMSG_START_MIRROR_TIMER, (21)); - data << (uint32)Type; - data << CurrentValue; - data << MaxValue; - data << Regen; - data << (uint8)0; - data << (uint32)0; // Spell ID + WorldPacket data; + MopMirrorTimerPackets::BuildStart( + data, uint32(Type), MaxValue, CurrentValue, Regen, 0, false); GetSession()->SendPacket(&data); } @@ -117,8 +113,8 @@ void Player::SendMirrorTimer(MirrorTimerType Type, uint32 MaxValue, uint32 Curre void Player::StopMirrorTimer(MirrorTimerType Type) { m_MirrorTimer[Type] = DISABLED_MIRROR_TIMER; - WorldPacket data(SMSG_STOP_MIRROR_TIMER, 4); - data << (uint32)Type; + WorldPacket data; + MopMirrorTimerPackets::BuildStop(data, uint32(Type)); GetSession()->SendPacket(&data); } @@ -175,7 +171,8 @@ uint32 Player::EnvironmentalDamage(EnviromentalDamage type, uint32 damage) DEBUG_LOG("We fell to death, losing 10 percent durability"); DurabilityLossAll(0.10f, false); // durability lost message - WorldPacket data2(SMSG_DURABILITY_DAMAGE_DEATH, 0); + WorldPacket data2; + MopDeathPackets::BuildDurabilityDamageDeath(data2); GetSession()->SendPacket(&data2); } diff --git a/src/game/Object/RuneMgr.cpp b/src/game/Object/RuneMgr.cpp index c43a4786a..3ec7853d9 100644 --- a/src/game/Object/RuneMgr.cpp +++ b/src/game/Object/RuneMgr.cpp @@ -199,9 +199,8 @@ void RuneMgr::ConvertRune(uint8 index, RuneType newType) { SetCurrentRune(index, newType); - WorldPacket data(SMSG_CONVERT_RUNE, 2); - data << uint8(index); - data << uint8(newType); + WorldPacket data; + MopRunePackets::BuildConvert(data, newType, index); m_owner->GetSession()->SendPacket(&data); } @@ -223,20 +222,21 @@ bool RuneMgr::ActivateRunes(RuneType type, uint32 count) void RuneMgr::ResyncRunes() { - WorldPacket data(SMSG_RESYNC_RUNES, 4 + MAX_RUNES * 2); - data << uint32(MAX_RUNES); + std::array runes; for (uint32 i = 0; i < MAX_RUNES; ++i) { - data << uint8(GetCurrentRune(i)); // rune type - data << uint8(GetRuneCooldownFraction(i)); + runes[i].type = GetCurrentRune(i); + runes[i].cooldownFraction = GetRuneCooldownFraction(i); } + WorldPacket data; + MopRunePackets::BuildResync(data, runes); m_owner->GetSession()->SendPacket(&data); } void RuneMgr::AddRunePower(uint8 index) { - WorldPacket data(SMSG_ADD_RUNE_POWER, 4); - data << uint32(1 << index); // mask (0x00-0x3F probably) + WorldPacket data; + MopRunePackets::BuildAddPower(data, uint32(1 << index)); m_owner->GetSession()->SendPacket(&data); } diff --git a/src/game/Object/RuneMgr.h b/src/game/Object/RuneMgr.h index 9c78c4391..55c016e02 100644 --- a/src/game/Object/RuneMgr.h +++ b/src/game/Object/RuneMgr.h @@ -27,6 +27,10 @@ #define MANGOS_H_RUNEMGR #include "Common.h" +#include "Opcodes.h" +#include "WorldPacket.h" + +#include class Player; class Aura; @@ -76,6 +80,44 @@ struct Runes } }; +namespace MopRunePackets +{ + struct RuneState + { + RuneType type = RUNE_BLOOD; + uint8 cooldownFraction = 0; + }; + + inline void BuildResync(WorldPacket& out, + std::array const& runes) + { + out.Initialize(SMSG_RESYNC_RUNES, 3 + 2 * runes.size()); + + // Wow.exe 18414 helper sub_69BB74 consumes a 23-bit count. Reader + // sub_73299D then reads cooldown fraction before rune type. + out.WriteBits(uint32(runes.size()), 23); + out.FlushBits(); + for (RuneState const& rune : runes) + out << rune.cooldownFraction << uint8(rune.type); + } + + inline void BuildAddPower(WorldPacket& out, uint32 runeMask) + { + // Reader sub_6D9F28 consumes the one uint32 available-rune mask. + out.Initialize(SMSG_ADD_RUNE_POWER, sizeof(runeMask)); + out << runeMask; + } + + inline void BuildConvert(WorldPacket& out, RuneType newType, uint8 index) + { + out.Initialize(SMSG_CONVERT_RUNE, 2); + + // Reader sub_6B9A69 and apply helper sub_951948 consume type first, + // then the rune slot index. + out << uint8(newType) << index; + } +} + /** * @brief Owns a death knight player's rune state and rune-system behaviour. * diff --git a/src/game/Object/SpellCooldownMgr.cpp b/src/game/Object/SpellCooldownMgr.cpp index b9c067c22..d49a1c008 100644 --- a/src/game/Object/SpellCooldownMgr.cpp +++ b/src/game/Object/SpellCooldownMgr.cpp @@ -160,9 +160,8 @@ void SpellCooldownMgr::SendCooldownEvent(SpellEntry const* spellInfo, uint32 ite AddSpellAndCategoryCooldowns(spellInfo, itemId, spell); // Send activate cooldown timer (possible 0) at client side - WorldPacket data(SMSG_COOLDOWN_EVENT, (4 + 8)); - data << uint32(spellInfo->ID); - data << m_owner->GetObjectGuid(); + WorldPacket data; + MopSpellPackets::BuildCooldownEvent(data, m_owner->GetObjectGuid(), spellInfo->ID); m_owner->SendDirectMessage(&data); } diff --git a/src/game/Object/Unit.cpp b/src/game/Object/Unit.cpp index 9b5603864..8ccdd3db0 100644 --- a/src/game/Object/Unit.cpp +++ b/src/game/Object/Unit.cpp @@ -888,9 +888,9 @@ uint32 Unit::DealDamage(Unit* pVictim, uint32 damage, CleanDamage const* cleanDa { player_tap->ProcDamageAndSpell(pVictim, PROC_FLAG_KILL, PROC_FLAG_KILLED, PROC_EX_NONE, 0); - WorldPacket data(SMSG_PARTYKILLLOG, (8 + 8)); // send event PARTY_KILL - data << player_tap->GetObjectGuid(); // player with killing blow - data << pVictim->GetObjectGuid(); // victim + WorldPacket data; + MopCompactPackets::BuildPartyKillLog(data, + player_tap->GetObjectGuid(), pVictim->GetObjectGuid()); if (group_tap) { @@ -1009,7 +1009,8 @@ uint32 Unit::DealDamage(Unit* pVictim, uint32 damage, CleanDamage const* cleanDa DEBUG_LOG("DealDamage: Killed %s, looing 10 percents durability", pVictim->GetGuidStr().c_str()); playerVictim->DurabilityLossAll(0.10f, false); // durability lost message - WorldPacket data(SMSG_DURABILITY_DAMAGE_DEATH, 0); + WorldPacket data; + MopDeathPackets::BuildDurabilityDamageDeath(data); playerVictim->GetSession()->SendPacket(&data); } @@ -2487,15 +2488,16 @@ void Unit::ProcDamageAndSpell(Unit* pVictim, uint32 procAttacker, uint32 procVic */ void Unit::SendSpellMiss(Unit* target, uint32 spellID, SpellMissInfo missInfo) { - WorldPacket data(SMSG_SPELLLOGMISS, (4 + 8 + 1 + 4 + 8 + 1)); - data << uint32(spellID); - data << GetObjectGuid(); - data << uint8(0); // can be 0 or 1, flag - data << uint32(1); // target count - // for(i = 0; i < target count; ++i) - data << target->GetObjectGuid(); // target GUID - data << uint8(missInfo); - // end loop + MopCombatLogPackets::SpellMissTarget miss = {}; + miss.guid = target->GetObjectGuid().GetRawValue(); + miss.missReason = uint8(missInfo); + MopCombatLogPackets::SpellMissLog log = {}; + log.casterGuid = GetObjectGuid().GetRawValue(); + log.spellId = spellID; + log.targets = &miss; + log.targetCount = 1; + WorldPacket data(SMSG_SPELLLOGMISS, 32); + MopCombatLogPackets::BuildSpellMissLog(data, log); SendMessageToSet(&data, true); } @@ -3521,16 +3523,16 @@ Unit* Unit::SelectMagnetTarget(Unit* victim, Spell* spell, SpellEffectIndex eff) */ void Unit::SendHealSpellLog(Unit* pVictim, uint32 SpellID, uint32 Damage, uint32 OverHeal, bool critical, uint32 absorb) { - // we guess size - WorldPacket data(SMSG_SPELLHEALLOG, (8 + 8 + 4 + 4 + 1)); - data << pVictim->GetPackGUID(); - data << GetPackGUID(); - data << uint32(SpellID); - data << uint32(Damage); - data << uint32(OverHeal); - data << uint32(absorb); - data << uint8(critical ? 1 : 0); - data << uint8(0); // unused in client? + MopCombatLogPackets::SpellHealLog log = {}; + log.casterGuid = GetObjectGuid().GetRawValue(); + log.targetGuid = pVictim->GetObjectGuid().GetRawValue(); + log.spellId = SpellID; + log.heal = Damage; + log.overheal = OverHeal; + log.absorb = absorb; + log.critical = critical; + WorldPacket data(SMSG_SPELLHEALLOG, 36); + MopCombatLogPackets::BuildSpellHealLog(data, log); SendMessageToSet(&data, true); } @@ -3544,12 +3546,14 @@ void Unit::SendHealSpellLog(Unit* pVictim, uint32 SpellID, uint32 Damage, uint32 */ void Unit::SendEnergizeSpellLog(Unit* pVictim, uint32 SpellID, uint32 Damage, Powers powertype) { - WorldPacket data(SMSG_SPELLENERGIZELOG, (8 + 8 + 4 + 4 + 4 + 1)); - data << pVictim->GetPackGUID(); - data << GetPackGUID(); - data << uint32(SpellID); - data << uint32(powertype); - data << uint32(Damage); + MopCombatLogPackets::SpellEnergizeLog log = {}; + log.targetGuid = pVictim->GetObjectGuid().GetRawValue(); + log.casterGuid = GetObjectGuid().GetRawValue(); + log.amount = Damage; + log.spellId = SpellID; + log.powerType = powertype; + WorldPacket data(SMSG_SPELLENERGIZELOG, 28); + MopCombatLogPackets::BuildSpellEnergizeLog(data, log); SendMessageToSet(&data, true); } @@ -3739,8 +3743,8 @@ void Unit::Unmount(bool from_aura) // Called NOT by Taxi system / GM command if (from_aura) { - WorldPacket data(SMSG_DISMOUNT, 8); - data << GetPackGUID(); + WorldPacket data; + MopCompactPackets::BuildDismount(data, GetObjectGuid()); SendMessageToSet(&data, true); } diff --git a/src/game/Object/Unit.h b/src/game/Object/Unit.h index 7d2ba4197..f6bd7ccb0 100644 --- a/src/game/Object/Unit.h +++ b/src/game/Object/Unit.h @@ -75,6 +75,8 @@ #include "WorldPacket.h" #include "Timer.h" +#include + namespace MopCompactPackets { inline uint8 AttackGuidByte(uint64 guid, uint8 index) @@ -181,6 +183,34 @@ namespace MopCompactPackets out.WriteGuidBytes<7, 6, 2, 5, 0, 4, 1, 3>(guid); } + inline void BuildPartyKillLog(WorldPacket& out, ObjectGuid killer, + ObjectGuid victim) + { + out.Initialize(SMSG_PARTYKILLLOG, 18); + + // Wow.exe 18414 reader sub_6F2FE4 consumes two packed GUIDs; + // terminal sub_841B83 treats them as credited killer then victim. + out.WriteGuidMask<7, 2>(victim); + out.WriteGuidMask<1>(killer); + out.WriteGuidMask<4>(victim); + out.WriteGuidMask<2, 5>(killer); + out.WriteGuidMask<3, 1, 0>(victim); + out.WriteGuidMask<3, 0, 4>(killer); + out.WriteGuidMask<6>(victim); + out.WriteGuidMask<7>(killer); + out.WriteGuidMask<5>(victim); + out.WriteGuidMask<6>(killer); + out.FlushBits(); + + out.WriteGuidBytes<0, 5>(victim); + out.WriteGuidBytes<0, 2>(killer); + out.WriteGuidBytes<7, 6, 1, 4>(victim); + out.WriteGuidBytes<4, 1>(killer); + out.WriteGuidBytes<2>(victim); + out.WriteGuidBytes<6, 3, 5, 7>(killer); + out.WriteGuidBytes<3>(victim); + } + struct AttackStateUpdateData { uint32 hitInfo = 0; @@ -347,6 +377,140 @@ namespace MopCompactPackets out.WriteGuidMask<1, 5, 6, 0, 7, 2, 3, 4>(guid); out.WriteGuidBytes<1, 6, 4, 3, 7, 0, 2, 5>(guid); } + + inline void BuildDismount(WorldPacket& out, ObjectGuid guid) + { + // Wow.exe 18414 reader helper sub_6D3AD4 consumes one packed unit + // GUID; terminal sub_82E6E0 applies a zero mount state to that unit. + out.Initialize(SMSG_DISMOUNT, 9); + out.WriteGuidMask<6, 3, 0, 7, 1, 2, 5, 4>(guid); + out.WriteGuidBytes<3, 6, 7, 5, 1, 4, 2, 0>(guid); + } + + inline void BuildPreResurrect(WorldPacket& out, ObjectGuid guid) + { + // Wow.exe 18414 parser sub_709F6B (dispatcher sub_659694 case 696, + // the dense selector for 0x19C0) constructs the message and reads one + // packed GUID and nothing else. Two independent readers agree on the + // order: the constructor's sub_6E7875 and the class's virtual + // deserialize slot sub_6D6EF4. + // + // The consumer is reached only through the per-message Arxan guard + // trampoline sub_6D1F55, whose target is assembled at runtime, so the + // GUID's role is NOT binary-proved. It is the repopping player's own + // GUID here only because that is what the inherited sender already + // supplied; this conversion changes the encoding, not the semantics. + out.Initialize(SMSG_PRE_RESURRECT, 9); + out.WriteGuidMask<1, 7, 5, 2, 6, 0, 3, 4>(guid); + out.WriteGuidBytes<5, 1, 7, 0, 6, 4, 2, 3>(guid); + } +} + +namespace MopThreatPackets +{ + struct ThreatEntry + { + ObjectGuid target; + uint32 threat = 0; + }; + + using ThreatEntries = std::vector; + + inline void BuildUpdate(WorldPacket& out, ObjectGuid owner, + ThreatEntries const& entries) + { + // Wow.exe 18414 reader sub_7344A4 consumes a 21-bit threat count, + // per-target packed GUIDs and values, then the threatened unit GUID. + MANGOS_ASSERT(entries.size() < (uint32(1) << 21)); + out.Initialize(SMSG_THREAT_UPDATE, 12 + entries.size() * 13); + out.WriteGuidMask<5, 6, 1, 3, 7, 0, 4>(owner); + out.WriteBits(uint32(entries.size()), 21); + for (ThreatEntry const& entry : entries) + out.WriteGuidMask<2, 3, 6, 5, 1, 4, 0, 7>(entry.target); + out.WriteGuidMask<2>(owner); + out.FlushBits(); + + for (ThreatEntry const& entry : entries) + { + out.WriteGuidBytes<6, 7, 0, 1, 2, 5, 3, 4>(entry.target); + out << entry.threat; + } + out.WriteGuidBytes<1, 4, 2, 3, 5, 6, 0, 7>(owner); + } + + inline void BuildHighest(WorldPacket& out, ObjectGuid owner, + ObjectGuid selected, ThreatEntries const& entries) + { + // Wow.exe 18414 reader sub_736527 interleaves the selected target, + // threatened unit and the same 21-bit threat-list representation. + MANGOS_ASSERT(entries.size() < (uint32(1) << 21)); + out.Initialize(SMSG_HIGHEST_THREAT_UPDATE, 21 + entries.size() * 13); + out.WriteGuidMask<3, 0>(selected); + out.WriteGuidMask<3, 6, 1>(owner); + out.WriteGuidMask<5, 1, 6>(selected); + out.WriteGuidMask<2, 5>(owner); + out.WriteGuidMask<7, 4>(selected); + out.WriteGuidMask<4>(owner); + out.WriteBits(uint32(entries.size()), 21); + for (ThreatEntry const& entry : entries) + out.WriteGuidMask<6, 1, 0, 2, 7, 4, 3, 5>(entry.target); + out.WriteGuidMask<7, 0>(owner); + out.WriteGuidMask<2>(selected); + out.FlushBits(); + + out.WriteGuidBytes<4>(owner); + for (ThreatEntry const& entry : entries) + { + out.WriteGuidBytes<6>(entry.target); + out << entry.threat; + out.WriteGuidBytes<4, 0, 3, 5, 2, 1, 7>(entry.target); + } + out.WriteGuidBytes<3>(selected); + out.WriteGuidBytes<5>(owner); + out.WriteGuidBytes<2>(selected); + out.WriteGuidBytes<1, 0, 2>(owner); + out.WriteGuidBytes<6, 1>(selected); + out.WriteGuidBytes<7>(owner); + out.WriteGuidBytes<0, 4, 7>(selected); + out.WriteGuidBytes<3, 6>(owner); + out.WriteGuidBytes<5>(selected); + } + + inline void BuildClear(WorldPacket& out, ObjectGuid owner) + { + // Reader sub_6F2392 and terminal sub_820714 identify this GUID as the + // unit whose client-side threat state is cleared. + out.Initialize(SMSG_THREAT_CLEAR, 9); + out.WriteGuidMask<6, 7, 4, 5, 2, 1, 0, 3>(owner); + out.WriteGuidBytes<7, 0, 4, 3, 2, 1, 6, 5>(owner); + } + + inline void BuildRemove(WorldPacket& out, ObjectGuid owner, + ObjectGuid removed) + { + // Reader sub_6DBFD5 gives terminal sub_8206E1 the threatened unit + // first and the removed target second. + out.Initialize(SMSG_THREAT_REMOVE, 18); + out.WriteGuidMask<0, 1, 5>(owner); + out.WriteGuidMask<4, 0>(removed); + out.WriteGuidMask<4, 6>(owner); + out.WriteGuidMask<7, 6, 3>(removed); + out.WriteGuidMask<2>(owner); + out.WriteGuidMask<1>(removed); + out.WriteGuidMask<3, 7>(owner); + out.WriteGuidMask<5, 2>(removed); + out.FlushBits(); + + out.WriteGuidBytes<3, 0, 2>(removed); + out.WriteGuidBytes<5, 4, 7, 3, 0>(owner); + out.WriteGuidBytes<4>(removed); + out.WriteGuidBytes<1>(owner); + out.WriteGuidBytes<1>(removed); + out.WriteGuidBytes<6>(owner); + out.WriteGuidBytes<7, 6>(removed); + out.WriteGuidBytes<2>(owner); + out.WriteGuidBytes<5>(removed); + } } #include diff --git a/src/game/Object/UnitMeleeDamage.cpp b/src/game/Object/UnitMeleeDamage.cpp index 7df8454e7..6314c81d6 100644 --- a/src/game/Object/UnitMeleeDamage.cpp +++ b/src/game/Object/UnitMeleeDamage.cpp @@ -469,18 +469,16 @@ void Unit::DealMeleeDamage(CalcDamageInfo* damageInfo, bool durabilityLoss) uint32 targetHealth = GetHealth(); uint32 overkill = damage > targetHealth ? damage - targetHealth : 0; - WorldPacket data(SMSG_SPELLDAMAGESHIELD, (8 + 8 + 4 + 4 + 4 + 4)); - data << pVictim->GetObjectGuid(); - data << GetObjectGuid(); - data << uint32(i_spellProto->ID); - data << uint32(damage); // Damage - data << uint32(overkill); // Overkill -#if !defined (MISTS) - data << uint32(i_spellProto->SchoolMask); -#else - data << uint32(i_spellProto->GetSchoolMask()); -#endif - data << uint32(resist); // Resist + MopCombatLogPackets::SpellDamageShieldLog log = {}; + log.casterGuid = pVictim->GetObjectGuid().GetRawValue(); + log.targetGuid = GetObjectGuid().GetRawValue(); + log.spellId = i_spellProto->ID; + log.damage = damage; + log.overkill = overkill; + log.schoolMask = i_spellProto->GetSchoolMask(); + log.resist = resist; + WorldPacket data(SMSG_SPELLDAMAGESHIELD, 36); + MopCombatLogPackets::BuildSpellDamageShieldLog(data, log); pVictim->SendMessageToSet(&data, true); pVictim->DealDamage(this, damage, 0, SPELL_DIRECT_DAMAGE, GetSpellSchoolMask(i_spellProto), i_spellProto, true); diff --git a/src/game/Object/UnitThreat.cpp b/src/game/Object/UnitThreat.cpp index 50629e930..a6e9a494c 100644 --- a/src/game/Object/UnitThreat.cpp +++ b/src/game/Object/UnitThreat.cpp @@ -455,14 +455,13 @@ void Unit::SendThreatUpdate() if (uint32 count = tlist.size()) { DEBUG_FILTER_LOG(LOG_FILTER_COMBAT, "WORLD: Send SMSG_THREAT_UPDATE Message"); - WorldPacket data(SMSG_THREAT_UPDATE, 8 + count * 8); - data << GetPackGUID(); - data << uint32(count); + MopThreatPackets::ThreatEntries entries; + entries.reserve(count); for (ThreatList::const_iterator itr = tlist.begin(); itr != tlist.end(); ++itr) - { - data << (*itr)->getUnitGuid().WriteAsPacked(); - data << uint32((*itr)->getThreat()); - } + entries.push_back({ (*itr)->getUnitGuid(), uint32((*itr)->getThreat()) }); + + WorldPacket data; + MopThreatPackets::BuildUpdate(data, GetObjectGuid(), entries); SendMessageToSet(&data, false); } } @@ -473,15 +472,14 @@ void Unit::SendHighestThreatUpdate(HostileReference* pHostilReference) if (uint32 count = tlist.size()) { DEBUG_FILTER_LOG(LOG_FILTER_COMBAT, "WORLD: Send SMSG_HIGHEST_THREAT_UPDATE Message"); - WorldPacket data(SMSG_HIGHEST_THREAT_UPDATE, 8 + 8 + count * 8); - data << GetPackGUID(); - data << pHostilReference->getUnitGuid().WriteAsPacked(); - data << uint32(count); + MopThreatPackets::ThreatEntries entries; + entries.reserve(count); for (ThreatList::const_iterator itr = tlist.begin(); itr != tlist.end(); ++itr) - { - data << (*itr)->getUnitGuid().WriteAsPacked(); - data << uint32((*itr)->getThreat()); - } + entries.push_back({ (*itr)->getUnitGuid(), uint32((*itr)->getThreat()) }); + + WorldPacket data; + MopThreatPackets::BuildHighest(data, GetObjectGuid(), + pHostilReference->getUnitGuid(), entries); SendMessageToSet(&data, false); } } @@ -489,16 +487,16 @@ void Unit::SendHighestThreatUpdate(HostileReference* pHostilReference) void Unit::SendThreatClear() { DEBUG_FILTER_LOG(LOG_FILTER_COMBAT, "WORLD: Send SMSG_THREAT_CLEAR Message"); - WorldPacket data(SMSG_THREAT_CLEAR, 8); - data << GetPackGUID(); + WorldPacket data; + MopThreatPackets::BuildClear(data, GetObjectGuid()); SendMessageToSet(&data, false); } void Unit::SendThreatRemove(HostileReference* pHostileReference) { DEBUG_FILTER_LOG(LOG_FILTER_COMBAT, "WORLD: Send SMSG_THREAT_REMOVE Message"); - WorldPacket data(SMSG_THREAT_REMOVE, 8 + 8); - data << GetPackGUID(); - data << pHostileReference->getUnitGuid().WriteAsPacked(); + WorldPacket data; + MopThreatPackets::BuildRemove(data, GetObjectGuid(), + pHostileReference->getUnitGuid()); SendMessageToSet(&data, false); } diff --git a/src/game/Server/MopUpdateObject.cpp b/src/game/Server/MopUpdateObject.cpp index 02fc0f2f6..39c09f09a 100644 --- a/src/game/Server/MopUpdateObject.cpp +++ b/src/game/Server/MopUpdateObject.cpp @@ -71,6 +71,16 @@ uint16 MopUpdateObject::TranslateSelfInventoryIndex(uint16 legacyIndex) return uint16(legacyIndex + 5); } +uint16 MopUpdateObject::TranslateSelfQuestLogIndex(uint16 legacyIndex) +{ + MANGOS_ASSERT(legacyIndex >= SelfQuestLogSourceStart && + legacyIndex < SelfQuestLogSourceStart + SelfQuestLogFieldCount); + const uint16 offset = uint16(legacyIndex - SelfQuestLogSourceStart); + const uint16 slot = uint16(offset / SelfQuestLogSourceStride); + const uint16 fieldInSlot = uint16(offset % SelfQuestLogSourceStride); + return uint16(SelfQuestLogTargetStart + slot * SelfQuestLogTargetStride + fieldInSlot); +} + bool MopUpdateObject::TranslateObserverPlayerIndex(uint16 legacyIndex, uint16& targetIndex) { if (legacyIndex >= ObserverVisibleItemSourceStart && @@ -276,6 +286,17 @@ void MopUpdateObject::AppendSelfPlayerValuesBlock(ByteBuffer& out, uint64 guid, const uint16 sourceIndex = sourceFields[i].index; const uint32 value = sourceFields[i].value; + // QuestLogFrame reads each slot at a fifteen-word stride, so Four's + // five-word slots have to be re-strided rather than shifted. Zero + // values are preserved: a cleared quest id is how the client is told + // a slot was abandoned. + if (sourceIndex >= SelfQuestLogSourceStart && + sourceIndex < SelfQuestLogSourceStart + SelfQuestLogFieldCount) + { + fields.push_back({ TranslateSelfQuestLogIndex(sourceIndex), value }); + continue; + } + if (sourceIndex >= ObserverVisibleItemSourceStart && sourceIndex < ObserverVisibleItemSourceStart + ObserverVisibleItemFieldCount) { diff --git a/src/game/Server/MopUpdateObject.h b/src/game/Server/MopUpdateObject.h index 131c5fa1e..b833e5ebd 100644 --- a/src/game/Server/MopUpdateObject.h +++ b/src/game/Server/MopUpdateObject.h @@ -47,6 +47,17 @@ class ByteBuffer; namespace MopUpdateObject { + // Four keeps fifty five-word quest slots; 18414's CGPlayerData::questLog + // is fifty fifteen-word slots. The extra ten words per client slot are + // MoP objective storage Four does not populate, so this range is the only + // self projection that re-strides instead of shifting by a constant. + static constexpr uint16 SelfQuestLogSourceStart = 166; + static constexpr uint16 SelfQuestLogTargetStart = 171; + static constexpr uint16 SelfQuestLogSlotCount = 50; + static constexpr uint16 SelfQuestLogSourceStride = 5; + static constexpr uint16 SelfQuestLogTargetStride = 15; + static constexpr uint16 SelfQuestLogFieldCount = + SelfQuestLogSlotCount * SelfQuestLogSourceStride; static constexpr uint16 SelfInventorySourceStart = 960; static constexpr uint16 SelfInventoryFieldCount = 172; static constexpr uint16 SelfSkillSourceStart = 1146; @@ -66,6 +77,11 @@ namespace MopUpdateObject uint16 TranslateSelfInventoryIndex(uint16 legacyIndex); + /// Map one legacy quest-log field to its 18414 index, preserving the + /// field's position within its slot while widening the slot stride from + /// five to fifteen. + uint16 TranslateSelfQuestLogIndex(uint16 legacyIndex); + /// Translate one field from Four's legacy Player storage to the narrow /// public observer projection proved for 18414. Returns false for every /// private or otherwise unsupported Player field. diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 2c4dbcdd7..23a70f91d 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -236,10 +236,18 @@ void InitializeOpcodes() DefS(SMSG_SPELL_PERIODIC_AURA_LOG, "SMSG_SPELL_PERIODIC_AURA_LOG"); DefS(SMSG_SPELLDISPELLOG, "SMSG_SPELLDISPELLOG"); DefS(SMSG_SPELLINTERRUPTLOG, "SMSG_SPELLINTERRUPTLOG"); + DefS(SMSG_SPELLINSTAKILLLOG, "SMSG_SPELLINSTAKILLLOG"); + DefS(SMSG_SPELLENERGIZELOG, "SMSG_SPELLENERGIZELOG"); + DefS(SMSG_SPELLHEALLOG, "SMSG_SPELLHEALLOG"); + DefS(SMSG_SPELLDAMAGESHIELD, "SMSG_SPELLDAMAGESHIELD"); + DefS(SMSG_SPELLLOGMISS, "SMSG_SPELLLOGMISS"); DefS(SMSG_AURA_UPDATE, "SMSG_AURA_UPDATE"); DefS(SMSG_UPDATE_OBJECT, "SMSG_UPDATE_OBJECT"); DefS(SMSG_DESTROY_OBJECT, "SMSG_DESTROY_OBJECT"); DefS(SMSG_MESSAGECHAT, "SMSG_MESSAGECHAT"); + DefS(SMSG_CHAT_PLAYER_NOT_FOUND, "SMSG_CHAT_PLAYER_NOT_FOUND"); + DefS(SMSG_CHAT_PLAYER_AMBIGUOUS, "SMSG_CHAT_PLAYER_AMBIGUOUS"); + DefS(SMSG_CHAT_RESTRICTED, "SMSG_CHAT_RESTRICTED"); DefC(CMSG_UNREGISTER_ALL_ADDON_PREFIXES, "CMSG_UNREGISTER_ALL_ADDON_PREFIXES", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleUnregisterAddonPrefixesOpcode); DefC(CMSG_ADDON_REGISTERED_PREFIXES, "CMSG_ADDON_REGISTERED_PREFIXES", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleAddonRegisteredPrefixesOpcode); // 18414 /say requests carry a uint32 language followed by the bit-packed message body. @@ -322,6 +330,27 @@ void InitializeOpcodes() DefS(SMSG_ATTACKSTART, "SMSG_ATTACKSTART"); DefS(SMSG_ATTACKSTOP, "SMSG_ATTACKSTOP"); DefS(SMSG_ATTACKERSTATEUPDATE, "SMSG_ATTACKERSTATEUPDATE"); + DefS(SMSG_PARTYKILLLOG, "SMSG_PARTYKILLLOG"); + DefS(SMSG_DUEL_OUTOFBOUNDS, "SMSG_DUEL_OUTOFBOUNDS"); + DefS(SMSG_DUEL_INBOUNDS, "SMSG_DUEL_INBOUNDS"); + DefS(SMSG_DUEL_COMPLETE, "SMSG_DUEL_COMPLETE"); + DefS(SMSG_DUEL_COUNTDOWN, "SMSG_DUEL_COUNTDOWN"); + DefS(SMSG_DUEL_REQUESTED, "SMSG_DUEL_REQUESTED"); + DefS(SMSG_DUEL_WINNER, "SMSG_DUEL_WINNER"); + DefS(SMSG_START_MIRROR_TIMER, "SMSG_START_MIRROR_TIMER"); + DefS(SMSG_STOP_MIRROR_TIMER, "SMSG_STOP_MIRROR_TIMER"); + DefS(SMSG_CHANNEL_START, "SMSG_CHANNEL_START"); + DefS(SMSG_CHANNEL_UPDATE, "SMSG_CHANNEL_UPDATE"); + DefS(SMSG_RESYNC_RUNES, "SMSG_RESYNC_RUNES"); + DefS(SMSG_ADD_RUNE_POWER, "SMSG_ADD_RUNE_POWER"); + DefS(SMSG_CONVERT_RUNE, "SMSG_CONVERT_RUNE"); + DefS(SMSG_THREAT_UPDATE, "SMSG_THREAT_UPDATE"); + DefS(SMSG_HIGHEST_THREAT_UPDATE, "SMSG_HIGHEST_THREAT_UPDATE"); + DefS(SMSG_THREAT_CLEAR, "SMSG_THREAT_CLEAR"); + DefS(SMSG_THREAT_REMOVE, "SMSG_THREAT_REMOVE"); + DefS(SMSG_DISMOUNT, "SMSG_DISMOUNT"); + DefS(SMSG_PRE_RESURRECT, "SMSG_PRE_RESURRECT"); + DefS(SMSG_UPDATE_COMBO_POINTS, "SMSG_UPDATE_COMBO_POINTS"); DefS(SMSG_CANCEL_COMBAT, "SMSG_CANCEL_COMBAT"); DefS(SMSG_CANCEL_AUTO_REPEAT, "SMSG_CANCEL_AUTO_REPEAT"); DefS(SMSG_AI_REACTION, "SMSG_AI_REACTION"); @@ -418,6 +447,9 @@ void InitializeOpcodes() // distinguish enter from leave; the quest marker reply batches packed GUIDs. DefC(CMSG_AREATRIGGER, "CMSG_AREATRIGGER", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleAreaTriggerOpcode); DefS(SMSG_AREA_TRIGGER_NO_CORPSE, "SMSG_AREA_TRIGGER_NO_CORPSE"); + DefS(SMSG_EXPLORATION_EXPERIENCE, "SMSG_EXPLORATION_EXPERIENCE"); + DefS(SMSG_LOG_XPGAIN, "SMSG_LOG_XPGAIN"); + DefS(SMSG_LEVELUP_INFO, "SMSG_LEVELUP_INFO"); DefC(CMSG_QUESTGIVER_STATUS_MULTIPLE_QUERY, "CMSG_QUESTGIVER_STATUS_MULTIPLE_QUERY", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleQuestgiverStatusMultipleQuery); DefS(SMSG_QUESTGIVER_STATUS_MULTIPLE, "SMSG_QUESTGIVER_STATUS_MULTIPLE"); @@ -447,7 +479,9 @@ void InitializeOpcodes() // the client action does not require a dedicated response packet. DefC(CMSG_QUESTLOG_REMOVE_QUEST, "CMSG_QUESTLOG_REMOVE_QUEST", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleQuestLogRemoveQuest); DefC(CMSG_QUEST_POI_QUERY, "CMSG_QUEST_POI_QUERY", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleQuestPOIQueryOpcode); + DefC(CMSG_QUEST_NPC_QUERY, "CMSG_QUEST_NPC_QUERY", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleQuestNpcQueryOpcode); DefS(SMSG_QUEST_POI_QUERY_RESPONSE, "SMSG_QUEST_POI_QUERY_RESPONSE"); + DefS(SMSG_QUEST_NPC_QUERY_RESPONSE, "SMSG_QUEST_NPC_QUERY_RESPONSE"); // Directly verified 18414 quest turn-in and reward flow. The client sends // a reward item ID, which the handler resolves back to the configured @@ -510,6 +544,7 @@ void InitializeOpcodes() // Wave 35 spirit-healer location state. DefS(SMSG_DEATH_RELEASE_LOC, "SMSG_DEATH_RELEASE_LOC"); + DefS(SMSG_DURABILITY_DAMAGE_DEATH, "SMSG_DURABILITY_DAMAGE_DEATH"); // Binary-proven scheduled cemetery-list refresh. DefC(CMSG_REQUEST_CEMETERY_LIST, "CMSG_REQUEST_CEMETERY_LIST", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleRequestCemeteryListOpcode); @@ -574,6 +609,9 @@ void InitializeOpcodes() DefS(SMSG_SPELL_GO, "SMSG_SPELL_GO"); DefS(SMSG_SPELL_COOLDOWN, "SMSG_SPELL_COOLDOWN"); DefS(SMSG_CLEAR_COOLDOWNS, "SMSG_CLEAR_COOLDOWNS"); + DefS(SMSG_COOLDOWN_EVENT, "SMSG_COOLDOWN_EVENT"); + DefS(SMSG_ITEM_COOLDOWN, "SMSG_ITEM_COOLDOWN"); + DefS(SMSG_CLEAR_TARGET, "SMSG_CLEAR_TARGET"); DefS(SMSG_LEARNED_SPELL, "SMSG_LEARNED_SPELL"); DefS(SMSG_REMOVED_SPELL, "SMSG_REMOVED_SPELL"); DefS(SMSG_SUPERCEDED_SPELL, "SMSG_SUPERCEDED_SPELL"); diff --git a/src/game/Server/Opcodes.h b/src/game/Server/Opcodes.h index 92cb23a86..d0060667c 100644 --- a/src/game/Server/Opcodes.h +++ b/src/game/Server/Opcodes.h @@ -518,6 +518,7 @@ enum OpcodesList SMSG_COOLDOWN_CHEAT = 0x0432, // 5.4.8 18414 (Wow.exe leaf; name reference-consensus) SMSG_SPELL_DELAYED = 0x087A, // 5.4.8 18414 (Wow.exe leaf; name reference-consensus) CMSG_QUEST_POI_QUERY = 0x10C2, // 5.4.8 18414 (Wow.exe binary) + CMSG_QUEST_NPC_QUERY = 0x1DAE, // 5.4.8 18414 (live client sends it; name reference-consensus) SMSG_QUEST_POI_QUERY_RESPONSE = 0x067F, // 5.4.8 18414 (Wow.exe leaf; name reference-consensus) SMSG_INVALID_PROMOTION_CODE = 0x1A0E, // 5.4.8 18414 (Wow.exe leaf; name single-source fork) MSG_GM_BIND_OTHER = 0x11E9, // (no client leaf) diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index d4a1df05c..f7584e698 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,9 +175,9 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=361, DOC=438, DORMANT=721 - * SMSG: ACTIVE=221, DOC=271, DORMANT=433 - * CMSG: ACTIVE=140, DOC=167, DORMANT=288 + * STATUS TOTALS: ACTIVE=399, DOC=437, DORMANT=684 + * SMSG: ACTIVE=258, DOC=271, DORMANT=396 + * CMSG: ACTIVE=141, DOC=166, DORMANT=288 */ // CAVEATS -- read before trusting any single row: @@ -389,8 +389,8 @@ typedef uint16_t uint16; * SMSG_UPDATE_LAST_INSTANCE 0x189B DORMANT [medium-conf] * SMSG_EXPECTED_SPAM_RECORDS 0x18C0 DORMANT [medium-conf] * SMSG_MESSAGECHAT 0x1A9A ACTIVE [medium-conf] - * SMSG_DURABILITY_DAMAGE_DEATH 0x1E3E DORMANT [medium-conf] - * SMSG_LOG_XPGAIN 0x1E9A DORMANT + * SMSG_DURABILITY_DAMAGE_DEATH 0x1E3E ACTIVE [Wow.exe binary: empty route to retained DURABILITYDAMAGE_DEATH semantic] + * SMSG_LOG_XPGAIN 0x1E9A ACTIVE [Wow.exe binary: sub_6F7E25 packed reader; sub_CE07DA combat-log semantic] * * -- CheckExecutableSignature.cpp (1) -- * SMSG_UI_TIME 0x0027 ACTIVE [low-conf] @@ -442,13 +442,13 @@ typedef uint16_t uint16; * SMSG_UNKNOWN_0x14E0 0x14E0 DOC [low-conf] * * -- DuelInfo.cpp (7) -- - * SMSG_DUEL_OUTOFBOUNDS 0x001A DORMANT [low-conf] - * SMSG_DUEL_REQUESTED 0x0022 DORMANT + * SMSG_DUEL_OUTOFBOUNDS 0x001A ACTIVE [high-conf] + * SMSG_DUEL_REQUESTED 0x0022 ACTIVE [high-conf] * SMSG_UNKNOWN_0x083F 0x083F DOC - * SMSG_DUEL_WINNER 0x10E1 DORMANT [low-conf] - * SMSG_DUEL_COUNTDOWN 0x129F DORMANT [low-conf] - * SMSG_DUEL_INBOUNDS 0x163A DORMANT [low-conf] - * SMSG_DUEL_COMPLETE 0x1C0A DORMANT [low-conf] + * SMSG_DUEL_WINNER 0x10E1 ACTIVE [high-conf] + * SMSG_DUEL_COUNTDOWN 0x129F ACTIVE [high-conf] + * SMSG_DUEL_INBOUNDS 0x163A ACTIVE [high-conf] + * SMSG_DUEL_COMPLETE 0x1C0A ACTIVE [high-conf] * * -- Effect_C.cpp (1) -- * SMSG_ACHIEVEMENT_EARNED 0x080B DORMANT [medium-conf] @@ -496,23 +496,23 @@ typedef uint16_t uint16; * SMSG_OVERRIDE_LIGHT 0x068A DORMANT [medium-conf] * SMSG_WEATHER 0x06AB ACTIVE [medium-conf] * SMSG_UNKNOWN_0x06BA 0x06BA DOC [low-conf] - * SMSG_UPDATE_COMBO_POINTS 0x082F DORMANT + * SMSG_UPDATE_COMBO_POINTS 0x082F ACTIVE [high-conf] * SMSG_TALENTS_INVOLUNTARILY_RESET 0x088A DORMANT [low-conf] * SMSG_AREA_TRIGGER_NO_CORPSE 0x089E ACTIVE [high-conf] * SMSG_GUILD_BANK_LIST 0x0B79 DORMANT [medium-conf] * SMSG_MONEY_NOTIFY 0x0C0F DORMANT [medium-conf] * SMSG_ITEM_PUSH_RESULT 0x0E0A ACTIVE * SMSG_CORPSE_TRANSPORT_QUERY 0x0E0B ACTIVE server-binding=SMSG_CORPSE_QUERY_RESPONSE - * SMSG_START_MIRROR_TIMER 0x0E12 DORMANT + * SMSG_START_MIRROR_TIMER 0x0E12 ACTIVE [high-conf] * SMSG_UNKNOWN_0x0E2B 0x0E2B DOC * SMSG_UNKNOWN_0x0E2E 0x0E2E DOC [medium-conf] * SMSG_GUILD_INVITE_CANCEL 0x0FE1 DORMANT [low-conf] * SMSG_PVP_CREDIT 0x100A DORMANT * SMSG_UNKNOWN_0x101F 0x101F DOC [medium-conf] - * SMSG_STOP_MIRROR_TIMER 0x1026 DORMANT [medium-conf] + * SMSG_STOP_MIRROR_TIMER 0x1026 ACTIVE [high-conf] * SMSG_PLAY_SOUND 0x102A ACTIVE * SMSG_GM_PLAYER_INFO 0x102B DORMANT [medium-conf] - * SMSG_CLEAR_TARGET 0x1061 DORMANT + * SMSG_CLEAR_TARGET 0x1061 ACTIVE [Wow.exe binary: sub_6D4AFB packed GUID reader; sub_85876E target-clear terminal] * SMSG_PROPOSE_LEVEL_GRANT 0x109A DORMANT * SMSG_UPDATE_INSTANCE_OWNERSHIP 0x10E0 DORMANT [medium-conf] * SMSG_REFER_A_FRIEND_EXPIRED 0x1143 DORMANT [medium-conf] @@ -590,8 +590,8 @@ typedef uint16_t uint16; * SMSG_UNKNOWN_0x1E68 0x1E68 ACTIVE [low-conf] server-binding=SMSG_GUILD_EVENT_DISBANDED * * -- IncomingResurrection.cpp (2) -- - * SMSG_RESYNC_RUNES 0x15E3 DORMANT [low-conf] - * SMSG_ADD_RUNE_POWER 0x1860 DORMANT [low-conf] + * SMSG_RESYNC_RUNES 0x15E3 ACTIVE [high-conf] + * SMSG_ADD_RUNE_POWER 0x1860 ACTIVE [high-conf] * * -- ItemSocketInfo.cpp (1) -- * SMSG_SHOW_BANK 0x0007 DORMANT [low-conf] @@ -736,7 +736,7 @@ typedef uint16_t uint16; * SMSG_QUESTGIVER_QUEST_LIST 0x02D4 ACTIVE [high-conf] * SMSG_UNKNOWN_0x02EF 0x02EF DOC * SMSG_QUESTGIVER_QUEST_COMPLETE 0x0346 ACTIVE - * SMSG_QUEST_NPC_QUERY_RESPONSE 0x036D DORMANT [medium-conf] + * SMSG_QUEST_NPC_QUERY_RESPONSE 0x036D ACTIVE [medium-conf] * SMSG_UNKNOWN_0x040F 0x040F DOC * SMSG_UNKNOWN_0x041E 0x041E DOC [medium-conf] * SMSG_REQUEST_CEMETERY_LIST_RESPONSE 0x042A ACTIVE [medium-conf] @@ -747,8 +747,8 @@ typedef uint16_t uint16; * SMSG_UNKNOWN_0x04AA 0x04AA DOC [medium-conf] * SMSG_ARENA_ERROR 0x04BA DORMANT [medium-conf] * SMSG_VOICE_PARENTAL_CONTROLS 0x04BF DORMANT [medium-conf] - * SMSG_SPELLDAMAGESHIELD 0x05F3 DORMANT - * SMSG_CHAT_PLAYER_AMBIGUOUS 0x061A DORMANT [medium-conf] + * SMSG_SPELLDAMAGESHIELD 0x05F3 ACTIVE + * SMSG_CHAT_PLAYER_AMBIGUOUS 0x061A ACTIVE [medium-conf] * SMSG_PLAY_TIME_WARNING 0x062A DORMANT [medium-conf] * SMSG_DISMOUNTRESULT 0x062F DORMANT [medium-conf] * SMSG_QUEST_POI_QUERY_RESPONSE 0x067F ACTIVE [medium-conf] @@ -767,8 +767,8 @@ typedef uint16_t uint16; * SMSG_PLAYERBOUND 0x088E ACTIVE * SMSG_UNKNOWN_0x089F 0x089F ACTIVE [medium-conf] server-binding=SMSG_SAVE_GUILD_EMBLEM * SMSG_UNKNOWN_0x08FB 0x08FB DOC - * SMSG_SPELLINSTAKILLLOG 0x09F8 DORMANT - * SMSG_SPELLHEALLOG 0x09FB DORMANT + * SMSG_SPELLINSTAKILLLOG 0x09F8 ACTIVE + * SMSG_SPELLHEALLOG 0x09FB ACTIVE * SMSG_UNKNOWN_0x0A2F 0x0A2F DOC [medium-conf] * SMSG_UNKNOWN_0x0A3B 0x0A3B DOC * SMSG_GUILD_RANKS_UPDATE 0x0A60 ACTIVE [medium-conf] @@ -782,7 +782,7 @@ typedef uint16_t uint16; * SMSG_UNKNOWN_0x0C3B 0x0C3B DOC [medium-conf] * SMSG_UNKNOWN_0x0C8E 0x0C8E DOC * SMSG_UNKNOWN_0x0D51 0x0D51 DOC [medium-conf] - * SMSG_SPELLENERGIZELOG 0x0D79 DORMANT [medium-conf] + * SMSG_SPELLENERGIZELOG 0x0D79 ACTIVE [medium-conf] * SMSG_SPELLDISPELLOG 0x0DF9 ACTIVE * SMSG_MOUNTRESULT 0x0E0F DORMANT [medium-conf] * SMSG_ITEM_EXPIRE_PURCHASE_REFUND 0x0E33 DORMANT @@ -796,7 +796,7 @@ typedef uint16_t uint16; * SMSG_UNKNOWN_0x1023 0x1023 DOC * SMSG_RESURRECT_REQUEST 0x1062 DORMANT * SMSG_DEATH_RELEASE_LOC 0x1063 ACTIVE [medium-conf] - * SMSG_CHAT_PLAYER_NOT_FOUND 0x1082 DORMANT [medium-conf] + * SMSG_CHAT_PLAYER_NOT_FOUND 0x1082 ACTIVE [medium-conf] * SMSG_UNKNOWN_0x108A 0x108A DOC [medium-conf] * SMSG_ITEM_ENCHANT_TIME_UPDATE 0x10A2 ACTIVE * SMSG_UNKNOWN_0x10BB 0x10BB DOC [medium-conf] @@ -819,7 +819,7 @@ typedef uint16_t uint16; * SMSG_VOID_STORAGE_TRANSFER_CHANGES 0x14BA DORMANT [medium-conf] * SMSG_REMOVED_SPELL 0x14C3 ACTIVE [high-conf] * SMSG_BUY_FAILED 0x1563 ACTIVE [medium-conf] - * SMSG_SPELLLOGMISS 0x1570 DORMANT + * SMSG_SPELLLOGMISS 0x1570 ACTIVE * SMSG_SHOW_NEURTRAL_PLAYER_FACTION_SELECT_UI 0x15E0 DORMANT [medium-conf] * SMSG_UNKNOWN_0x161A 0x161A DOC [medium-conf] * SMSG_GMTICKET_SYSTEMSTATUS 0x163B ACTIVE [medium-conf] @@ -829,18 +829,18 @@ typedef uint16_t uint16; * SMSG_INSPECT_RESULTS 0x1842 ACTIVE * SMSG_SPELLINTERRUPTLOG 0x1851 ACTIVE * SMSG_GODMODE 0x1862 DORMANT [medium-conf] - * SMSG_EXPLORATION_EXPERIENCE 0x189A DORMANT + * SMSG_EXPLORATION_EXPERIENCE 0x189A ACTIVE [Wow.exe binary: sub_6BB9C1 reads area ID then experience; sub_7B1384 retained semantic] * SMSG_TRAINER_LIST 0x189F DORMANT [medium-conf] * SMSG_REPORT_PVP_AFK_RESULT 0x18BE DORMANT [medium-conf] * SMSG_GROUP_SET_LEADER 0x18BF DORMANT [medium-conf] * SMSG_ITEM_TIME_UPDATE 0x18C1 ACTIVE * SMSG_PETGODMODE 0x1940 DORMANT [medium-conf] * SMSG_SUPERCEDED_SPELL 0x1943 ACTIVE [high-conf] - * SMSG_LEVELUP_INFO 0x1961 DORMANT + * SMSG_LEVELUP_INFO 0x1961 ACTIVE [Wow.exe binary: sub_6BAC39 reads 13 uint32 fields; sub_7B12E9 maps them to PLAYER_LEVEL_UP] * SMSG_UNKNOWN_0x19C2 0x19C2 DOC [medium-conf] - * SMSG_CONVERT_RUNE 0x1A1B DORMANT [medium-conf] + * SMSG_CONVERT_RUNE 0x1A1B ACTIVE [high-conf] * SMSG_UNKNOWN_0x1A2B 0x1A2B DOC [medium-conf] - * SMSG_CHAT_RESTRICTED 0x1A3B DORMANT [medium-conf] + * SMSG_CHAT_RESTRICTED 0x1A3B ACTIVE [medium-conf] * SMSG_TIME_SYNC_REQ 0x1A8F ACTIVE * SMSG_LIST_INVENTORY 0x1AAE ACTIVE [medium-conf] * SMSG_GUILD_DECLINE 0x1AF9 DORMANT [medium-conf] @@ -935,10 +935,10 @@ typedef uint16_t uint16; * SMSG_GAMEOBJECT_RESET_STATE 0x100E DORMANT * SMSG_SPELL_START 0x107A ACTIVE * SMSG_SET_FLAT_SPELL_MODIFIER 0x10F2 DORMANT - * SMSG_CHANNEL_START 0x10F9 DORMANT - * SMSG_COOLDOWN_EVENT 0x1163 DORMANT + * SMSG_CHANNEL_START 0x10F9 ACTIVE [high-conf] + * SMSG_COOLDOWN_EVENT 0x1163 ACTIVE * SMSG_UNKNOWN_0x117A 0x117A DOC - * SMSG_CHANNEL_UPDATE 0x11D9 DORMANT + * SMSG_CHANNEL_UPDATE 0x11D9 ACTIVE [high-conf] * SMSG_PLAY_SPELL_VISUAL_KIT 0x11E3 DORMANT * SMSG_NOTIFY_MISSILE_TRAJECTORY_COLLISION 0x120A DORMANT * SMSG_CAST_FAILED 0x143A ACTIVE @@ -973,7 +973,7 @@ typedef uint16_t uint16; * SMSG_SET_PLAY_HOVER_ANIM 0x069F DORMANT * * -- UnitCombatLog_C.cpp (6) -- - * SMSG_PARTYKILLLOG 0x048A DORMANT + * SMSG_PARTYKILLLOG 0x048A ACTIVE [Wow.exe binary: sub_6F2FE4 reads killer/victim GUIDs; sub_841B83 emits party-kill event] * SMSG_DISPEL_FAILED 0x085B DORMANT * SMSG_SPELL_PERIODIC_AURA_LOG 0x0CF2 ACTIVE * SMSG_ENCHANTMENTLOG 0x12A3 DORMANT @@ -1021,7 +1021,7 @@ typedef uint16_t uint16; * SMSG_MIRROR_IMAGE_COMPONENTED_DATA 0x04D9 DORMANT * SMSG_MOVE_KNOCK_BACK 0x0562 DORMANT * SMSG_PLAY_SPELL_VISUAL 0x061E DORMANT - * SMSG_THREAT_UPDATE 0x0632 DORMANT + * SMSG_THREAT_UPDATE 0x0632 ACTIVE [high-conf] * SMSG_AI_REACTION 0x06AF ACTIVE * SMSG_SPLINE_MOVE_ROOT 0x0728 DORMANT * SMSG_MOVE_SET_SWIM_SPEED 0x0817 ACTIVE @@ -1053,7 +1053,7 @@ typedef uint16_t uint16; * SMSG_MOVE_UNSET_CAN_TURN_WHILE_FALLING 0x0D61 DOC * SMSG_MOVE_UPDATE_TURN_RATE 0x0D62 DORMANT * SMSG_SPLINE_MOVE_UNSET_FLYING 0x0DE2 DORMANT - * SMSG_DISMOUNT 0x0E3A DORMANT + * SMSG_DISMOUNT 0x0E3A ACTIVE [high-conf] * SMSG_SPLINE_MOVE_START_SWIM 0x0F29 DORMANT * SMSG_UNKNOWN_0x100B 0x100B DOC * SMSG_CLIENT_CONTROL_UPDATE 0x1043 ACTIVE @@ -1063,7 +1063,7 @@ typedef uint16_t uint16; * SMSG_MOVE_UPDATE_REMOVE_MOVEMENT_FORCE 0x1464 DORMANT * SMSG_HEALTH_UPDATE 0x148B DORMANT * SMSG_SET_VEHICLE_REC_ID 0x149F DORMANT - * SMSG_HIGHEST_THREAT_UPDATE 0x14AE DORMANT + * SMSG_HIGHEST_THREAT_UPDATE 0x14AE ACTIVE [high-conf] * SMSG_UNKNOWN_0x1553 0x1553 DOC * SMSG_MOVE_UPDATE_RUN_SPEED 0x158E DORMANT * SMSG_MOVE_GRAVITY_DISABLE 0x159F DORMANT @@ -1075,7 +1075,7 @@ typedef uint16_t uint16; * SMSG_SPLINE_MOVE_STOP_SWIM 0x1798 DORMANT * SMSG_MOVE_SET_PITCH_RATE 0x17AB DORMANT * SMSG_MOVE_SET_HOVER 0x1802 DORMANT - * SMSG_THREAT_CLEAR 0x180B DORMANT + * SMSG_THREAT_CLEAR 0x180B ACTIVE [high-conf] * SMSG_MOVE_UPDATE_COLLISION_HEIGHT 0x1812 DORMANT * SMSG_UNKNOWN_0x181B 0x181B DOC * SMSG_SPLINE_MOVE_SET_WATER_WALK 0x1823 ACTIVE @@ -1085,8 +1085,8 @@ typedef uint16_t uint16; * SMSG_UNKNOWN_0x186F 0x186F DOC * SMSG_SPLINE_MOVE_SET_FEATHER_FALL 0x1893 ACTIVE * SMSG_SPLINE_MOVE_SET_LAND_WALK 0x18B6 ACTIVE - * SMSG_THREAT_REMOVE 0x1960 DORMANT - * SMSG_PRE_RESURRECT 0x19C0 DORMANT + * SMSG_THREAT_REMOVE 0x1960 ACTIVE [high-conf] + * SMSG_PRE_RESURRECT 0x19C0 ACTIVE [high-conf] * SMSG_MONSTER_MOVE 0x1A07 ACTIVE * SMSG_PLAYER_MOVE 0x1A32 ACTIVE * SMSG_PET_DISMISS_SOUND 0x1ABB DORMANT @@ -1273,7 +1273,7 @@ typedef uint16_t uint16; * SMSG_UNKNOWN_0x188B 0x188B DOC [unattributed] handler never installed in this build * SMSG_UNKNOWN_0x18AA 0x18AA DOC [unattributed] handler never installed in this build * SMSG_UNKNOWN_0x18BB 0x18BB DOC [unattributed] handler never installed in this build - * SMSG_ITEM_COOLDOWN 0x1904 DORMANT [unattributed] dynamic slot 772 installed by 0x78F488 + * SMSG_ITEM_COOLDOWN 0x1904 ACTIVE [Wow.exe binary: dynamic slot 772 handler sub_77D70B reads uint64 item GUID then uint32 spell ID] * SMSG_UNKNOWN_0x1949 0x1949 DOC [unattributed] special-control (ingress) * SMSG_UNKNOWN_0x1968 0x1968 DOC [unattributed] special-control (ingress) * SMSG_PONG 0x1969 ACTIVE [unattributed] special-control (ingress) @@ -1879,7 +1879,7 @@ typedef uint16_t uint16; * CMSG_UNKNOWN_0x1D9B 0x1D9B DOC * CMSG_LFG_PROPOSAL_RESPONSE 0x1D9D DORMANT * CMSG_LF_GUILD_SET_GUILD_POST 0x1D9F DOC - * CMSG_QUEST_NPC_QUERY 0x1DAE DOC + * CMSG_QUEST_NPC_QUERY 0x1DAE ACTIVE * CMSG_UNKNOWN_0x1DB9 0x1DB9 DOC * CMSG_SEND_MAIL 0x1DBA DORMANT * CMSG_LOAD_SCREEN 0x1DBD ACTIVE diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index ac17a10b2..3c04fa026 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -335,18 +335,29 @@ static bool IsEnterWorldConverted(uint16 opcode) case SMSG_SPELL_PERIODIC_AURA_LOG: // MopCombatLogPackets::BuildPeriodicAuraLog case SMSG_SPELLDISPELLOG: // MopCombatLogPackets::BuildDispelLog case SMSG_SPELLINTERRUPTLOG: // MopCombatLogPackets::BuildSpellInterruptLog + case SMSG_SPELLINSTAKILLLOG: // MopCombatLogPackets::BuildSpellInstakillLog + case SMSG_SPELLENERGIZELOG: // MopCombatLogPackets::BuildSpellEnergizeLog + case SMSG_SPELLHEALLOG: // MopCombatLogPackets::BuildSpellHealLog + case SMSG_SPELLDAMAGESHIELD: // MopCombatLogPackets::BuildSpellDamageShieldLog + case SMSG_SPELLLOGMISS: // MopCombatLogPackets::BuildSpellMissLog case SMSG_CAST_FAILED: // MopSpellPackets::BuildCastFailed case SMSG_PET_CAST_FAILED: // MopSpellPackets::BuildCastFailed (pet bit order) case SMSG_SPELL_START: // MopSpellPackets::BuildSpellStart case SMSG_SPELL_GO: // MopSpellPackets::BuildSpellGo case SMSG_SPELL_COOLDOWN: // MopSpellPackets::BuildSpellCooldown case SMSG_CLEAR_COOLDOWNS: // MopSpellPackets::BuildClearCooldowns + case SMSG_COOLDOWN_EVENT: // MopSpellPackets::BuildCooldownEvent + case SMSG_ITEM_COOLDOWN: // MopSpellPackets::BuildItemCooldown + case SMSG_CLEAR_TARGET: // MopSpellPackets::BuildClearTarget case SMSG_LEARNED_SPELL: // MopSpellPackets::BuildLearnedSpell case SMSG_REMOVED_SPELL: // MopSpellPackets::BuildRemovedSpell case SMSG_SUPERCEDED_SPELL: // MopSpellPackets::BuildSupersededSpell case SMSG_PET_LEARNED_SPELL: // MopSpellPackets::BuildPetLearnedSpell case SMSG_PET_REMOVED_SPELL: // MopSpellPackets::BuildPetRemovedSpell case SMSG_MESSAGECHAT: // MopChatPackets::BuildMessage + case SMSG_CHAT_PLAYER_NOT_FOUND: // MopChatPackets::BuildPlayerNotFound + case SMSG_CHAT_PLAYER_AMBIGUOUS: // MopChatPackets::BuildPlayerAmbiguous + case SMSG_CHAT_RESTRICTED: // MopChatPackets::BuildChatRestrictedNotice case SMSG_CHANNEL_NOTIFY: // MopChannelPackets direct 18414 subtype serializers case SMSG_CHANNEL_LIST: // MopChannelPackets::BuildList case SMSG_TEXT_EMOTE: // MopChatPackets::BuildTextEmote @@ -358,6 +369,27 @@ static bool IsEnterWorldConverted(uint16 opcode) case SMSG_ATTACKSTART: // MopCompactPackets::BuildAttackStart case SMSG_ATTACKSTOP: // MopCompactPackets::BuildAttackStop case SMSG_ATTACKERSTATEUPDATE: // nested UnitCombat_C record; reader sub_858A94 + case SMSG_PARTYKILLLOG: // MopCompactPackets::BuildPartyKillLog + case SMSG_DUEL_OUTOFBOUNDS: // MopDuelPackets::BuildOutOfBounds + case SMSG_DUEL_INBOUNDS: // MopDuelPackets::BuildInBounds + case SMSG_DUEL_COMPLETE: // MopDuelPackets::BuildComplete + case SMSG_DUEL_COUNTDOWN: // MopDuelPackets::BuildCountdown + case SMSG_DUEL_REQUESTED: // MopDuelPackets::BuildRequested + case SMSG_DUEL_WINNER: // MopDuelPackets::BuildWinner + case SMSG_START_MIRROR_TIMER: // MopMirrorTimerPackets::BuildStart + case SMSG_STOP_MIRROR_TIMER: // MopMirrorTimerPackets::BuildStop + case SMSG_CHANNEL_START: // MopSpellPackets::BuildChannelStart + case SMSG_CHANNEL_UPDATE: // MopSpellPackets::BuildChannelUpdate + case SMSG_RESYNC_RUNES: // MopRunePackets::BuildResync + case SMSG_ADD_RUNE_POWER: // MopRunePackets::BuildAddPower + case SMSG_CONVERT_RUNE: // MopRunePackets::BuildConvert + case SMSG_THREAT_UPDATE: // MopThreatPackets::BuildUpdate + case SMSG_HIGHEST_THREAT_UPDATE: // MopThreatPackets::BuildHighest + case SMSG_THREAT_CLEAR: // MopThreatPackets::BuildClear + case SMSG_THREAT_REMOVE: // MopThreatPackets::BuildRemove + case SMSG_DISMOUNT: // MopCompactPackets::BuildDismount + case SMSG_PRE_RESURRECT: // MopCompactPackets::BuildPreResurrect + case SMSG_UPDATE_COMBO_POINTS: // MopComboPointPackets::BuildUpdate case SMSG_CANCEL_COMBAT: // Empty reader; terminal clears local-player combat state case SMSG_CANCEL_AUTO_REPEAT: // packed unit GUID; Unit_C leaf 0x819546 clears auto-repeat case SMSG_AI_REACTION: // packed unit GUID plus reaction; Unit_C.cpp leaf 0x80AD80 @@ -396,6 +428,9 @@ static bool IsEnterWorldConverted(uint16 opcode) case SMSG_FISH_ESCAPED: // empty; direct terminal leaf displays ERR_FISH_ESCAPED case SMSG_FISH_NOT_HOOKED: // empty; direct terminal leaf displays ERR_FISH_NOT_HOOKED case SMSG_AREA_TRIGGER_NO_CORPSE: // MopAreaTriggerPackets::BuildNoCorpse + case SMSG_EXPLORATION_EXPERIENCE: // MopAreaTriggerPackets::BuildExplorationExperience + case SMSG_LOG_XPGAIN: // MopProgressionPackets::BuildExperienceGain + case SMSG_LEVELUP_INFO: // MopProgressionPackets::BuildLevelUpInfo case SMSG_QUESTGIVER_STATUS_MULTIPLE: // MopQuestStatusPackets::BuildMultipleStatus case SMSG_QUESTGIVER_QUEST_LIST: // MopQuestGiverPackets::BuildQuestList case SMSG_QUESTGIVER_QUEST_DETAILS: // MopQuestGiverPackets::BuildQuestDetails @@ -410,6 +445,7 @@ static bool IsEnterWorldConverted(uint16 opcode) case SMSG_QUESTUPDATE_COMPLETE: // MopQuestGiverPackets::BuildQuestUpdateComplete case SMSG_QUEST_QUERY_RESPONSE: // MopQuestQueryPackets::BuildResponse / BuildAbsentResponse case SMSG_QUEST_POI_QUERY_RESPONSE: // MopQueryPackets::BuildQuestPoiQueryResponse + case SMSG_QUEST_NPC_QUERY_RESPONSE: // MopQueryPackets::BuildQuestNpcQueryResponse case SMSG_NPC_TEXT_UPDATE: // MopNpcTextPackets::BuildResponse case SMSG_CHAR_CUSTOMIZE: // MopCharacterCustomizePackets::BuildResponse return true; @@ -426,6 +462,7 @@ static bool IsEnterWorldConverted(uint16 opcode) case SMSG_CORPSE_QUERY_RESPONSE: // MopQueryPackets::BuildCorpseQueryResponse case SMSG_CORPSE_MAP_POSITION_QUERY_RESPONSE: // MopQueryPackets::BuildCorpseMapPositionQueryResponse case SMSG_DEATH_RELEASE_LOC: // MopDeathPackets::BuildDeathReleaseLocation + case SMSG_DURABILITY_DAMAGE_DEATH: // MopDeathPackets::BuildDurabilityDamageDeath case SMSG_REQUEST_CEMETERY_LIST_RESPONSE: // MopDeathPackets::BuildCemeteryListResponse case SMSG_BATTLE_PET_JOURNAL: // MopBattlePetPackets::BuildEmptyJournal case SMSG_QUEST_CONFIRM_ACCEPT: // MopQuestPackets::BuildQuestConfirmAccept diff --git a/src/game/Server/WorldSession.h b/src/game/Server/WorldSession.h index 4adadf0bb..e2a5f2a96 100644 --- a/src/game/Server/WorldSession.h +++ b/src/game/Server/WorldSession.h @@ -328,6 +328,16 @@ namespace MopQueryPackets std::vector& questIds); bool BuildQuestPoiQueryResponse(WorldPacket& out, std::vector const& response); + + struct QuestNpcResponse + { + uint32 questId = 0; + std::vector npcIds; + }; + + bool ParseQuestNpcQueryRequest(WorldPacket& in, uint32& questId); + bool BuildQuestNpcQueryResponse(WorldPacket& out, + std::vector const& response); } namespace MopStablePackets @@ -951,6 +961,56 @@ inline bool MopQueryPackets::BuildQuestPoiQueryResponse(WorldPacket& out, return true; } +inline bool MopQueryPackets::ParseQuestNpcQueryRequest(WorldPacket& in, + uint32& questId) +{ + if (in.size() < 4) + return false; + + // The 18414 client always sends a 204-byte body but initialises only the + // leading quest id; the remainder is uninitialised client stack memory + // (on the 64-bit client, image pointers identical across a whole run). + // Read the one defined field and deliberately ignore the rest rather than + // validating a length the client does not actually populate. + in.rpos(0); + in >> questId; + in.rpos(in.size()); + return true; +} + +inline bool MopQueryPackets::BuildQuestNpcQueryResponse(WorldPacket& out, + std::vector const& response) +{ + if (response.size() >= (size_t(1) << 21)) + return false; + + for (QuestNpcResponse const& quest : response) + { + if (quest.npcIds.size() >= (size_t(1) << 22)) + return false; + } + + // Grammar from client parser sub_6B8B3B -> sub_6B8A06: a 21-bit quest + // count, then one 22-bit NPC count per quest, then a byte-aligned phase + // of quest id followed by that quest's NPC ids. Confirmed against real + // 18414 retail captures, which decode byte-exact under this reader. + WorldPacket built(SMSG_QUEST_NPC_QUERY_RESPONSE, 4); + built.WriteBits(uint32(response.size()), 21); + for (QuestNpcResponse const& quest : response) + built.WriteBits(uint32(quest.npcIds.size()), 22); + built.FlushBits(); + + for (QuestNpcResponse const& quest : response) + { + built << quest.questId; + for (uint32 npcId : quest.npcIds) + built << npcId; + } + + out = built; + return true; +} + inline uint64 MopStablePackets::ReadStableListRequest(WorldPacket& in) { uint8 const maskOrder[] = { 0, 5, 1, 3, 6, 7, 2, 4 }; @@ -2133,6 +2193,7 @@ class WorldSession void HandleUITimeRequestOpcode(WorldPacket& recv_data); void HandleReadyForAccountDataTimesOpcode(WorldPacket& recv_data); void HandleQuestPOIQueryOpcode(WorldPacket& recv_data); + void HandleQuestNpcQueryOpcode(WorldPacket& recv_data); void HandleSetCurrencyFlagsOpcode(WorldPacket& recv_data); // Reforge diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index cca2df653..c8b22b299 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -102,7 +102,7 @@ add_test(NAME mop_self_values_source COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/mop_self_values_source_test.cmake) -foreach(mutation IN ITEMS inventory_only health_feed progression_feed visible_item_feed skill_feed buyback_feed) +foreach(mutation IN ITEMS inventory_only health_feed progression_feed visible_item_feed skill_feed buyback_feed questlog_feed) add_test(NAME mop_self_values_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} @@ -188,7 +188,25 @@ foreach(mutation IN ITEMS cancel_sender cancel_registration cancel_allowlist can auto_repeat_allowlist auto_repeat_opcode auto_repeat_reference read_item_alias read_item_sender attacker_sender attacker_registration attacker_allowlist attacker_opcode - attacker_reference attacker_envelope) + attacker_reference attacker_envelope + party_kill_mask party_kill_bytes party_kill_sender party_kill_registration + party_kill_allowlist party_kill_reference + duel_complete_bit duel_countdown_width duel_sender duel_registration + duel_allowlist duel_reference + duel_request_mask duel_request_bytes duel_winner_bits duel_winner_realm_order + duel_request_sender duel_winner_sender duel_pair_registration + duel_pair_allowlist duel_pair_reference + mirror_start_order mirror_pause_width mirror_sender mirror_registration + mirror_allowlist mirror_reference + rune_count_width rune_record_order rune_convert_order rune_sender + rune_registration rune_allowlist rune_reference + threat_count_width threat_update_mask threat_highest_mask threat_clear_bytes + threat_remove_mask threat_sender threat_registration threat_allowlist threat_reference + dismount_mask dismount_bytes dismount_sender dismount_registration + dismount_allowlist dismount_reference + pre_resurrect_mask pre_resurrect_bytes pre_resurrect_sender + pre_resurrect_registration pre_resurrect_allowlist pre_resurrect_reference + combo_mask combo_bytes combo_sender combo_registration combo_allowlist combo_reference) add_test(NAME mop_compact_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} @@ -226,6 +244,17 @@ add_test(NAME mop_combat_log_packets_source COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/mop_combat_log_packets_source_test.cmake) +foreach(mutation IN ITEMS instakill_builder instakill_sender instakill_registration instakill_gate instakill_reference + energize_builder energize_sender energize_registration energize_gate energize_reference + heal_builder heal_sender heal_registration heal_gate heal_reference + damage_shield_builder damage_shield_sender damage_shield_registration damage_shield_gate damage_shield_reference + spell_miss_builder spell_miss_sender spell_miss_registration spell_miss_gate spell_miss_reference + spell_miss_count_width spell_miss_count_bound) + add_test(NAME mop_combat_log_packets_source_mutation_${mutation} + COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} + -P ${CMAKE_CURRENT_SOURCE_DIR}/mop_combat_log_packets_source_test.cmake) + set_tests_properties(mop_combat_log_packets_source_mutation_${mutation} PROPERTIES WILL_FAIL TRUE) +endforeach() add_executable(mop_respec_packets_test mop_respec_packets_test.cpp) target_include_directories(mop_respec_packets_test PRIVATE @@ -479,6 +508,14 @@ set_target_properties(mop_quest_poi_query_test PROPERTIES CXX_STANDARD 17 CXX_ST target_link_libraries(mop_quest_poi_query_test PRIVATE game) add_test(NAME mop_quest_poi_query COMMAND mop_quest_poi_query_test) +add_executable(mop_quest_npc_query_test mop_quest_npc_query_test.cpp) +target_include_directories(mop_quest_npc_query_test PRIVATE + ${CMAKE_SOURCE_DIR}/src/game/Server + ${CMAKE_SOURCE_DIR}/src/game) +set_target_properties(mop_quest_npc_query_test PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON) +target_link_libraries(mop_quest_npc_query_test PRIVATE game) +add_test(NAME mop_quest_npc_query COMMAND mop_quest_npc_query_test) + add_test(NAME mop_quest_poi_query_source COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} @@ -660,6 +697,8 @@ foreach(mutation IN ITEMS builder registration allowlist emote_builder emote_rea addon_count_width addon_length_width addon_softcap addon_unregister_registration addon_batch_registration addon_unregister_handler addon_batch_handler addon_group_filter addon_group_delivery addon_guild_filter addon_whisper_filter say_registration say_parser_wiring say_parser_layout say_language_spell say_reference_status afk_registration afk_parser_wiring afk_parser_layout + notice_length not_found_sender ambiguous_sender restricted_sender + notice_registration notice_allowlist notice_reference legacy_wrong_faction legacy_gm_chat legacy_member_count) add_test(NAME mop_chat_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} @@ -1034,7 +1073,9 @@ foreach(mutation IN ITEMS area_parser area_leave no_corpse_builder sender_admission opcode_values npc_parser npc_mapping npc_loader npc_client_registration npc_server_registration npc_admission - npc_legacy_sender npc_db_content npc_reference_status) + npc_legacy_sender npc_db_content npc_reference_status + exploration_wire_order exploration_sender exploration_registration + exploration_admission exploration_reference) add_test(NAME mop_world_quest_interaction_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} @@ -1179,7 +1220,9 @@ add_test(NAME mop_death_packets_source foreach(mutation IN ITEMS wire_order player_call misc_call registration whitelist opcode_value cemetery_wire cemetery_bound cemetery_query cemetery_serializer - cemetery_registration cemetery_whitelist cemetery_opcode cemetery_team_filter) + cemetery_registration cemetery_whitelist cemetery_opcode cemetery_team_filter + durability_death_body durability_death_mirror_sender durability_death_unit_sender + durability_death_registration durability_death_whitelist durability_death_reference) add_test(NAME mop_death_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} @@ -1369,7 +1412,15 @@ foreach(mutation IN ITEMS reader registration target_initializer target_mapping clear_cooldowns_gate clear_cooldowns_reference spellbook_wire_order spellbook_sender spellbook_registration spellbook_gate spellbook_reference pet_spellbook_builder pet_spellbook_sender pet_spellbook_registration pet_spellbook_gate - pet_spellbook_reference) + pet_spellbook_reference + cooldown_event_mask_order cooldown_event_byte_order cooldown_event_sender + cooldown_event_registration cooldown_event_gate cooldown_event_reference + item_cooldown_wire_order item_cooldown_sender item_cooldown_registration + item_cooldown_gate item_cooldown_reference + clear_target_mask_order clear_target_byte_order clear_target_sender + clear_target_registration clear_target_gate clear_target_reference + channel_start_mask_order channel_update_byte_order channel_sender + channel_registration channel_gate channel_reference) add_test(NAME mop_spell_cast_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} @@ -1534,6 +1585,27 @@ foreach(mutation IN ITEMS field_order gameobject_type interact_type guid_mask gu set_tests_properties(mop_quest_progress_packets_source_mutation_${mutation} PROPERTIES WILL_FAIL TRUE) endforeach() +add_executable(mop_player_progression_packets_test mop_player_progression_packets_test.cpp) +target_include_directories(mop_player_progression_packets_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_SOURCE_DIR}/src/shared) +set_target_properties(mop_player_progression_packets_test PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON) +target_link_libraries(mop_player_progression_packets_test PRIVATE game) +add_test(NAME mop_player_progression_packets COMMAND mop_player_progression_packets_test) + +add_test(NAME mop_player_progression_packets_source + COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} + -P ${CMAKE_CURRENT_SOURCE_DIR}/mop_player_progression_packets_source_test.cmake) + +foreach(mutation IN ITEMS wire_order xp_wire_order sender xp_sender registration admission reference + xp_registration xp_admission xp_reference) + add_test(NAME mop_player_progression_packets_source_mutation_${mutation} + COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} + -DMUTATION=${mutation} + -P ${CMAKE_CURRENT_SOURCE_DIR}/mop_player_progression_packets_source_test.cmake) + set_tests_properties(mop_player_progression_packets_source_mutation_${mutation} PROPERTIES WILL_FAIL TRUE) +endforeach() + add_executable(mop_world_state_packets_test mop_world_state_packets_test.cpp) target_include_directories(mop_world_state_packets_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.. diff --git a/src/game/Server/tests/mop_chat_packets_source_test.cmake b/src/game/Server/tests/mop_chat_packets_source_test.cmake index f446b96de..0178fc682 100644 --- a/src/game/Server/tests/mop_chat_packets_source_test.cmake +++ b/src/game/Server/tests/mop_chat_packets_source_test.cmake @@ -168,6 +168,41 @@ elseif(MUTATION STREQUAL "afk_parser_layout") "uint8 const length = in.ReadUInt8();" "uint8 const length = uint8(in.ReadBits(9)); /* damaged AFK length */" chat_header "${chat_header}") +elseif(MUTATION STREQUAL "notice_length") + string(REPLACE + "out.WriteBits(name.size(), 9);" + "out.WriteBits(name.size(), 8); /* damaged notice length */" + chat_header "${chat_header}") +elseif(MUTATION STREQUAL "not_found_sender") + string(REPLACE + "MopChatPackets::BuildPlayerNotFound(data, name)" + "false /* removed player-not-found builder */" + chat_handler "${chat_handler}") +elseif(MUTATION STREQUAL "ambiguous_sender") + string(REPLACE + "MopChatPackets::BuildPlayerAmbiguous(data, name)" + "false /* removed ambiguous-player builder */" + chat_handler "${chat_handler}") +elseif(MUTATION STREQUAL "restricted_sender") + string(REPLACE + "MopChatPackets::BuildChatRestrictedNotice(data, uint8(restriction))" + "/* removed restricted-chat builder */" + chat_handler "${chat_handler}") +elseif(MUTATION STREQUAL "notice_registration") + string(REPLACE + "DefS(SMSG_CHAT_PLAYER_NOT_FOUND, \"SMSG_CHAT_PLAYER_NOT_FOUND\");" + "/* removed player-not-found registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "notice_allowlist") + string(REPLACE + "case SMSG_CHAT_PLAYER_NOT_FOUND:" + "case 0xFFFF: /* removed player-not-found allowlist */" + world_session "${world_session}") +elseif(MUTATION STREQUAL "notice_reference") + string(REPLACE + "SMSG_CHAT_PLAYER_NOT_FOUND 0x1082 ACTIVE" + "SMSG_CHAT_PLAYER_NOT_FOUND 0x1082 DORMANT" + opcode_reference "${opcode_reference}") elseif(MUTATION STREQUAL "legacy_wrong_faction") string(APPEND opcode_header "\nWorldPacket legacy(SMSG_CHAT_WRONG_FACTION);\n") elseif(MUTATION STREQUAL "legacy_gm_chat") @@ -228,6 +263,38 @@ require_once("${opcode_registry}" require_once("${world_session}" "case SMSG_MESSAGECHAT:" "generic chat suppression allowlist") +require_once("${chat_header}" + "out.WriteBits(name.size(), 9);" + "chat notice 9-bit name length") +require_once("${chat_handler}" + "MopChatPackets::BuildPlayerNotFound(data, name)" + "player-not-found sender") +require_once("${chat_handler}" + "MopChatPackets::BuildPlayerAmbiguous(data, name)" + "ambiguous-player sender") +require_once("${chat_handler}" + "MopChatPackets::BuildChatRestrictedNotice(data, uint8(restriction))" + "restricted-chat sender") +foreach(name IN ITEMS SMSG_CHAT_PLAYER_NOT_FOUND SMSG_CHAT_PLAYER_AMBIGUOUS SMSG_CHAT_RESTRICTED) + require_once("${opcode_registry}" + "DefS(${name}, \"${name}\");" + "${name} registration") + require_once("${world_session}" + "case ${name}:" + "${name} suppression allowlist") +endforeach() +require_once("${opcode_reference}" + "SMSG_CHAT_PLAYER_NOT_FOUND 0x1082 ACTIVE" + "player-not-found reference status") +require_once("${opcode_reference}" + "SMSG_CHAT_PLAYER_AMBIGUOUS 0x061A ACTIVE" + "ambiguous-player reference status") +require_once("${opcode_reference}" + "SMSG_CHAT_RESTRICTED 0x1A3B ACTIVE" + "restricted-chat reference status") +forbid("${chat_handler}" "WorldPacket data(SMSG_CHAT_PLAYER_NOT_FOUND" "legacy player-not-found sender") +forbid("${chat_handler}" "WorldPacket data(SMSG_CHAT_PLAYER_AMBIGUOUS" "legacy ambiguous-player sender") +forbid("${chat_handler}" "WorldPacket data(SMSG_CHAT_RESTRICTED" "legacy restricted-chat sender") require_once("${chat_handler}" "MopChatPackets::BuildTextEmote(data," "text-emote response builder call") diff --git a/src/game/Server/tests/mop_chat_packets_test.cpp b/src/game/Server/tests/mop_chat_packets_test.cpp index e0f868698..5ca1d84a6 100644 --- a/src/game/Server/tests/mop_chat_packets_test.cpp +++ b/src/game/Server/tests/mop_chat_packets_test.cpp @@ -179,6 +179,43 @@ static void test_length_boundaries() CHECK(!MopChatPackets::BuildMessage(tagRejected, oversized)); } +static void test_player_name_notices() +{ + WorldPacket notFound; + CHECK(MopChatPackets::BuildPlayerNotFound(notFound, "Al")); + CHECK(notFound.GetOpcode() == SMSG_CHAT_PLAYER_NOT_FOUND); + CHECK(Equal(notFound, { 0x01, 0x00, 'A', 'l' })); + + WorldPacket ambiguous; + CHECK(MopChatPackets::BuildPlayerAmbiguous(ambiguous, "Bob")); + CHECK(ambiguous.GetOpcode() == SMSG_CHAT_PLAYER_AMBIGUOUS); + CHECK(Equal(ambiguous, { 0x01, 0x80, 'B', 'o', 'b' })); + + WorldPacket maximum; + CHECK(MopChatPackets::BuildPlayerNotFound(maximum, + std::string((size_t(1) << 9) - 1, 'x'))); + CHECK(maximum.size() == (size_t(1) << 9) + 1); + CHECK(maximum.contents()[0] == 0xFF); + CHECK(maximum.contents()[1] == 0x80); + + WorldPacket oversized; + CHECK(!MopChatPackets::BuildPlayerAmbiguous(oversized, + std::string(size_t(1) << 9, 'x'))); + CHECK(oversized.empty()); +} + +static void test_chat_restricted_notice() +{ + WorldPacket trial; + MopChatPackets::BuildChatRestrictedNotice(trial, 0); + CHECK(trial.GetOpcode() == SMSG_CHAT_RESTRICTED); + CHECK(Equal(trial, { 0x00 })); + + WorldPacket silenced; + MopChatPackets::BuildChatRestrictedNotice(silenced, 3); + CHECK(Equal(silenced, { 0x03 })); +} + static void test_opcode() { CHECK(uint32(SMSG_MESSAGECHAT) == 0x1A9Au); @@ -189,6 +226,9 @@ static void test_opcode() CHECK(uint32(CMSG_MESSAGECHAT_AFK) < uint32(OPCODE_TABLE_SIZE)); CHECK(uint32(CMSG_UNREGISTER_ALL_ADDON_PREFIXES) == 0x029Fu); CHECK(uint32(CMSG_ADDON_REGISTERED_PREFIXES) == 0x040Eu); + CHECK(uint32(SMSG_CHAT_PLAYER_NOT_FOUND) == 0x1082u); + CHECK(uint32(SMSG_CHAT_PLAYER_AMBIGUOUS) == 0x061Au); + CHECK(uint32(SMSG_CHAT_RESTRICTED) == 0x1A3Bu); } static void test_say_message_request() @@ -332,6 +372,8 @@ int main(int /*argc*/, char** /*argv*/) test_group_message(); test_guild_message(); test_length_boundaries(); + test_player_name_notices(); + test_chat_restricted_notice(); test_opcode(); test_say_message_request(); test_afk_message_request(); diff --git a/src/game/Server/tests/mop_combat_log_packets_source_test.cmake b/src/game/Server/tests/mop_combat_log_packets_source_test.cmake index 11b63e761..4a5a8f156 100644 --- a/src/game/Server/tests/mop_combat_log_packets_source_test.cmake +++ b/src/game/Server/tests/mop_combat_log_packets_source_test.cmake @@ -7,8 +7,66 @@ file(READ "${SOURCE_ROOT}/src/game/Server/WorldSession.cpp" world_session) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/SpellEffectSkillEnchantPet.cpp" dispel_effect) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/SpellEffectTail.cpp" steal_effect) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/SpellEffectObjectCombat.cpp" interrupt_effect) +file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/SpellEffectDamageTeleport.cpp" instakill_effect) +file(READ "${SOURCE_ROOT}/src/game/Object/UnitMeleeDamage.cpp" melee_damage_cpp) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/Spell.h" spell_h) +if(MUTATION STREQUAL "instakill_builder") + string(REPLACE "inline void BuildSpellInstakillLog" "inline void RemovedSpellInstakillLog" spell_h "${spell_h}") +elseif(MUTATION STREQUAL "instakill_sender") + string(REPLACE "MopCombatLogPackets::BuildSpellInstakillLog(data, log);" "/* removed instakill serializer */" instakill_effect "${instakill_effect}") +elseif(MUTATION STREQUAL "instakill_registration") + string(REPLACE "DefS(SMSG_SPELLINSTAKILLLOG, \"SMSG_SPELLINSTAKILLLOG\");" "/* removed instakill registration */" opcodes_cpp "${opcodes_cpp}") +elseif(MUTATION STREQUAL "instakill_gate") + string(REPLACE "case SMSG_SPELLINSTAKILLLOG:" "case SMSG_UNKNOWN_0:" world_session "${world_session}") +elseif(MUTATION STREQUAL "instakill_reference") + string(REPLACE "SMSG_SPELLINSTAKILLLOG 0x09F8 ACTIVE" "SMSG_SPELLINSTAKILLLOG 0x09F8 DORMANT" opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "energize_builder") + string(REPLACE "inline void BuildSpellEnergizeLog" "inline void RemovedSpellEnergizeLog" spell_h "${spell_h}") +elseif(MUTATION STREQUAL "energize_sender") + string(REPLACE "MopCombatLogPackets::BuildSpellEnergizeLog(data, log);" "/* removed energize serializer */" unit_cpp "${unit_cpp}") +elseif(MUTATION STREQUAL "energize_registration") + string(REPLACE "DefS(SMSG_SPELLENERGIZELOG, \"SMSG_SPELLENERGIZELOG\");" "/* removed energize registration */" opcodes_cpp "${opcodes_cpp}") +elseif(MUTATION STREQUAL "energize_gate") + string(REPLACE "case SMSG_SPELLENERGIZELOG:" "case SMSG_UNKNOWN_0:" world_session "${world_session}") +elseif(MUTATION STREQUAL "energize_reference") + string(REPLACE "SMSG_SPELLENERGIZELOG 0x0D79 ACTIVE" "SMSG_SPELLENERGIZELOG 0x0D79 DORMANT" opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "heal_builder") + string(REPLACE "inline void BuildSpellHealLog" "inline void RemovedSpellHealLog" spell_h "${spell_h}") +elseif(MUTATION STREQUAL "heal_sender") + string(REPLACE "MopCombatLogPackets::BuildSpellHealLog(data, log);" "/* removed heal serializer */" unit_cpp "${unit_cpp}") +elseif(MUTATION STREQUAL "heal_registration") + string(REPLACE "DefS(SMSG_SPELLHEALLOG, \"SMSG_SPELLHEALLOG\");" "/* removed heal registration */" opcodes_cpp "${opcodes_cpp}") +elseif(MUTATION STREQUAL "heal_gate") + string(REPLACE "case SMSG_SPELLHEALLOG:" "case SMSG_UNKNOWN_0:" world_session "${world_session}") +elseif(MUTATION STREQUAL "heal_reference") + string(REPLACE "SMSG_SPELLHEALLOG 0x09FB ACTIVE" "SMSG_SPELLHEALLOG 0x09FB DORMANT" opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "damage_shield_builder") + string(REPLACE "inline void BuildSpellDamageShieldLog" "inline void RemovedSpellDamageShieldLog" spell_h "${spell_h}") +elseif(MUTATION STREQUAL "damage_shield_sender") + string(REPLACE "MopCombatLogPackets::BuildSpellDamageShieldLog(data, log);" "/* removed damage-shield serializer */" melee_damage_cpp "${melee_damage_cpp}") +elseif(MUTATION STREQUAL "damage_shield_registration") + string(REPLACE "DefS(SMSG_SPELLDAMAGESHIELD, \"SMSG_SPELLDAMAGESHIELD\");" "/* removed damage-shield registration */" opcodes_cpp "${opcodes_cpp}") +elseif(MUTATION STREQUAL "damage_shield_gate") + string(REPLACE "case SMSG_SPELLDAMAGESHIELD:" "case SMSG_UNKNOWN_0:" world_session "${world_session}") +elseif(MUTATION STREQUAL "damage_shield_reference") + string(REPLACE "SMSG_SPELLDAMAGESHIELD 0x05F3 ACTIVE" "SMSG_SPELLDAMAGESHIELD 0x05F3 DORMANT" opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "spell_miss_builder") + string(REPLACE "inline bool BuildSpellMissLog" "inline bool RemovedSpellMissLog" spell_h "${spell_h}") +elseif(MUTATION STREQUAL "spell_miss_sender") + string(REPLACE "MopCombatLogPackets::BuildSpellMissLog(data, log);" "/* removed spell-miss serializer */" unit_cpp "${unit_cpp}") +elseif(MUTATION STREQUAL "spell_miss_registration") + string(REPLACE "DefS(SMSG_SPELLLOGMISS, \"SMSG_SPELLLOGMISS\");" "/* removed spell-miss registration */" opcodes_cpp "${opcodes_cpp}") +elseif(MUTATION STREQUAL "spell_miss_gate") + string(REPLACE "case SMSG_SPELLLOGMISS:" "case SMSG_UNKNOWN_0:" world_session "${world_session}") +elseif(MUTATION STREQUAL "spell_miss_reference") + string(REPLACE "SMSG_SPELLLOGMISS 0x1570 ACTIVE" "SMSG_SPELLLOGMISS 0x1570 DORMANT" opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "spell_miss_count_width") + string(REPLACE "out.WriteBits(uint32(log.targetCount), 23);" "out.WriteBits(uint32(log.targetCount), 22);" spell_h "${spell_h}") +elseif(MUTATION STREQUAL "spell_miss_count_bound") + string(REPLACE "log.targetCount >= (size_t(1) << 23)" "log.targetCount >= (size_t(1) << 24)" spell_h "${spell_h}") +endif() + if(spell_packets MATCHES "SMSG_SPELLLOGEXECUTE") message(FATAL_ERROR "legacy execute-log opcode remains in SpellPackets.cpp") endif() @@ -97,3 +155,90 @@ endif() if(NOT world_session MATCHES "case SMSG_SPELLINTERRUPTLOG:") message(FATAL_ERROR "spell-interrupt log remains blocked by enter-world suppression") endif() + +if(NOT spell_h MATCHES "BuildSpellInstakillLog") + message(FATAL_ERROR "spell-instakill serializer is absent from the owning spell code") +endif() +if(NOT instakill_effect MATCHES "MopCombatLogPackets::BuildSpellInstakillLog" OR + NOT instakill_effect MATCHES "SMSG_SPELLINSTAKILLLOG") + message(FATAL_ERROR "instakill effect bypasses the 18414 instakill serializer") +endif() +if(NOT opcodes_cpp MATCHES "DefS\\(SMSG_SPELLINSTAKILLLOG, \"SMSG_SPELLINSTAKILLLOG\"\\)") + message(FATAL_ERROR "spell-instakill log lacks outbound opcode metadata") +endif() +if(NOT opcode_reference MATCHES "SMSG_SPELLINSTAKILLLOG[ ]+0x09F8 ACTIVE") + message(FATAL_ERROR "reference inventory does not reflect the active spell-instakill log") +endif() +if(NOT world_session MATCHES "case SMSG_SPELLINSTAKILLLOG:") + message(FATAL_ERROR "spell-instakill log remains blocked by enter-world suppression") +endif() + +if(NOT spell_h MATCHES "BuildSpellEnergizeLog") + message(FATAL_ERROR "spell-energize serializer is absent from the owning spell code") +endif() +if(NOT unit_cpp MATCHES "MopCombatLogPackets::BuildSpellEnergizeLog" OR + NOT unit_cpp MATCHES "SMSG_SPELLENERGIZELOG") + message(FATAL_ERROR "energize sender bypasses the 18414 energize serializer") +endif() +if(NOT opcodes_cpp MATCHES "DefS\\(SMSG_SPELLENERGIZELOG, \"SMSG_SPELLENERGIZELOG\"\\)") + message(FATAL_ERROR "spell-energize log lacks outbound opcode metadata") +endif() +if(NOT opcode_reference MATCHES "SMSG_SPELLENERGIZELOG[ ]+0x0D79 ACTIVE") + message(FATAL_ERROR "reference inventory does not reflect the active spell-energize log") +endif() +if(NOT world_session MATCHES "case SMSG_SPELLENERGIZELOG:") + message(FATAL_ERROR "spell-energize log remains blocked by enter-world suppression") +endif() + +if(NOT spell_h MATCHES "BuildSpellHealLog") + message(FATAL_ERROR "spell-heal serializer is absent from the owning spell code") +endif() +if(NOT unit_cpp MATCHES "MopCombatLogPackets::BuildSpellHealLog" OR + NOT unit_cpp MATCHES "SMSG_SPELLHEALLOG") + message(FATAL_ERROR "heal sender bypasses the 18414 heal serializer") +endif() +if(NOT opcodes_cpp MATCHES "DefS\\(SMSG_SPELLHEALLOG, \"SMSG_SPELLHEALLOG\"\\)") + message(FATAL_ERROR "spell-heal log lacks outbound opcode metadata") +endif() +if(NOT opcode_reference MATCHES "SMSG_SPELLHEALLOG[ ]+0x09FB ACTIVE") + message(FATAL_ERROR "reference inventory does not reflect the active spell-heal log") +endif() +if(NOT world_session MATCHES "case SMSG_SPELLHEALLOG:") + message(FATAL_ERROR "spell-heal log remains blocked by enter-world suppression") +endif() + +if(NOT spell_h MATCHES "BuildSpellDamageShieldLog") + message(FATAL_ERROR "damage-shield serializer is absent from the owning spell code") +endif() +if(NOT melee_damage_cpp MATCHES "MopCombatLogPackets::BuildSpellDamageShieldLog" OR + NOT melee_damage_cpp MATCHES "SMSG_SPELLDAMAGESHIELD") + message(FATAL_ERROR "damage-shield sender bypasses the 18414 serializer") +endif() +if(NOT opcodes_cpp MATCHES "DefS\\(SMSG_SPELLDAMAGESHIELD, \"SMSG_SPELLDAMAGESHIELD\"\\)") + message(FATAL_ERROR "damage-shield log lacks outbound opcode metadata") +endif() +if(NOT opcode_reference MATCHES "SMSG_SPELLDAMAGESHIELD[ ]+0x05F3 ACTIVE") + message(FATAL_ERROR "reference inventory does not reflect the active damage-shield log") +endif() +if(NOT world_session MATCHES "case SMSG_SPELLDAMAGESHIELD:") + message(FATAL_ERROR "damage-shield log remains blocked by enter-world suppression") +endif() + +if(NOT spell_h MATCHES "BuildSpellMissLog" OR + NOT spell_h MATCHES "out.WriteBits\\(uint32\\(log.targetCount\\), 23\\)" OR + NOT spell_h MATCHES "log.targetCount >= \\(size_t\\(1\\) << 23\\)") + message(FATAL_ERROR "spell-miss serializer or its 23-bit count contract is absent") +endif() +if(NOT unit_cpp MATCHES "MopCombatLogPackets::BuildSpellMissLog" OR + NOT unit_cpp MATCHES "SMSG_SPELLLOGMISS") + message(FATAL_ERROR "spell-miss sender bypasses the 18414 serializer") +endif() +if(NOT opcodes_cpp MATCHES "DefS\\(SMSG_SPELLLOGMISS, \"SMSG_SPELLLOGMISS\"\\)") + message(FATAL_ERROR "spell-miss log lacks outbound opcode metadata") +endif() +if(NOT opcode_reference MATCHES "SMSG_SPELLLOGMISS[ ]+0x1570 ACTIVE") + message(FATAL_ERROR "reference inventory does not reflect the active spell-miss log") +endif() +if(NOT world_session MATCHES "case SMSG_SPELLLOGMISS:") + message(FATAL_ERROR "spell-miss log remains blocked by enter-world suppression") +endif() diff --git a/src/game/Server/tests/mop_combat_log_packets_test.cpp b/src/game/Server/tests/mop_combat_log_packets_test.cpp index 7719ab386..2c842d55b 100644 --- a/src/game/Server/tests/mop_combat_log_packets_test.cpp +++ b/src/game/Server/tests/mop_combat_log_packets_test.cpp @@ -77,6 +77,8 @@ class RefWriter U32(raw); } + void U8(uint8 value) { Align(); m_bytes.push_back(value); } + void GuidBit(uint64 guid, uint8 index) { Bit(GuidByteValue(guid, index) != 0); } void GuidByte(uint64 guid, uint8 index) @@ -90,7 +92,6 @@ class RefWriter private: static uint8 GuidByteValue(uint64 guid, uint8 index) { return uint8(guid >> (8 * index)); } - void U8(uint8 value) { Align(); m_bytes.push_back(value); } std::vector m_bytes; uint8 m_bit = 8; @@ -108,6 +109,177 @@ static void GuidBytes(RefWriter& writer, uint64 guid, uint8 const* order, size_t writer.GuidByte(guid, order[i]); } +static std::vector ExpectedInstakill(MopCombatLogPackets::SpellInstakillLog const& log) +{ + static uint8 const casterMaskA[] = { 6 }; + static uint8 const victimMaskA[] = { 0 }; + static uint8 const casterMaskB[] = { 7 }; + static uint8 const victimMaskB[] = { 2 }; + static uint8 const casterMaskC[] = { 3, 1, 2, 0, 4 }; + static uint8 const victimMaskC[] = { 4, 7, 1, 6, 5 }; + static uint8 const casterMaskD[] = { 5 }; + static uint8 const victimMaskD[] = { 3 }; + static uint8 const bytesA[][2] = { + { 0, 0 }, { 1, 1 }, { 3, 0 }, { 4, 0 }, { 5, 0 }, { 7, 0 }, + { 0, 1 }, { 6, 0 }, { 2, 1 }, { 4, 1 }, { 1, 0 } + }; + static uint8 const bytesB[][2] = { + { 3, 1 }, { 2, 0 }, { 7, 1 }, { 6, 1 }, { 5, 1 } + }; + + RefWriter writer; + GuidBits(writer, log.casterGuid, casterMaskA, 1); + GuidBits(writer, log.victimGuid, victimMaskA, 1); + GuidBits(writer, log.casterGuid, casterMaskB, 1); + GuidBits(writer, log.victimGuid, victimMaskB, 1); + GuidBits(writer, log.casterGuid, casterMaskC, 5); + GuidBits(writer, log.victimGuid, victimMaskC, 5); + GuidBits(writer, log.casterGuid, casterMaskD, 1); + GuidBits(writer, log.victimGuid, victimMaskD, 1); + writer.Align(); + for (auto const& byte : bytesA) + writer.GuidByte(byte[1] ? log.victimGuid : log.casterGuid, byte[0]); + writer.U32(log.spellId); + for (auto const& byte : bytesB) + writer.GuidByte(byte[1] ? log.victimGuid : log.casterGuid, byte[0]); + return writer.Bytes(); +} + +static std::vector ExpectedEnergize(MopCombatLogPackets::SpellEnergizeLog const& log) +{ + static uint8 const mask[][2] = { + { 7, 0 }, { 3, 0 }, { 1, 1 }, { 4, 0 }, { 2, 0 }, { 3, 1 }, { 5, 0 }, + { 7, 1 }, { 0, 1 }, { 2, 1 }, { 4, 1 }, { 6, 1 }, { 6, 0 }, { 1, 0 }, + { 0, 0 }, { 5, 1 } + }; + static uint8 const bytesA[][2] = { + { 0, 0 }, { 5, 1 }, { 6, 0 }, { 6, 1 }, { 2, 0 }, { 0, 1 }, { 1, 0 } + }; + static uint8 const bytesB[][2] = { + { 4, 0 }, { 1, 1 }, { 7, 1 }, { 5, 0 }, { 2, 1 }, { 3, 1 }, + { 7, 0 }, { 4, 1 }, { 3, 0 } + }; + + RefWriter writer; + for (size_t i = 0; i < 7; ++i) + writer.GuidBit(mask[i][1] ? log.casterGuid : log.targetGuid, mask[i][0]); + writer.Bit(false); // no optional spell-cast-log data + for (size_t i = 7; i < 16; ++i) + writer.GuidBit(mask[i][1] ? log.casterGuid : log.targetGuid, mask[i][0]); + writer.Align(); + for (auto const& byte : bytesA) + writer.GuidByte(byte[1] ? log.casterGuid : log.targetGuid, byte[0]); + writer.U32(log.amount); + for (auto const& byte : bytesB) + writer.GuidByte(byte[1] ? log.casterGuid : log.targetGuid, byte[0]); + writer.U32(log.spellId); + writer.U32(log.powerType); + return writer.Bytes(); +} + +static std::vector ExpectedHeal(MopCombatLogPackets::SpellHealLog const& log) +{ + static uint8 const mask[][2] = { + { 0, 1 }, { 2, 0 }, { 6, 0 }, { 2, 1 }, { 3, 0 }, { 0, 0 }, { 5, 0 }, + { 3, 1 }, { 7, 1 }, { 5, 1 }, { 7, 0 }, { 4, 1 }, { 4, 0 }, { 1, 0 }, + { 1, 1 }, { 6, 1 } + }; + static uint8 const bytes[][2] = { + { 2, 0 }, { 6, 1 }, { 5, 0 }, { 3, 0 }, { 7, 1 }, { 7, 0 }, + { 6, 0 }, { 1, 0 }, { 2, 1 }, { 4, 1 }, { 3, 1 }, { 0, 1 }, + { 5, 1 }, { 0, 0 }, { 1, 1 }, { 4, 0 } + }; + + RefWriter writer; + writer.U32(log.spellId); + writer.U32(log.absorb); + writer.U32(log.heal); + writer.U32(log.overheal); + for (size_t i = 0; i < 4; ++i) + writer.GuidBit(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]); + writer.Bit(log.critical); + for (size_t i = 4; i < 8; ++i) + writer.GuidBit(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]); + writer.Bit(false); // no optional spell-cast-log data + for (size_t i = 8; i < 12; ++i) + writer.GuidBit(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]); + writer.Bit(false); // no optional first float + writer.Bit(false); // no optional second float + for (size_t i = 12; i < 16; ++i) + writer.GuidBit(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]); + writer.Align(); + for (auto const& byte : bytes) + writer.GuidByte(byte[1] ? log.targetGuid : log.casterGuid, byte[0]); + return writer.Bytes(); +} + +static std::vector ExpectedDamageShield(MopCombatLogPackets::SpellDamageShieldLog const& log) +{ + static uint8 const mask[][2] = { + { 1, 1 }, { 2, 0 }, { 6, 0 }, { 3, 1 }, { 4, 0 }, { 2, 1 }, + { 5, 1 }, { 6, 1 }, { 3, 0 }, { 0, 1 }, { 5, 0 }, { 1, 0 }, + { 0, 0 }, { 7, 1 }, { 4, 1 }, { 7, 0 } + }; + + RefWriter writer; + for (size_t i = 0; i < 2; ++i) + writer.GuidBit(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]); + writer.Bit(false); // no optional spell-cast-log data + for (size_t i = 2; i < 16; ++i) + writer.GuidBit(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]); + writer.Align(); + writer.GuidByte(log.targetGuid, 2); + writer.GuidByte(log.casterGuid, 6); + writer.GuidByte(log.targetGuid, 6); + writer.GuidByte(log.targetGuid, 4); + writer.GuidByte(log.casterGuid, 3); + writer.GuidByte(log.targetGuid, 7); + writer.U32(log.resist); + writer.GuidByte(log.casterGuid, 4); + writer.GuidByte(log.targetGuid, 1); + writer.U32(log.damage); + writer.GuidByte(log.casterGuid, 7); + writer.U32(log.spellId); + writer.U32(log.overkill); + writer.GuidByte(log.targetGuid, 5); + writer.GuidByte(log.casterGuid, 5); + writer.GuidByte(log.targetGuid, 0); + writer.GuidByte(log.casterGuid, 1); + writer.GuidByte(log.casterGuid, 0); + writer.GuidByte(log.casterGuid, 2); + writer.U32(log.schoolMask); + writer.GuidByte(log.targetGuid, 3); + return writer.Bytes(); +} + +static std::vector ExpectedSpellMiss(MopCombatLogPackets::SpellMissLog const& log) +{ + static uint8 const casterMask[] = { 5, 1, 4, 0, 7, 3, 2, 6 }; + static uint8 const targetMask[] = { 0, 1, 6, 2, 5, 3, 4, 7 }; + static uint8 const targetBytes[] = { 7, 5, 0, 6, 3, 2, 1, 4 }; + static uint8 const casterBytesA[] = { 6, 4, 2, 0, 1 }; + static uint8 const casterBytesB[] = { 3, 7, 5 }; + + RefWriter writer; + GuidBits(writer, log.casterGuid, casterMask, 8); + writer.Bits(uint32(log.targetCount), 23); + for (size_t i = 0; i < log.targetCount; ++i) + { + GuidBits(writer, log.targets[i].guid, targetMask, 8); + writer.Bit(false); // no optional target position pair + } + writer.Align(); + for (size_t i = 0; i < log.targetCount; ++i) + { + writer.U8(log.targets[i].missReason); + GuidBytes(writer, log.targets[i].guid, targetBytes, 8); + } + GuidBytes(writer, log.casterGuid, casterBytesA, 5); + writer.U32(log.spellId); + GuidBytes(writer, log.casterGuid, casterBytesB, 3); + return writer.Bytes(); +} + static std::vector ExpectedExecute(MopCombatLogPackets::SpellExecuteLog const& log) { static uint8 const casterMaskPre[] = { 0, 6, 5, 7, 2 }; @@ -376,6 +548,144 @@ static void test_execute_variants() CHECK(invalid.size() == 1 && invalid.contents()[0] == 0xAA); } +static void test_spell_instakill_log() +{ + MopCombatLogPackets::SpellInstakillLog dense = {}; + dense.casterGuid = 0x0123456789ABCDEFull; + dense.victimGuid = 0xF1E2D3C4B5A69788ull; + dense.spellId = 0x11223344u; + + WorldPacket densePacket(SMSG_SPELLINSTAKILLLOG, 24); + MopCombatLogPackets::BuildSpellInstakillLog(densePacket, dense); + CHECK(densePacket.GetOpcode() == SMSG_SPELLINSTAKILLLOG); + CHECK(Equal(densePacket, ExpectedInstakill(dense))); + + MopCombatLogPackets::SpellInstakillLog sparse = {}; + sparse.casterGuid = 0x0002000400060008ull; + sparse.victimGuid = 0x0100030005000700ull; + sparse.spellId = 5; + + WorldPacket sparsePacket(SMSG_SPELLINSTAKILLLOG, 24); + MopCombatLogPackets::BuildSpellInstakillLog(sparsePacket, sparse); + CHECK(Equal(sparsePacket, ExpectedInstakill(sparse))); +} + +static void test_spell_energize_log() +{ + MopCombatLogPackets::SpellEnergizeLog dense = {}; + dense.targetGuid = 0x0123456789ABCDEFull; + dense.casterGuid = 0xF1E2D3C4B5A69788ull; + dense.amount = 0x11223344u; + dense.spellId = 0x55667788u; + dense.powerType = 3; + + WorldPacket densePacket(SMSG_SPELLENERGIZELOG, 32); + MopCombatLogPackets::BuildSpellEnergizeLog(densePacket, dense); + CHECK(densePacket.GetOpcode() == SMSG_SPELLENERGIZELOG); + CHECK(Equal(densePacket, ExpectedEnergize(dense))); + + MopCombatLogPackets::SpellEnergizeLog sparse = {}; + sparse.targetGuid = 0x0002000400060008ull; + sparse.casterGuid = 0x0100030005000700ull; + sparse.amount = 1; + sparse.spellId = 2; + sparse.powerType = 0; + + WorldPacket sparsePacket(SMSG_SPELLENERGIZELOG, 32); + MopCombatLogPackets::BuildSpellEnergizeLog(sparsePacket, sparse); + CHECK(Equal(sparsePacket, ExpectedEnergize(sparse))); +} + +static void test_spell_heal_log() +{ + MopCombatLogPackets::SpellHealLog dense = {}; + dense.casterGuid = 0x0123456789ABCDEFull; + dense.targetGuid = 0xF1E2D3C4B5A69788ull; + dense.spellId = 0x11223344u; + dense.heal = 0x55667788u; + dense.overheal = 0x01020304u; + dense.absorb = 0xA1A2A3A4u; + dense.critical = true; + + WorldPacket densePacket(SMSG_SPELLHEALLOG, 40); + MopCombatLogPackets::BuildSpellHealLog(densePacket, dense); + CHECK(densePacket.GetOpcode() == SMSG_SPELLHEALLOG); + CHECK(Equal(densePacket, ExpectedHeal(dense))); + + MopCombatLogPackets::SpellHealLog sparse = {}; + sparse.casterGuid = 0x0002000400060008ull; + sparse.targetGuid = 0x0100030005000700ull; + sparse.spellId = 1; + sparse.heal = 2; + + WorldPacket sparsePacket(SMSG_SPELLHEALLOG, 40); + MopCombatLogPackets::BuildSpellHealLog(sparsePacket, sparse); + CHECK(Equal(sparsePacket, ExpectedHeal(sparse))); +} + +static void test_spell_damage_shield_log() +{ + MopCombatLogPackets::SpellDamageShieldLog dense = {}; + dense.casterGuid = 0x0123456789ABCDEFull; + dense.targetGuid = 0xF1E2D3C4B5A69788ull; + dense.spellId = 0x11223344u; + dense.damage = 0x55667788u; + dense.overkill = 0x01020304u; + dense.schoolMask = 0xA1A2A3A4u; + dense.resist = 0x05060708u; + + WorldPacket densePacket(SMSG_SPELLDAMAGESHIELD, 40); + MopCombatLogPackets::BuildSpellDamageShieldLog(densePacket, dense); + CHECK(densePacket.GetOpcode() == SMSG_SPELLDAMAGESHIELD); + CHECK(Equal(densePacket, ExpectedDamageShield(dense))); + + MopCombatLogPackets::SpellDamageShieldLog sparse = {}; + sparse.casterGuid = 0x0002000400060008ull; + sparse.targetGuid = 0x0100030005000700ull; + sparse.spellId = 1; + sparse.damage = 2; + sparse.schoolMask = 4; + + WorldPacket sparsePacket(SMSG_SPELLDAMAGESHIELD, 40); + MopCombatLogPackets::BuildSpellDamageShieldLog(sparsePacket, sparse); + CHECK(Equal(sparsePacket, ExpectedDamageShield(sparse))); +} + +static void test_spell_miss_log() +{ + MopCombatLogPackets::SpellMissTarget targets[] = { + { 0xF1E2D3C4B5A69788ull, 2 }, + { 0x0100030005000700ull, 7 } + }; + MopCombatLogPackets::SpellMissLog dense = {}; + dense.casterGuid = 0x0123456789ABCDEFull; + dense.spellId = 0x11223344u; + dense.targets = targets; + dense.targetCount = 2; + + WorldPacket densePacket(SMSG_SPELLLOGMISS, 48); + CHECK(MopCombatLogPackets::BuildSpellMissLog(densePacket, dense)); + CHECK(densePacket.GetOpcode() == SMSG_SPELLLOGMISS); + CHECK(Equal(densePacket, ExpectedSpellMiss(dense))); + + MopCombatLogPackets::SpellMissLog empty = {}; + empty.casterGuid = 0x0002000400060008ull; + empty.spellId = 1; + WorldPacket emptyPacket(SMSG_SPELLLOGMISS, 20); + CHECK(MopCombatLogPackets::BuildSpellMissLog(emptyPacket, empty)); + CHECK(Equal(emptyPacket, ExpectedSpellMiss(empty))); + + WorldPacket invalid(SMSG_SPELLLOGMISS, 1); + invalid << uint8(0xAA); + dense.targetCount = size_t(1) << 23; + CHECK(!MopCombatLogPackets::BuildSpellMissLog(invalid, dense)); + CHECK(invalid.size() == 1 && invalid.contents()[0] == 0xAA); + dense.targetCount = 1; + dense.targets = nullptr; + CHECK(!MopCombatLogPackets::BuildSpellMissLog(invalid, dense)); + CHECK(invalid.size() == 1 && invalid.contents()[0] == 0xAA); +} + static void CheckPeriodic(MopCombatLogPackets::PeriodicAuraLog const& log) { WorldPacket packet(SMSG_SPELL_PERIODIC_AURA_LOG, 64); @@ -481,6 +791,11 @@ static void test_spell_interrupt_log() static void test_successor_opcodes_are_framable() { + CHECK(uint32(SMSG_SPELLINSTAKILLLOG) == 0x09F8u); + CHECK(uint32(SMSG_SPELLENERGIZELOG) == 0x0D79u); + CHECK(uint32(SMSG_SPELLHEALLOG) == 0x09FBu); + CHECK(uint32(SMSG_SPELLDAMAGESHIELD) == 0x05F3u); + CHECK(uint32(SMSG_SPELLLOGMISS) == 0x1570u); CHECK(uint32(SMSG_SPELL_EXECUTE_LOG) == 0x00D8u); CHECK(uint32(SMSG_SPELL_PERIODIC_AURA_LOG) == 0x0CF2u); CHECK(uint32(SMSG_SPELLDISPELLOG) == 0x0DF9u); @@ -493,6 +808,31 @@ static void test_successor_opcodes_are_framable() CHECK(executePacket.size() == 29); uint8 header[4] = {}; + MopCombatLogPackets::SpellInstakillLog instakill = {}; + WorldPacket instakillPacket(SMSG_SPELLINSTAKILLLOG, 6); + MopCombatLogPackets::BuildSpellInstakillLog(instakillPacket, instakill); + CHECK(MopWire::BuildServerHeader(true, instakillPacket.size(), instakillPacket.GetOpcode(), header)); + + MopCombatLogPackets::SpellEnergizeLog energize = {}; + WorldPacket energizePacket(SMSG_SPELLENERGIZELOG, 15); + MopCombatLogPackets::BuildSpellEnergizeLog(energizePacket, energize); + CHECK(MopWire::BuildServerHeader(true, energizePacket.size(), energizePacket.GetOpcode(), header)); + + MopCombatLogPackets::SpellHealLog heal = {}; + WorldPacket healPacket(SMSG_SPELLHEALLOG, 19); + MopCombatLogPackets::BuildSpellHealLog(healPacket, heal); + CHECK(MopWire::BuildServerHeader(true, healPacket.size(), healPacket.GetOpcode(), header)); + + MopCombatLogPackets::SpellDamageShieldLog damageShield = {}; + WorldPacket damageShieldPacket(SMSG_SPELLDAMAGESHIELD, 23); + MopCombatLogPackets::BuildSpellDamageShieldLog(damageShieldPacket, damageShield); + CHECK(MopWire::BuildServerHeader(true, damageShieldPacket.size(), damageShieldPacket.GetOpcode(), header)); + + MopCombatLogPackets::SpellMissLog spellMiss = {}; + WorldPacket spellMissPacket(SMSG_SPELLLOGMISS, 11); + CHECK(MopCombatLogPackets::BuildSpellMissLog(spellMissPacket, spellMiss)); + CHECK(MopWire::BuildServerHeader(true, spellMissPacket.size(), spellMissPacket.GetOpcode(), header)); + CHECK(MopWire::BuildServerHeader(true, executePacket.size(), executePacket.GetOpcode(), header)); CHECK(header[0] == 0xD8 && header[1] == 0xA0 && header[2] == 0x03 && header[3] == 0x00); @@ -522,6 +862,11 @@ static void test_successor_opcodes_are_framable() int main(int, char**) { + test_spell_instakill_log(); + test_spell_energize_log(); + test_spell_heal_log(); + test_spell_damage_shield_log(); + test_spell_miss_log(); test_execute_variants(); test_periodic_variants(); test_dispel_and_steal_variants(); diff --git a/src/game/Server/tests/mop_compact_packets_source_test.cmake b/src/game/Server/tests/mop_compact_packets_source_test.cmake index 1ec81fbf4..ec5f45899 100644 --- a/src/game/Server/tests/mop_compact_packets_source_test.cmake +++ b/src/game/Server/tests/mop_compact_packets_source_test.cmake @@ -1,4 +1,12 @@ file(READ "${SOURCE_ROOT}/src/game/Object/PlayerCombat.cpp" player_combat) +file(READ "${SOURCE_ROOT}/src/game/Object/PlayerDuel.cpp" player_duel) +file(READ "${SOURCE_ROOT}/src/game/Object/PlayerMirror.cpp" player_mirror) +file(READ "${SOURCE_ROOT}/src/game/Object/PlayerCombo.cpp" player_combo) +file(READ "${SOURCE_ROOT}/src/game/Object/PlayerDeath.cpp" player_death) +file(READ "${SOURCE_ROOT}/src/game/Object/RuneMgr.cpp" rune_source) +file(READ "${SOURCE_ROOT}/src/game/Object/RuneMgr.h" rune_header) +file(READ "${SOURCE_ROOT}/src/game/Object/Player.h" player_header) +file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/SpellEffectObjectCombat.cpp" spell_effect_object_combat) file(READ "${SOURCE_ROOT}/src/game/Object/UnitSpeed.cpp" unit_speed) file(READ "${SOURCE_ROOT}/src/game/ChatCommands/PlayerStatsMods.cpp" player_stats_mods) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/GroupHandler.cpp" group_handler) @@ -10,6 +18,7 @@ file(READ "${SOURCE_ROOT}/src/game/Server/Opcodes.h" opcode_header) file(READ "${SOURCE_ROOT}/src/game/Object/UnitCombat.cpp" unit_combat) file(READ "${SOURCE_ROOT}/src/game/Object/Unit.cpp" unit) file(READ "${SOURCE_ROOT}/src/game/Object/Unit.h" unit_header) +file(READ "${SOURCE_ROOT}/src/game/Object/UnitThreat.cpp" unit_threat) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/CombatHandler.cpp" combat_handler) file(READ "${SOURCE_ROOT}/src/game/Server/WorldSession.cpp" world_session) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/ItemHandler.cpp" item_handler) @@ -114,6 +123,311 @@ elseif(MUTATION STREQUAL "attacker_envelope") "out.WriteBit(false);" "out.WriteBit(true);" unit_header "${unit_header}") +elseif(MUTATION STREQUAL "party_kill_mask") + string(REPLACE + "out.WriteGuidMask<7, 2>(victim);" + "out.WriteGuidMask<2, 7>(victim);" + unit_header "${unit_header}") +elseif(MUTATION STREQUAL "party_kill_bytes") + string(REPLACE + "out.WriteGuidBytes<0, 5>(victim);" + "out.WriteGuidBytes<5, 0>(victim);" + unit_header "${unit_header}") +elseif(MUTATION STREQUAL "party_kill_sender") + string(REPLACE + "MopCompactPackets::BuildPartyKillLog(data," + "/* removed party-kill sender */ (data," + unit "${unit}") +elseif(MUTATION STREQUAL "party_kill_registration") + string(REPLACE + "DefS(SMSG_PARTYKILLLOG, \"SMSG_PARTYKILLLOG\");" + "/* removed party-kill registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "party_kill_allowlist") + string(REPLACE + "case SMSG_PARTYKILLLOG:" + "case 0xFFFF: /* removed party-kill allowlist */" + world_session "${world_session}") +elseif(MUTATION STREQUAL "party_kill_reference") + string(REPLACE + "SMSG_PARTYKILLLOG 0x048A ACTIVE" + "SMSG_PARTYKILLLOG 0x048A DORMANT" + opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "duel_complete_bit") + string(REPLACE + "out.WriteBit(completed);" + "out << uint8(completed);" + player_header "${player_header}") +elseif(MUTATION STREQUAL "duel_countdown_width") + string(REPLACE + "out << milliseconds;" + "out << uint16(milliseconds);" + player_header "${player_header}") +elseif(MUTATION STREQUAL "duel_sender") + string(REPLACE + "MopDuelPackets::BuildComplete(data, type != DUEL_INTERRUPTED);" + "data.Initialize(SMSG_DUEL_COMPLETE, 1);" + player_duel "${player_duel}") +elseif(MUTATION STREQUAL "duel_registration") + string(REPLACE + "DefS(SMSG_DUEL_COMPLETE, \"SMSG_DUEL_COMPLETE\");" + "/* removed duel-complete registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "duel_allowlist") + string(REPLACE + "case SMSG_DUEL_COMPLETE:" + "case 0xFFFF: /* removed duel-complete allowlist */" + world_session "${world_session}") +elseif(MUTATION STREQUAL "duel_reference") + string(REPLACE + "SMSG_DUEL_COMPLETE 0x1C0A ACTIVE" + "SMSG_DUEL_COMPLETE 0x1C0A DORMANT" + opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "duel_request_mask") + string(REPLACE + "out.WriteGuidMask<4, 2, 7>(initiator);" + "out.WriteGuidMask<2, 4, 7>(initiator);" + player_header "${player_header}") +elseif(MUTATION STREQUAL "duel_request_bytes") + string(REPLACE + "out.WriteGuidBytes<5, 3>(arbiter);" + "out.WriteGuidBytes<3, 5>(arbiter);" + player_header "${player_header}") +elseif(MUTATION STREQUAL "duel_winner_bits") + string(REPLACE + "out.WriteBits(uint32(winnerName.size()), 6);" + "out.WriteBits(uint32(winnerName.size()), 5);" + player_header "${player_header}") +elseif(MUTATION STREQUAL "duel_winner_realm_order") + string(REPLACE + "out << loserRealmAddress;" + "out << winnerRealmAddress;" + player_header "${player_header}") +elseif(MUTATION STREQUAL "duel_request_sender") + string(REPLACE + "MopDuelPackets::BuildRequested(" + "/* removed duel-request sender */ (" + spell_effect_object_combat "${spell_effect_object_combat}") +elseif(MUTATION STREQUAL "duel_winner_sender") + string(REPLACE + "MopDuelPackets::BuildWinner(data, type != DUEL_WON," + "/* removed duel-winner sender */ (data, type != DUEL_WON," + player_duel "${player_duel}") +elseif(MUTATION STREQUAL "duel_pair_registration") + string(REPLACE + "DefS(SMSG_DUEL_WINNER, \"SMSG_DUEL_WINNER\");" + "/* removed duel-winner registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "duel_pair_allowlist") + string(REPLACE + "case SMSG_DUEL_REQUESTED:" + "case 0xFFFF: /* removed duel-request allowlist */" + world_session "${world_session}") +elseif(MUTATION STREQUAL "duel_pair_reference") + string(REPLACE + "SMSG_DUEL_WINNER 0x10E1 ACTIVE" + "SMSG_DUEL_WINNER 0x10E1 DORMANT" + opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "mirror_start_order") + string(REPLACE + "out << maxValue << spellId << currentValue << uint32(regeneration) << type;" + "out << type << maxValue << currentValue << uint32(regeneration) << spellId;" + player_header "${player_header}") +elseif(MUTATION STREQUAL "mirror_pause_width") + string(REPLACE + "out.WriteBit(paused);" + "out << uint8(paused);" + player_header "${player_header}") +elseif(MUTATION STREQUAL "mirror_sender") + string(REPLACE + "MopMirrorTimerPackets::BuildStart(" + "/* removed mirror-timer builder */ (" + player_mirror "${player_mirror}") +elseif(MUTATION STREQUAL "mirror_registration") + string(REPLACE + "DefS(SMSG_START_MIRROR_TIMER, \"SMSG_START_MIRROR_TIMER\");" + "/* removed mirror-timer registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "mirror_allowlist") + string(REPLACE + "case SMSG_START_MIRROR_TIMER:" + "case 0xFFFF: /* removed mirror-timer allowlist */" + world_session "${world_session}") +elseif(MUTATION STREQUAL "mirror_reference") + string(REPLACE + "SMSG_START_MIRROR_TIMER 0x0E12 ACTIVE" + "SMSG_START_MIRROR_TIMER 0x0E12 DORMANT" + opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "rune_count_width") + string(REPLACE + "out.WriteBits(uint32(runes.size()), 23);" + "out.WriteBits(uint32(runes.size()), 22);" + rune_header "${rune_header}") +elseif(MUTATION STREQUAL "rune_record_order") + string(REPLACE + "out << rune.cooldownFraction << uint8(rune.type);" + "out << uint8(rune.type) << rune.cooldownFraction;" + rune_header "${rune_header}") +elseif(MUTATION STREQUAL "rune_convert_order") + string(REPLACE + "out << uint8(newType) << index;" + "out << index << uint8(newType);" + rune_header "${rune_header}") +elseif(MUTATION STREQUAL "rune_sender") + string(REPLACE + "MopRunePackets::BuildResync(data, runes);" + "/* removed rune-resync builder */" + rune_source "${rune_source}") +elseif(MUTATION STREQUAL "rune_registration") + string(REPLACE + "DefS(SMSG_RESYNC_RUNES, \"SMSG_RESYNC_RUNES\");" + "/* removed rune registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "rune_allowlist") + string(REPLACE + "case SMSG_RESYNC_RUNES:" + "case 0xFFFF: /* removed rune allowlist */" + world_session "${world_session}") +elseif(MUTATION STREQUAL "rune_reference") + string(REPLACE + "SMSG_RESYNC_RUNES 0x15E3 ACTIVE" + "SMSG_RESYNC_RUNES 0x15E3 DORMANT" + opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "threat_count_width") + string(REPLACE + "out.WriteBits(uint32(entries.size()), 21);" + "out.WriteBits(uint32(entries.size()), 20);" + unit_header "${unit_header}") +elseif(MUTATION STREQUAL "threat_update_mask") + string(REPLACE + "out.WriteGuidMask<5, 6, 1, 3, 7, 0, 4>(owner);" + "out.WriteGuidMask<6, 5, 1, 3, 7, 0, 4>(owner);" + unit_header "${unit_header}") +elseif(MUTATION STREQUAL "threat_highest_mask") + string(REPLACE + "out.WriteGuidMask<3, 0>(selected);" + "out.WriteGuidMask<0, 3>(selected);" + unit_header "${unit_header}") +elseif(MUTATION STREQUAL "threat_clear_bytes") + string(REPLACE + "out.WriteGuidBytes<7, 0, 4, 3, 2, 1, 6, 5>(owner);" + "out.WriteGuidBytes<0, 7, 4, 3, 2, 1, 6, 5>(owner);" + unit_header "${unit_header}") +elseif(MUTATION STREQUAL "threat_remove_mask") + string(REPLACE + "out.WriteGuidMask<0, 1, 5>(owner);" + "out.WriteGuidMask<1, 0, 5>(owner);" + unit_header "${unit_header}") +elseif(MUTATION STREQUAL "threat_sender") + string(REPLACE + "MopThreatPackets::BuildUpdate(data, GetObjectGuid(), entries);" + "/* removed threat-update builder */" + unit_threat "${unit_threat}") +elseif(MUTATION STREQUAL "threat_registration") + string(REPLACE + "DefS(SMSG_THREAT_UPDATE, \"SMSG_THREAT_UPDATE\");" + "/* removed threat-update registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "threat_allowlist") + string(REPLACE + "case SMSG_THREAT_UPDATE:" + "case 0xFFFF: /* removed threat-update allowlist */" + world_session "${world_session}") +elseif(MUTATION STREQUAL "threat_reference") + string(REPLACE + "SMSG_THREAT_UPDATE 0x0632 ACTIVE" + "SMSG_THREAT_UPDATE 0x0632 DORMANT" + opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "dismount_mask") + string(REPLACE + "out.WriteGuidMask<6, 3, 0, 7, 1, 2, 5, 4>(guid);" + "out.WriteGuidMask<3, 6, 0, 7, 1, 2, 5, 4>(guid);" + unit_header "${unit_header}") +elseif(MUTATION STREQUAL "dismount_bytes") + string(REPLACE + "out.WriteGuidBytes<3, 6, 7, 5, 1, 4, 2, 0>(guid);" + "out.WriteGuidBytes<6, 3, 7, 5, 1, 4, 2, 0>(guid);" + unit_header "${unit_header}") +elseif(MUTATION STREQUAL "dismount_sender") + string(REPLACE + "MopCompactPackets::BuildDismount(data, GetObjectGuid());" + "/* removed dismount builder */" + unit "${unit}") +elseif(MUTATION STREQUAL "dismount_registration") + string(REPLACE + "DefS(SMSG_DISMOUNT, \"SMSG_DISMOUNT\");" + "/* removed dismount registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "dismount_allowlist") + string(REPLACE + "case SMSG_DISMOUNT:" + "case 0xFFFF: /* removed dismount allowlist */" + world_session "${world_session}") +elseif(MUTATION STREQUAL "dismount_reference") + string(REPLACE + "SMSG_DISMOUNT 0x0E3A ACTIVE" + "SMSG_DISMOUNT 0x0E3A DORMANT" + opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "combo_mask") + string(REPLACE + "out.WriteGuidMask<0, 5, 6, 3, 7, 4, 1, 2>(target);" + "out.WriteGuidMask<5, 0, 6, 3, 7, 4, 1, 2>(target);" + player_header "${player_header}") +elseif(MUTATION STREQUAL "combo_bytes") + string(REPLACE + "out.WriteGuidBytes<5, 6, 4, 7, 3, 0>(target);" + "out.WriteGuidBytes<6, 5, 4, 7, 3, 0>(target);" + player_header "${player_header}") +elseif(MUTATION STREQUAL "combo_sender") + string(REPLACE + "MopComboPointPackets::BuildUpdate(data, combotarget->GetObjectGuid(), uint8(m_comboPoints));" + "/* removed combo-point builder */" + player_combo "${player_combo}") +elseif(MUTATION STREQUAL "combo_registration") + string(REPLACE + "DefS(SMSG_UPDATE_COMBO_POINTS, \"SMSG_UPDATE_COMBO_POINTS\");" + "/* removed combo-point registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "combo_allowlist") + string(REPLACE + "case SMSG_UPDATE_COMBO_POINTS:" + "case 0xFFFF: /* removed combo-point allowlist */" + world_session "${world_session}") +elseif(MUTATION STREQUAL "combo_reference") + string(REPLACE + "SMSG_UPDATE_COMBO_POINTS 0x082F ACTIVE" + "SMSG_UPDATE_COMBO_POINTS 0x082F DORMANT" + opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "pre_resurrect_mask") + string(REPLACE + "out.WriteGuidMask<1, 7, 5, 2, 6, 0, 3, 4>(guid);" + "out.WriteGuidMask<7, 1, 5, 2, 6, 0, 3, 4>(guid);" + unit_header "${unit_header}") +elseif(MUTATION STREQUAL "pre_resurrect_bytes") + string(REPLACE + "out.WriteGuidBytes<5, 1, 7, 0, 6, 4, 2, 3>(guid);" + "out.WriteGuidBytes<1, 5, 7, 0, 6, 4, 2, 3>(guid);" + unit_header "${unit_header}") +elseif(MUTATION STREQUAL "pre_resurrect_sender") + string(REPLACE + "MopCompactPackets::BuildPreResurrect(data, GetObjectGuid());" + "/* removed pre-resurrect builder */" + player_death "${player_death}") +elseif(MUTATION STREQUAL "pre_resurrect_registration") + string(REPLACE + "DefS(SMSG_PRE_RESURRECT, \"SMSG_PRE_RESURRECT\");" + "/* removed pre-resurrect registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "pre_resurrect_allowlist") + string(REPLACE + "case SMSG_PRE_RESURRECT:" + "case 0xFFFF: /* removed pre-resurrect allowlist */" + world_session "${world_session}") +elseif(MUTATION STREQUAL "pre_resurrect_reference") + string(REPLACE + "SMSG_PRE_RESURRECT 0x19C0 ACTIVE" + "SMSG_PRE_RESURRECT 0x19C0 DORMANT" + opcode_reference "${opcode_reference}") endif() if(player_combat MATCHES "WorldPacket[ \t]+data\\(SMSG_ATTACKSWING_NOTINRANGE") @@ -256,3 +570,292 @@ endif() if(NOT unit_header MATCHES "out.WriteBit\\(false\\)") message(FATAL_ERROR "attacker-state outer envelope does not omit optional metadata") endif() + +if(NOT unit_header MATCHES "WriteGuidMask<7, 2>\\(victim\\)") + message(FATAL_ERROR "party-kill GUID mask order does not match reader sub_6F2FE4") +endif() +if(NOT unit_header MATCHES "WriteGuidBytes<0, 5>\\(victim\\)") + message(FATAL_ERROR "party-kill GUID byte order does not match reader sub_6F2FE4") +endif() +if(NOT unit MATCHES "MopCompactPackets::BuildPartyKillLog\\(data,") + message(FATAL_ERROR "party-kill sender bypasses the 18414 serializer") +endif() +if(unit MATCHES "WorldPacket[ \\t]+data\\(SMSG_PARTYKILLLOG") + message(FATAL_ERROR "legacy raw party-kill sender remains") +endif() +if(NOT opcode_registry MATCHES "DefS\\(SMSG_PARTYKILLLOG,[ \\t]*\"SMSG_PARTYKILLLOG\"\\)") + message(FATAL_ERROR "SMSG_PARTYKILLLOG is missing outbound opcode metadata") +endif() +if(NOT world_session MATCHES "case[ \\t]+SMSG_PARTYKILLLOG:") + message(FATAL_ERROR "SMSG_PARTYKILLLOG is missing from the converted-packet gate") +endif() +if(NOT opcode_reference MATCHES "SMSG_PARTYKILLLOG[ \\t]+0x048A[ \\t]+ACTIVE") + message(FATAL_ERROR "reference inventory does not record active party-kill log") +endif() + +if(NOT player_header MATCHES "namespace MopDuelPackets") + message(FATAL_ERROR "duel-state serializers are missing from owning player code") +endif() +if(NOT player_header MATCHES "out.WriteBit\\(completed\\)") + message(FATAL_ERROR "duel-complete body is not the one-bit 18414 layout") +endif() +if(NOT player_header MATCHES "out << milliseconds;") + message(FATAL_ERROR "duel-countdown body is not the one-uint32 18414 layout") +endif() +foreach(builder IN ITEMS BuildOutOfBounds BuildInBounds BuildComplete BuildCountdown) + if(NOT player_duel MATCHES "MopDuelPackets::${builder}") + message(FATAL_ERROR "duel sender bypasses ${builder}") + endif() +endforeach() +foreach(server_name IN ITEMS + SMSG_DUEL_OUTOFBOUNDS SMSG_DUEL_INBOUNDS SMSG_DUEL_COMPLETE SMSG_DUEL_COUNTDOWN) + if(NOT opcode_registry MATCHES "DefS\\(${server_name},[ \\t]*\"${server_name}\"\\)") + message(FATAL_ERROR "${server_name} is missing outbound opcode metadata") + endif() + if(NOT world_session MATCHES "case[ \\t]+${server_name}:") + message(FATAL_ERROR "${server_name} is missing from the converted-packet gate") + endif() + if(NOT opcode_reference MATCHES "${server_name}[ \\t]+0x[0-9A-F]+[ \\t]+ACTIVE") + message(FATAL_ERROR "reference inventory does not record active ${server_name}") + endif() +endforeach() +if(player_duel MATCHES "WorldPacket[ \\t]+data\\(SMSG_DUEL_(OUTOFBOUNDS|INBOUNDS|COMPLETE|COUNTDOWN)") + message(FATAL_ERROR "legacy raw duel-state packet construction remains") +endif() + +if(NOT player_header MATCHES "out.WriteGuidMask<4, 2, 7>\\(initiator\\)") + message(FATAL_ERROR "duel-request initiator mask order does not match reader sub_6CA34C") +endif() +if(NOT player_header MATCHES "out.WriteGuidBytes<5, 3>\\(arbiter\\)") + message(FATAL_ERROR "duel-request arbiter byte order does not match reader sub_6CA34C") +endif() +if(NOT player_header MATCHES "out.WriteBits\\(uint32\\(winnerName.size\\(\\)\\), 6\\)") + message(FATAL_ERROR "duel-winner name length is not six bits") +endif() +if(NOT player_header MATCHES "out << loserRealmAddress;") + message(FATAL_ERROR "duel-winner crossed realm/name order does not match reader sub_6CFDCC") +endif() +if(NOT spell_effect_object_combat MATCHES "MopDuelPackets::BuildRequested") + message(FATAL_ERROR "duel-request sender bypasses the 18414 serializer") +endif() +if(NOT player_duel MATCHES "MopDuelPackets::BuildWinner") + message(FATAL_ERROR "duel-winner sender bypasses the 18414 serializer") +endif() +foreach(server_name IN ITEMS SMSG_DUEL_REQUESTED SMSG_DUEL_WINNER) + if(NOT opcode_registry MATCHES "DefS\\(${server_name},[ \\t]*\"${server_name}\"\\)") + message(FATAL_ERROR "${server_name} is missing outbound opcode metadata") + endif() + if(NOT world_session MATCHES "case[ \\t]+${server_name}:") + message(FATAL_ERROR "${server_name} is missing from the converted-packet gate") + endif() + if(NOT opcode_reference MATCHES "${server_name}[ \\t]+0x[0-9A-F]+[ \\t]+ACTIVE") + message(FATAL_ERROR "reference inventory does not record active ${server_name}") + endif() +endforeach() +if(spell_effect_object_combat MATCHES "WorldPacket[ \\t]+data\\(SMSG_DUEL_REQUESTED") + message(FATAL_ERROR "legacy raw duel-request construction remains") +endif() +if(player_duel MATCHES "Initialize\\(SMSG_DUEL_WINNER") + message(FATAL_ERROR "legacy guessed duel-winner construction remains") +endif() + +if(NOT player_header MATCHES "namespace MopMirrorTimerPackets") + message(FATAL_ERROR "mirror-timer serializers are missing from owning player code") +endif() +if(NOT player_header MATCHES + "out << maxValue << spellId << currentValue << uint32\\(regeneration\\) << type;") + message(FATAL_ERROR "start mirror-timer field order does not match reader sub_6F16F9") +endif() +if(NOT player_header MATCHES "out.WriteBit\\(paused\\)") + message(FATAL_ERROR "start mirror-timer pause flag is not one bit") +endif() +foreach(builder IN ITEMS BuildStart BuildStop) + if(NOT player_mirror MATCHES "MopMirrorTimerPackets::${builder}") + message(FATAL_ERROR "mirror-timer sender bypasses ${builder}") + endif() +endforeach() +if(player_mirror MATCHES "WorldPacket[ \\t]+data\\(SMSG_(START|STOP)_MIRROR_TIMER") + message(FATAL_ERROR "legacy raw mirror-timer packet construction remains") +endif() +foreach(server_name IN ITEMS SMSG_START_MIRROR_TIMER SMSG_STOP_MIRROR_TIMER) + if(NOT opcode_registry MATCHES "DefS\\(${server_name},[ \\t]*\"${server_name}\"\\)") + message(FATAL_ERROR "${server_name} is missing outbound opcode metadata") + endif() + if(NOT world_session MATCHES "case[ \\t]+${server_name}:") + message(FATAL_ERROR "${server_name} is missing from the converted-packet gate") + endif() + if(NOT opcode_reference MATCHES "${server_name}[ \\t]+0x[0-9A-F]+[ \\t]+ACTIVE") + message(FATAL_ERROR "reference inventory does not record active ${server_name}") + endif() +endforeach() + +if(NOT rune_header MATCHES "out.WriteBits\\(uint32\\(runes.size\\(\\)\\), 23\\)") + message(FATAL_ERROR "rune-resync count is not the 23-bit 18414 layout") +endif() +if(NOT rune_header MATCHES "out << rune.cooldownFraction << uint8\\(rune.type\\);") + message(FATAL_ERROR "rune-resync record order does not match reader sub_73299D") +endif() +if(NOT rune_header MATCHES "out << uint8\\(newType\\) << index;") + message(FATAL_ERROR "rune-convert order does not match reader sub_6B9A69") +endif() +foreach(builder IN ITEMS BuildResync BuildAddPower BuildConvert) + if(NOT rune_source MATCHES "MopRunePackets::${builder}") + message(FATAL_ERROR "rune sender bypasses ${builder}") + endif() +endforeach() +if(rune_source MATCHES "WorldPacket[ \\t]+data\\(SMSG_(RESYNC_RUNES|ADD_RUNE_POWER|CONVERT_RUNE)") + message(FATAL_ERROR "legacy raw rune packet construction remains") +endif() +foreach(server_name IN ITEMS SMSG_RESYNC_RUNES SMSG_ADD_RUNE_POWER SMSG_CONVERT_RUNE) + if(NOT opcode_registry MATCHES "DefS\\(${server_name},[ \\t]*\"${server_name}\"\\)") + message(FATAL_ERROR "${server_name} is missing outbound opcode metadata") + endif() + if(NOT world_session MATCHES "case[ \\t]+${server_name}:") + message(FATAL_ERROR "${server_name} is missing from the converted-packet gate") + endif() + if(NOT opcode_reference MATCHES "${server_name}[ \\t]+0x[0-9A-F]+[ \\t]+ACTIVE") + message(FATAL_ERROR "reference inventory does not record active ${server_name}") + endif() +endforeach() + +if(NOT unit_header MATCHES "namespace MopThreatPackets") + message(FATAL_ERROR "threat serializers are missing from owning unit code") +endif() +string(FIND "${unit_header}" "out.WriteBits(uint32(entries.size()), 21);" threat_count_layout) +if(threat_count_layout EQUAL -1) + message(FATAL_ERROR "threat-list count is not the 21-bit 18414 layout") +endif() +string(FIND "${unit_header}" "out.WriteGuidMask<5, 6, 1, 3, 7, 0, 4>(owner);" threat_update_layout) +if(threat_update_layout EQUAL -1) + message(FATAL_ERROR "threat-update owner mask does not match reader sub_7344A4") +endif() +string(FIND "${unit_header}" "out.WriteGuidMask<3, 0>(selected);" threat_highest_layout) +if(threat_highest_layout EQUAL -1) + message(FATAL_ERROR "highest-threat selected mask does not match reader sub_736527") +endif() +string(FIND "${unit_header}" "out.WriteGuidBytes<7, 0, 4, 3, 2, 1, 6, 5>(owner);" threat_clear_layout) +if(threat_clear_layout EQUAL -1) + message(FATAL_ERROR "threat-clear byte order does not match reader sub_6F2392") +endif() +string(FIND "${unit_header}" "out.WriteGuidMask<0, 1, 5>(owner);" threat_remove_layout) +if(threat_remove_layout EQUAL -1) + message(FATAL_ERROR "threat-remove owner mask does not match reader sub_6DBFD5") +endif() +foreach(builder IN ITEMS BuildUpdate BuildHighest BuildClear BuildRemove) + if(NOT unit_threat MATCHES "MopThreatPackets::${builder}") + message(FATAL_ERROR "threat sender bypasses ${builder}") + endif() +endforeach() +foreach(server_name IN ITEMS + SMSG_THREAT_UPDATE SMSG_HIGHEST_THREAT_UPDATE SMSG_THREAT_CLEAR SMSG_THREAT_REMOVE) + string(FIND "${unit_threat}" "WorldPacket data(${server_name}" legacy_threat_sender) + if(NOT legacy_threat_sender EQUAL -1) + message(FATAL_ERROR "legacy raw ${server_name} construction remains") + endif() + string(FIND "${opcode_registry}" "DefS(${server_name}, \"${server_name}\");" threat_registration) + if(threat_registration EQUAL -1) + message(FATAL_ERROR "${server_name} is missing outbound opcode metadata") + endif() + string(FIND "${world_session}" "case ${server_name}:" threat_allowlist) + if(threat_allowlist EQUAL -1) + message(FATAL_ERROR "${server_name} is missing from the converted-packet gate") + endif() + string(REGEX MATCH "${server_name}[ \t]+0x[0-9A-F]+[ \t]+ACTIVE" threat_reference "${opcode_reference}") + if(threat_reference STREQUAL "") + message(FATAL_ERROR "reference inventory does not record active ${server_name}") + endif() +endforeach() + +string(FIND "${unit_header}" "out.WriteGuidMask<6, 3, 0, 7, 1, 2, 5, 4>(guid);" dismount_mask) +if(dismount_mask EQUAL -1) + message(FATAL_ERROR "dismount GUID mask does not match reader sub_6D3AD4") +endif() +string(FIND "${unit_header}" "out.WriteGuidBytes<3, 6, 7, 5, 1, 4, 2, 0>(guid);" dismount_bytes) +if(dismount_bytes EQUAL -1) + message(FATAL_ERROR "dismount GUID byte order does not match reader sub_6D3AD4") +endif() +string(FIND "${unit}" "MopCompactPackets::BuildDismount(data, GetObjectGuid());" dismount_sender) +if(dismount_sender EQUAL -1) + message(FATAL_ERROR "dismount sender bypasses the 18414 serializer") +endif() +string(FIND "${unit}" "WorldPacket data(SMSG_DISMOUNT" legacy_dismount_sender) +if(NOT legacy_dismount_sender EQUAL -1) + message(FATAL_ERROR "legacy raw dismount packet construction remains") +endif() +string(FIND "${opcode_registry}" "DefS(SMSG_DISMOUNT, \"SMSG_DISMOUNT\");" dismount_registration) +if(dismount_registration EQUAL -1) + message(FATAL_ERROR "SMSG_DISMOUNT is missing outbound opcode metadata") +endif() +string(FIND "${world_session}" "case SMSG_DISMOUNT:" dismount_allowlist) +if(dismount_allowlist EQUAL -1) + message(FATAL_ERROR "SMSG_DISMOUNT is missing from the converted-packet gate") +endif() +string(REGEX MATCH "SMSG_DISMOUNT[ \t]+0x0E3A[ \t]+ACTIVE" dismount_reference "${opcode_reference}") +if(dismount_reference STREQUAL "") + message(FATAL_ERROR "reference inventory does not record active 0x0E3A dismount") +endif() + +string(FIND "${player_header}" "out.WriteGuidMask<0, 5, 6, 3, 7, 4, 1, 2>(target);" combo_mask) +if(combo_mask EQUAL -1) + message(FATAL_ERROR "combo-point target mask does not match reader sub_6E2BC4") +endif() +string(FIND "${player_header}" "out.WriteGuidBytes<5, 6, 4, 7, 3, 0>(target);" combo_bytes_first) +if(combo_bytes_first EQUAL -1) + message(FATAL_ERROR "combo-point leading target bytes do not match reader sub_6E2BC4") +endif() +string(FIND "${player_header}" "out << points;" combo_value) +if(combo_value EQUAL -1) + message(FATAL_ERROR "combo-point value is missing from the 18414 serializer") +endif() +string(FIND "${player_header}" "out.WriteGuidBytes<2, 1>(target);" combo_bytes_last) +if(combo_bytes_last EQUAL -1) + message(FATAL_ERROR "combo-point trailing target bytes do not match reader sub_6E2BC4") +endif() +string(FIND "${player_combo}" "MopComboPointPackets::BuildUpdate(data, combotarget->GetObjectGuid(), uint8(m_comboPoints));" combo_sender) +if(combo_sender EQUAL -1) + message(FATAL_ERROR "combo-point sender bypasses the 18414 serializer") +endif() +string(FIND "${player_combo}" "WorldPacket data(SMSG_UPDATE_COMBO_POINTS" legacy_combo_sender) +if(NOT legacy_combo_sender EQUAL -1) + message(FATAL_ERROR "legacy raw combo-point packet construction remains") +endif() +string(FIND "${opcode_registry}" "DefS(SMSG_UPDATE_COMBO_POINTS, \"SMSG_UPDATE_COMBO_POINTS\");" combo_registration) +if(combo_registration EQUAL -1) + message(FATAL_ERROR "SMSG_UPDATE_COMBO_POINTS is missing outbound opcode metadata") +endif() +string(FIND "${world_session}" "case SMSG_UPDATE_COMBO_POINTS:" combo_allowlist) +if(combo_allowlist EQUAL -1) + message(FATAL_ERROR "SMSG_UPDATE_COMBO_POINTS is missing from the converted-packet gate") +endif() +string(REGEX MATCH "SMSG_UPDATE_COMBO_POINTS[ \t]+0x082F[ \t]+ACTIVE" combo_reference "${opcode_reference}") +if(combo_reference STREQUAL "") + message(FATAL_ERROR "reference inventory does not record active 0x082F combo points") +endif() +string(FIND "${unit_header}" "out.WriteGuidMask<1, 7, 5, 2, 6, 0, 3, 4>(guid);" pre_resurrect_mask) +if(pre_resurrect_mask EQUAL -1) + message(FATAL_ERROR "pre-resurrect GUID mask does not match readers sub_6E7875/sub_6D6EF4") +endif() +string(FIND "${unit_header}" "out.WriteGuidBytes<5, 1, 7, 0, 6, 4, 2, 3>(guid);" pre_resurrect_bytes) +if(pre_resurrect_bytes EQUAL -1) + message(FATAL_ERROR "pre-resurrect GUID byte order does not match readers sub_6E7875/sub_6D6EF4") +endif() +string(FIND "${player_death}" "MopCompactPackets::BuildPreResurrect(data, GetObjectGuid());" pre_resurrect_sender) +if(pre_resurrect_sender EQUAL -1) + message(FATAL_ERROR "pre-resurrect sender bypasses the 18414 serializer") +endif() +string(FIND "${player_death}" "WorldPacket data(SMSG_PRE_RESURRECT" legacy_pre_resurrect_sender) +if(NOT legacy_pre_resurrect_sender EQUAL -1) + message(FATAL_ERROR "legacy raw pre-resurrect packet construction remains") +endif() +string(FIND "${opcode_registry}" "DefS(SMSG_PRE_RESURRECT, \"SMSG_PRE_RESURRECT\");" pre_resurrect_registration) +if(pre_resurrect_registration EQUAL -1) + message(FATAL_ERROR "SMSG_PRE_RESURRECT is missing outbound opcode metadata") +endif() +string(FIND "${world_session}" "case SMSG_PRE_RESURRECT:" pre_resurrect_allowlist) +if(pre_resurrect_allowlist EQUAL -1) + message(FATAL_ERROR "SMSG_PRE_RESURRECT is missing from the converted-packet gate") +endif() +string(REGEX MATCH "SMSG_PRE_RESURRECT[ ]+0x19C0[ ]+ACTIVE" pre_resurrect_reference "${opcode_reference}") +if(pre_resurrect_reference STREQUAL "") + message(FATAL_ERROR "reference inventory does not record active 0x19C0 pre-resurrect") +endif() diff --git a/src/game/Server/tests/mop_compact_packets_test.cpp b/src/game/Server/tests/mop_compact_packets_test.cpp index 1533ea596..43f48dae5 100644 --- a/src/game/Server/tests/mop_compact_packets_test.cpp +++ b/src/game/Server/tests/mop_compact_packets_test.cpp @@ -352,6 +352,238 @@ static void test_cancel_combat() CHECK(uint32_t(packet.GetOpcode()) == 0x0E8Bu); } +static void test_party_kill_log() +{ + WorldPacket packet; + MopCompactPackets::BuildPartyKillLog( + packet, + ObjectGuid(UINT64_C(0x8877665544332211)), + ObjectGuid(UINT64_C(0xFFEEDDCCBBAA9901))); + CHECK(packet.GetOpcode() == SMSG_PARTYKILLLOG); + CHECK(BytesEqual(packet, { + 0xFF, 0xFF, + 0x00, 0xDC, 0x10, 0x32, + 0xFE, 0xEF, 0x98, 0xCD, + 0x54, 0x23, 0xAB, 0x76, + 0x45, 0x67, 0x89, 0xBA, + })); +} + +static void test_duel_state_packets() +{ + WorldPacket outOfBounds; + MopDuelPackets::BuildOutOfBounds(outOfBounds); + CHECK(outOfBounds.GetOpcode() == SMSG_DUEL_OUTOFBOUNDS); + CHECK(outOfBounds.empty()); + + WorldPacket inBounds; + MopDuelPackets::BuildInBounds(inBounds); + CHECK(inBounds.GetOpcode() == SMSG_DUEL_INBOUNDS); + CHECK(inBounds.empty()); + + WorldPacket completed; + MopDuelPackets::BuildComplete(completed, true); + CHECK(completed.GetOpcode() == SMSG_DUEL_COMPLETE); + CHECK(BytesEqual(completed, { 0x80 })); + + WorldPacket interrupted; + MopDuelPackets::BuildComplete(interrupted, false); + CHECK(BytesEqual(interrupted, { 0x00 })); + + WorldPacket countdown; + MopDuelPackets::BuildCountdown(countdown, 0x12345678u); + CHECK(countdown.GetOpcode() == SMSG_DUEL_COUNTDOWN); + CHECK(BytesEqual(countdown, { 0x78, 0x56, 0x34, 0x12 })); +} + +static void test_duel_request_and_winner_packets() +{ + WorldPacket requested; + MopDuelPackets::BuildRequested( + requested, + ObjectGuid(UINT64_C(0x0807060504030201)), + ObjectGuid(UINT64_C(0x100F0E0D0C0B0A09))); + CHECK(requested.GetOpcode() == SMSG_DUEL_REQUESTED); + CHECK(BytesEqual(requested, { + 0xFF, 0xFF, + 0x07, 0x05, 0x11, 0x0C, + 0x09, 0x0D, 0x0E, 0x08, + 0x04, 0x0A, 0x0B, 0x00, + 0x02, 0x06, 0x03, 0x0F, + })); + + WorldPacket winner; + CHECK(MopDuelPackets::BuildWinner( + winner, false, "Winner", 0x10203040u, "Loser", 0xA1B2C3D4u)); + CHECK(winner.GetOpcode() == SMSG_DUEL_WINNER); + CHECK(BytesEqual(winner, { + 0x0C, 0x28, + 0xD4, 0xC3, 0xB2, 0xA1, + 'W', 'i', 'n', 'n', 'e', 'r', + 0x40, 0x30, 0x20, 0x10, + 'L', 'o', 's', 'e', 'r', + })); + + WorldPacket retreat; + CHECK(MopDuelPackets::BuildWinner( + retreat, true, "Winner", 0x10203040u, "Loser", 0xA1B2C3D4u)); + CHECK(retreat[0] == 0x8C); + + WorldPacket maximum; + CHECK(MopDuelPackets::BuildWinner( + maximum, false, std::string(63, 'W'), 1, std::string(63, 'L'), 2)); + WorldPacket tooLong; + CHECK(!MopDuelPackets::BuildWinner( + tooLong, false, std::string(64, 'W'), 1, "L", 2)); + CHECK(tooLong.empty()); +} + +static void test_mirror_timer_packets() +{ + WorldPacket started; + MopMirrorTimerPackets::BuildStart( + started, 0xDDEEFF00u, 0x11223344u, 0x99AABBCCu, + int32_t(-2), 0x55667788u, true); + CHECK(started.GetOpcode() == SMSG_START_MIRROR_TIMER); + CHECK(BytesEqual(started, { + 0x44, 0x33, 0x22, 0x11, + 0x88, 0x77, 0x66, 0x55, + 0xCC, 0xBB, 0xAA, 0x99, + 0xFE, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0xEE, 0xDD, + 0x80, + })); + + WorldPacket running; + MopMirrorTimerPackets::BuildStart( + running, 1, 1000, 750, -1, 0, false); + CHECK(running[20] == 0x00); + + WorldPacket stopped; + MopMirrorTimerPackets::BuildStop(stopped, 0x12345678u); + CHECK(stopped.GetOpcode() == SMSG_STOP_MIRROR_TIMER); + CHECK(BytesEqual(stopped, { 0x78, 0x56, 0x34, 0x12 })); +} + +static void test_rune_packets() +{ + std::array const runes = {{ + { RUNE_BLOOD, 0x10 }, + { RUNE_BLOOD, 0x20 }, + { RUNE_UNHOLY, 0x30 }, + { RUNE_UNHOLY, 0x40 }, + { RUNE_FROST, 0x50 }, + { RUNE_DEATH, 0x60 }, + }}; + + WorldPacket resync; + MopRunePackets::BuildResync(resync, runes); + CHECK(resync.GetOpcode() == SMSG_RESYNC_RUNES); + CHECK(BytesEqual(resync, { + 0x00, 0x00, 0x0C, + 0x10, 0x00, 0x20, 0x00, + 0x30, 0x01, 0x40, 0x01, + 0x50, 0x02, 0x60, 0x03, + })); + + WorldPacket power; + MopRunePackets::BuildAddPower(power, 0x20u); + CHECK(power.GetOpcode() == SMSG_ADD_RUNE_POWER); + CHECK(BytesEqual(power, { 0x20, 0x00, 0x00, 0x00 })); + + WorldPacket converted; + MopRunePackets::BuildConvert(converted, RUNE_DEATH, 4); + CHECK(converted.GetOpcode() == SMSG_CONVERT_RUNE); + CHECK(BytesEqual(converted, { 0x03, 0x04 })); +} + +static void test_threat_packets() +{ + ObjectGuid const owner(UINT64_C(0x0807060504030201)); + ObjectGuid const selected(UINT64_C(0x100F0E0D0C0B0A09)); + MopThreatPackets::ThreatEntries const entries = {{ + ObjectGuid(UINT64_C(0x1817161514131211)), 0xA1B2C3D4u + }}; + + WorldPacket update; + MopThreatPackets::BuildUpdate(update, owner, entries); + CHECK(update.GetOpcode() == SMSG_THREAT_UPDATE); + CHECK(BytesEqual(update, { + 0xFE, 0x00, 0x00, 0x1F, 0xF8, + 0x16, 0x19, 0x10, 0x13, 0x12, 0x17, 0x15, 0x14, + 0xD4, 0xC3, 0xB2, 0xA1, + 0x03, 0x04, 0x02, 0x05, 0x07, 0x06, 0x00, 0x09, + })); + + WorldPacket highest; + MopThreatPackets::BuildHighest(highest, owner, selected, entries); + CHECK(highest.GetOpcode() == SMSG_HIGHEST_THREAT_UPDATE); + CHECK(BytesEqual(highest, { + 0xFF, 0xF8, 0x00, 0x00, 0x7F, 0xF8, + 0x04, 0x16, 0xD4, 0xC3, 0xB2, 0xA1, + 0x14, 0x10, 0x15, 0x17, 0x12, 0x13, 0x19, + 0x0D, 0x07, 0x0A, 0x03, 0x00, 0x02, 0x0E, 0x0B, + 0x09, 0x08, 0x0C, 0x11, 0x05, 0x06, 0x0F, + })); + + WorldPacket clear; + MopThreatPackets::BuildClear(clear, owner); + CHECK(clear.GetOpcode() == SMSG_THREAT_CLEAR); + CHECK(BytesEqual(clear, { + 0xFF, 0x09, 0x00, 0x04, 0x05, 0x02, 0x03, 0x06, 0x07, + })); + + WorldPacket remove; + MopThreatPackets::BuildRemove(remove, owner, selected); + CHECK(remove.GetOpcode() == SMSG_THREAT_REMOVE); + CHECK(BytesEqual(remove, { + 0xFF, 0xFF, 0x0D, 0x08, 0x0A, 0x07, 0x04, 0x09, 0x05, + 0x00, 0x0C, 0x03, 0x0B, 0x06, 0x11, 0x0E, 0x02, 0x0F, + })); +} + +static void test_dismount_packet() +{ + WorldPacket packet; + MopCompactPackets::BuildDismount( + packet, ObjectGuid(UINT64_C(0x0807060504030201))); + CHECK(packet.GetOpcode() == SMSG_DISMOUNT); + CHECK(BytesEqual(packet, { + 0xFF, 0x05, 0x06, 0x09, 0x07, 0x03, 0x04, 0x02, 0x00, + })); +} + +static void test_combo_points_packet() +{ + WorldPacket packet; + MopComboPointPackets::BuildUpdate( + packet, ObjectGuid(UINT64_C(0x0807060504030201)), 5); + CHECK(packet.GetOpcode() == SMSG_UPDATE_COMBO_POINTS); + CHECK(BytesEqual(packet, { + 0xFF, 0x07, 0x06, 0x04, 0x09, 0x05, 0x00, 0x05, 0x02, 0x03, + })); +} + +static void test_pre_resurrect_packet() +{ + WorldPacket packet; + MopCompactPackets::BuildPreResurrect( + packet, ObjectGuid(UINT64_C(0x0807060504030201))); + CHECK(packet.GetOpcode() == SMSG_PRE_RESURRECT); + CHECK(BytesEqual(packet, { + 0xFF, 0x07, 0x03, 0x09, 0x00, 0x06, 0x04, 0x02, 0x05, + })); + + // A zero byte clears its mask bit and is omitted entirely, so the body + // shortens. Guards against writing a fixed nine-byte body. + WorldPacket sparse; + MopCompactPackets::BuildPreResurrect( + sparse, ObjectGuid(UINT64_C(0x0000060000030001))); + CHECK(BytesEqual(sparse, { + 0x34, 0x07, 0x00, 0x02, + })); +} + static void test_opcode_values_are_framable() { CHECK(uint32_t(SMSG_ATTACKSWING_ERROR) == 0x11E1u); @@ -364,6 +596,25 @@ static void test_opcode_values_are_framable() CHECK(uint32_t(SMSG_ATTACKSTOP) == 0x12AFu); CHECK(uint32_t(SMSG_ATTACKERSTATEUPDATE) == 0x06AAu); CHECK(uint32_t(SMSG_CANCEL_COMBAT) == 0x0E8Bu); + CHECK(uint32_t(SMSG_PARTYKILLLOG) == 0x048Au); + CHECK(uint32_t(SMSG_DUEL_OUTOFBOUNDS) == 0x001Au); + CHECK(uint32_t(SMSG_DUEL_INBOUNDS) == 0x163Au); + CHECK(uint32_t(SMSG_DUEL_COMPLETE) == 0x1C0Au); + CHECK(uint32_t(SMSG_DUEL_COUNTDOWN) == 0x129Fu); + CHECK(uint32_t(SMSG_DUEL_REQUESTED) == 0x0022u); + CHECK(uint32_t(SMSG_DUEL_WINNER) == 0x10E1u); + CHECK(uint32_t(SMSG_START_MIRROR_TIMER) == 0x0E12u); + CHECK(uint32_t(SMSG_STOP_MIRROR_TIMER) == 0x1026u); + CHECK(uint32_t(SMSG_RESYNC_RUNES) == 0x15E3u); + CHECK(uint32_t(SMSG_ADD_RUNE_POWER) == 0x1860u); + CHECK(uint32_t(SMSG_CONVERT_RUNE) == 0x1A1Bu); + CHECK(uint32_t(SMSG_THREAT_UPDATE) == 0x0632u); + CHECK(uint32_t(SMSG_HIGHEST_THREAT_UPDATE) == 0x14AEu); + CHECK(uint32_t(SMSG_THREAT_CLEAR) == 0x180Bu); + CHECK(uint32_t(SMSG_THREAT_REMOVE) == 0x1960u); + CHECK(uint32_t(SMSG_DISMOUNT) == 0x0E3Au); + CHECK(uint32_t(SMSG_PRE_RESURRECT) == 0x19C0u); + CHECK(uint32_t(SMSG_UPDATE_COMBO_POINTS) == 0x082Fu); CHECK(uint32_t(SMSG_ATTACKSWING_ERROR) <= 0x1FFFu); CHECK(uint32_t(SMSG_MOVE_SET_SWIM_SPEED) <= 0x1FFFu); @@ -375,6 +626,25 @@ static void test_opcode_values_are_framable() CHECK(uint32_t(SMSG_ATTACKSTOP) <= 0x1FFFu); CHECK(uint32_t(SMSG_ATTACKERSTATEUPDATE) <= 0x1FFFu); CHECK(uint32_t(SMSG_CANCEL_COMBAT) <= 0x1FFFu); + CHECK(uint32_t(SMSG_PARTYKILLLOG) <= 0x1FFFu); + CHECK(uint32_t(SMSG_DUEL_OUTOFBOUNDS) <= 0x1FFFu); + CHECK(uint32_t(SMSG_DUEL_INBOUNDS) <= 0x1FFFu); + CHECK(uint32_t(SMSG_DUEL_COMPLETE) <= 0x1FFFu); + CHECK(uint32_t(SMSG_DUEL_COUNTDOWN) <= 0x1FFFu); + CHECK(uint32_t(SMSG_DUEL_REQUESTED) <= 0x1FFFu); + CHECK(uint32_t(SMSG_DUEL_WINNER) <= 0x1FFFu); + CHECK(uint32_t(SMSG_START_MIRROR_TIMER) <= 0x1FFFu); + CHECK(uint32_t(SMSG_STOP_MIRROR_TIMER) <= 0x1FFFu); + CHECK(uint32_t(SMSG_RESYNC_RUNES) <= 0x1FFFu); + CHECK(uint32_t(SMSG_ADD_RUNE_POWER) <= 0x1FFFu); + CHECK(uint32_t(SMSG_CONVERT_RUNE) <= 0x1FFFu); + CHECK(uint32_t(SMSG_THREAT_UPDATE) <= 0x1FFFu); + CHECK(uint32_t(SMSG_HIGHEST_THREAT_UPDATE) <= 0x1FFFu); + CHECK(uint32_t(SMSG_THREAT_CLEAR) <= 0x1FFFu); + CHECK(uint32_t(SMSG_THREAT_REMOVE) <= 0x1FFFu); + CHECK(uint32_t(SMSG_DISMOUNT) <= 0x1FFFu); + CHECK(uint32_t(SMSG_PRE_RESURRECT) <= 0x1FFFu); + CHECK(uint32_t(SMSG_UPDATE_COMBO_POINTS) <= 0x1FFFu); } int main(int /*argc*/, char** /*argv*/) @@ -388,6 +658,15 @@ int main(int /*argc*/, char** /*argv*/) test_raid_difficulty(); test_dungeon_difficulty(); test_cancel_combat(); + test_party_kill_log(); + test_duel_state_packets(); + test_duel_request_and_winner_packets(); + test_mirror_timer_packets(); + test_rune_packets(); + test_threat_packets(); + test_dismount_packet(); + test_pre_resurrect_packet(); + test_combo_points_packet(); test_opcode_values_are_framable(); if (g_fail) diff --git a/src/game/Server/tests/mop_death_packets_source_test.cmake b/src/game/Server/tests/mop_death_packets_source_test.cmake index 6b0843ded..eafd2e202 100644 --- a/src/game/Server/tests/mop_death_packets_source_test.cmake +++ b/src/game/Server/tests/mop_death_packets_source_test.cmake @@ -1,5 +1,7 @@ file(READ "${SOURCE_ROOT}/src/game/Object/Player.h" player_header) file(READ "${SOURCE_ROOT}/src/game/Object/PlayerDeath.cpp" player_death) +file(READ "${SOURCE_ROOT}/src/game/Object/PlayerMirror.cpp" player_mirror) +file(READ "${SOURCE_ROOT}/src/game/Object/Unit.cpp" unit_source) file(READ "${SOURCE_ROOT}/src/game/Object/ObjectMgr.h" object_mgr_header) file(READ "${SOURCE_ROOT}/src/game/Object/ObjectMgrGraveyard.cpp" object_mgr_graveyard) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/MiscHandler.cpp" misc_handler) @@ -7,6 +9,7 @@ file(READ "${SOURCE_ROOT}/src/game/Server/Opcodes.h" opcode_header) file(READ "${SOURCE_ROOT}/src/game/Server/Opcodes.cpp" opcode_registry) file(READ "${SOURCE_ROOT}/src/game/Server/WorldSession.h" session_header) file(READ "${SOURCE_ROOT}/src/game/Server/WorldSession.cpp" session_source) +file(READ "${SOURCE_ROOT}/src/game/Server/Opcodes_reference.h" opcode_reference) if(MUTATION STREQUAL "wire_order") string(REPLACE "out << mapId << y << x << z;" "out << mapId << x << y << z;" @@ -72,6 +75,36 @@ elseif(MUTATION STREQUAL "cemetery_team_filter") "data.team != TEAM_BOTH_ALLOWED && data.team != team" "data.team != team" object_mgr_graveyard "${object_mgr_graveyard}") +elseif(MUTATION STREQUAL "durability_death_body") + string(REPLACE + "out.Initialize(SMSG_DURABILITY_DAMAGE_DEATH, 0);" + "out.Initialize(SMSG_DURABILITY_DAMAGE_DEATH, 1); out << uint8(0);" + player_header "${player_header}") +elseif(MUTATION STREQUAL "durability_death_mirror_sender") + string(REPLACE + "MopDeathPackets::BuildDurabilityDamageDeath(data2);" + "/* removed fall-death durability sender */" + player_mirror "${player_mirror}") +elseif(MUTATION STREQUAL "durability_death_unit_sender") + string(REPLACE + "MopDeathPackets::BuildDurabilityDamageDeath(data);" + "/* removed combat-death durability sender */" + unit_source "${unit_source}") +elseif(MUTATION STREQUAL "durability_death_registration") + string(REPLACE + "DefS(SMSG_DURABILITY_DAMAGE_DEATH, \"SMSG_DURABILITY_DAMAGE_DEATH\");" + "/* removed durability-death registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "durability_death_whitelist") + string(REPLACE + "case SMSG_DURABILITY_DAMAGE_DEATH:" + "/* removed durability-death whitelist */" + session_source "${session_source}") +elseif(MUTATION STREQUAL "durability_death_reference") + string(REPLACE + "SMSG_DURABILITY_DAMAGE_DEATH 0x1E3E ACTIVE" + "SMSG_DURABILITY_DAMAGE_DEATH 0x1E3E DORMANT" + opcode_reference "${opcode_reference}") endif() function(require_once source token context) @@ -86,6 +119,30 @@ require_once("${player_header}" "out << mapId << y << x << z" "death-release 18414 field order") +require_once("${player_header}" + "out\\.Initialize\\(SMSG_DURABILITY_DAMAGE_DEATH, 0\\)" + "empty durability-death serializer") +require_once("${player_mirror}" + "MopDeathPackets::BuildDurabilityDamageDeath\\(data2\\)" + "fall-death durability sender") +require_once("${unit_source}" + "MopDeathPackets::BuildDurabilityDamageDeath\\(data\\)" + "combat-death durability sender") +require_once("${opcode_registry}" + "DefS\\(SMSG_DURABILITY_DAMAGE_DEATH,[ \t]*\"SMSG_DURABILITY_DAMAGE_DEATH\"\\)" + "durability-death registration") +require_once("${session_source}" + "case[ \t]+SMSG_DURABILITY_DAMAGE_DEATH:" + "durability-death suppression whitelist") +require_once("${opcode_reference}" + "SMSG_DURABILITY_DAMAGE_DEATH[ \t]+0x1E3E[ \t]+ACTIVE" + "active direct-client durability-death reference") +foreach(source IN ITEMS player_mirror unit_source) + if("${${source}}" MATCHES "WorldPacket[ \t]+[A-Za-z0-9_]+\\(SMSG_DURABILITY_DAMAGE_DEATH") + message(FATAL_ERROR "live inline durability-death sender remains in ${source}") + endif() +endforeach() + string(REGEX MATCHALL "MopDeathPackets::BuildDeathReleaseLocation\\(data," player_calls "${player_death}") diff --git a/src/game/Server/tests/mop_death_packets_test.cpp b/src/game/Server/tests/mop_death_packets_test.cpp index b0c4d44da..0de2b0bb6 100644 --- a/src/game/Server/tests/mop_death_packets_test.cpp +++ b/src/game/Server/tests/mop_death_packets_test.cpp @@ -85,6 +85,14 @@ static void test_opcode_is_framable() CHECK(uint32(SMSG_DEATH_RELEASE_LOC) < uint32(OPCODE_TABLE_SIZE)); } +static void test_durability_damage_death_is_empty() +{ + WorldPacket packet; + MopDeathPackets::BuildDurabilityDamageDeath(packet); + CHECK(packet.GetOpcode() == SMSG_DURABILITY_DAMAGE_DEATH); + CHECK(packet.empty()); +} + static void test_empty_cemetery_list_response() { WorldPacket packet; @@ -148,6 +156,7 @@ int main(int /*argc*/, char** /*argv*/) test_graveyard_location(); test_clear_location(); test_opcode_is_framable(); + test_durability_damage_death_is_empty(); test_empty_cemetery_list_response(); test_cemetery_list_response(); test_cemetery_list_response_is_bounded(); diff --git a/src/game/Server/tests/mop_player_progression_packets_source_test.cmake b/src/game/Server/tests/mop_player_progression_packets_source_test.cmake new file mode 100644 index 000000000..a87e0dc4c --- /dev/null +++ b/src/game/Server/tests/mop_player_progression_packets_source_test.cmake @@ -0,0 +1,129 @@ +if(NOT DEFINED SOURCE_ROOT) + message(FATAL_ERROR "SOURCE_ROOT is required") +endif() + +file(READ "${SOURCE_ROOT}/src/game/Object/Player.h" player_header) +file(READ "${SOURCE_ROOT}/src/game/Object/Player.cpp" player_source) +file(READ "${SOURCE_ROOT}/src/game/Server/Opcodes.cpp" opcode_registry) +file(READ "${SOURCE_ROOT}/src/game/Server/Opcodes_reference.h" opcode_reference) +file(READ "${SOURCE_ROOT}/src/game/Server/WorldSession.cpp" world_session) + +if(DEFINED MUTATION) + if(MUTATION STREQUAL "wire_order") + string(REPLACE + "out << info.talentDelta << info.healthDelta;" + "out << info.healthDelta << info.talentDelta;" + player_header "${player_header}") + elseif(MUTATION STREQUAL "xp_wire_order") + string(REPLACE + "out.WriteGuidMask<1, 2, 7, 4, 3>(info.sourceGuid);" + "out.WriteGuidMask<2, 1, 7, 4, 3>(info.sourceGuid);" + player_header "${player_header}") + elseif(MUTATION STREQUAL "sender") + string(REPLACE + "MopProgressionPackets::BuildLevelUpInfo(data, packetInfo);" + "/* removed level-up sender */" + player_source "${player_source}") + elseif(MUTATION STREQUAL "xp_sender") + string(REPLACE + "MopProgressionPackets::BuildExperienceGain(data, packetInfo);" + "/* removed experience-gain sender */" + player_source "${player_source}") + elseif(MUTATION STREQUAL "xp_registration") + string(REPLACE + "DefS(SMSG_LOG_XPGAIN, \"SMSG_LOG_XPGAIN\");" + "/* removed experience-gain registration */" + opcode_registry "${opcode_registry}") + elseif(MUTATION STREQUAL "xp_admission") + string(REPLACE + "case SMSG_LOG_XPGAIN:" + "/* removed experience-gain admission */" + world_session "${world_session}") + elseif(MUTATION STREQUAL "xp_reference") + string(REPLACE + "SMSG_LOG_XPGAIN 0x1E9A ACTIVE" + "SMSG_LOG_XPGAIN 0x1E9A DORMANT" + opcode_reference "${opcode_reference}") + elseif(MUTATION STREQUAL "registration") + string(REPLACE + "DefS(SMSG_LEVELUP_INFO, \"SMSG_LEVELUP_INFO\");" + "/* removed level-up registration */" + opcode_registry "${opcode_registry}") + elseif(MUTATION STREQUAL "admission") + string(REPLACE + "case SMSG_LEVELUP_INFO:" + "/* removed level-up admission */" + world_session "${world_session}") + elseif(MUTATION STREQUAL "reference") + string(REPLACE + "SMSG_LEVELUP_INFO 0x1961 ACTIVE" + "SMSG_LEVELUP_INFO 0x1961 DORMANT" + opcode_reference "${opcode_reference}") + else() + message(FATAL_ERROR "unknown MUTATION=${MUTATION}") + endif() +endif() + +function(require_once text needle label) + string(REGEX MATCHALL "${needle}" matches "${text}") + list(LENGTH matches count) + if(NOT count EQUAL 1) + message(FATAL_ERROR "${label}: expected exactly once, found ${count}") + endif() +endfunction() + +require_once("${player_header}" + "out << info\\.talentDelta << info\\.healthDelta" + "18414 level-up leading field order") +require_once("${player_header}" + "for \\(uint32 delta : info\\.statDeltas\\)" + "five level-up stat deltas") +require_once("${player_header}" + "out << info\\.level" + "18414 level-up level slot") +require_once("${player_header}" + "for \\(uint32 delta : info\\.powerDeltas\\)" + "five level-up power deltas") +require_once("${player_source}" + "MopProgressionPackets::BuildLevelUpInfo\\(data, packetInfo\\)" + "level-up sender") +require_once("${player_header}" + "out\\.WriteGuidMask<1, 2, 7, 4, 3>\\(info\\.sourceGuid\\)" + "experience source GUID leading mask") +require_once("${player_header}" + "out\\.WriteGuidMask<0, 5, 6>\\(info\\.sourceGuid\\)" + "experience source GUID trailing mask") +require_once("${player_header}" + "out << info\\.totalExperience" + "experience total field") +require_once("${player_header}" + "if \\(info\\.hasBaseExperience\\)" + "optional base-experience branch") +require_once("${player_source}" + "MopProgressionPackets::BuildExperienceGain\\(data, packetInfo\\)" + "experience-gain sender") +require_once("${opcode_registry}" + "DefS\\(SMSG_LEVELUP_INFO,[ \\t]*\"SMSG_LEVELUP_INFO\"\\)" + "level-up registration") +require_once("${opcode_registry}" + "DefS\\(SMSG_LOG_XPGAIN,[ \\t]*\"SMSG_LOG_XPGAIN\"\\)" + "experience-gain registration") +require_once("${world_session}" + "case[ \t]+SMSG_LEVELUP_INFO:" + "level-up admission") +require_once("${world_session}" + "case[ \\t]+SMSG_LOG_XPGAIN:" + "experience-gain admission") +require_once("${opcode_reference}" + "SMSG_LEVELUP_INFO[ \t]+0x1961[ \t]+ACTIVE" + "active direct-client level-up reference") +require_once("${opcode_reference}" + "SMSG_LOG_XPGAIN[ \\t]+0x1E9A[ \\t]+ACTIVE" + "active direct-client experience reference") + +if("${player_source}" MATCHES "WorldPacket[ \\t]+[A-Za-z0-9_]+\\(SMSG_LEVELUP_INFO") + message(FATAL_ERROR "legacy inline level-up sender remains") +endif() +if("${player_source}" MATCHES "WorldPacket[ \\t]+[A-Za-z0-9_]+\\(SMSG_LOG_XPGAIN") + message(FATAL_ERROR "legacy inline experience-gain sender remains") +endif() diff --git a/src/game/Server/tests/mop_player_progression_packets_test.cpp b/src/game/Server/tests/mop_player_progression_packets_test.cpp new file mode 100644 index 000000000..1960e8383 --- /dev/null +++ b/src/game/Server/tests/mop_player_progression_packets_test.cpp @@ -0,0 +1,114 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2026 MaNGOS + */ + +/** + * Byte-exact tests for directly verified 5.4.8 player-progression packets. + */ + +#include "Player.h" +#include "Opcodes.h" +#include "WorldPacket.h" + +#include +#include +#include + +static int g_fail = 0; +#define CHECK(c) do { if (!(c)) { std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #c); ++g_fail; } } while (0) + +static void CheckBytes(WorldPacket const& packet, + std::initializer_list expected) +{ + CHECK(packet.size() == expected.size()); + size_t index = 0; + for (uint8_t byte : expected) + { + if (index < packet.size()) + CHECK(packet[index] == byte); + ++index; + } +} + +static void TestLevelUpInfo() +{ + MopProgressionPackets::LevelUpInfo info; + info.talentDelta = 0x01020304u; + info.healthDelta = 0x11121314u; + info.statDeltas = {{ + 0x21222324u, 0x31323334u, 0x41424344u, + 0x51525354u, 0x61626364u, + }}; + info.level = 0x71727374u; + info.powerDeltas = {{ + 0x81828384u, 0x91929394u, 0xA1A2A3A4u, + 0xB1B2B3B4u, 0xC1C2C3C4u, + }}; + + WorldPacket packet; + MopProgressionPackets::BuildLevelUpInfo(packet, info); + CHECK(packet.GetOpcode() == SMSG_LEVELUP_INFO); + CheckBytes(packet, { + 0x04, 0x03, 0x02, 0x01, + 0x14, 0x13, 0x12, 0x11, + 0x24, 0x23, 0x22, 0x21, + 0x34, 0x33, 0x32, 0x31, + 0x44, 0x43, 0x42, 0x41, + 0x54, 0x53, 0x52, 0x51, + 0x64, 0x63, 0x62, 0x61, + 0x74, 0x73, 0x72, 0x71, + 0x84, 0x83, 0x82, 0x81, + 0x94, 0x93, 0x92, 0x91, + 0xA4, 0xA3, 0xA2, 0xA1, + 0xB4, 0xB3, 0xB2, 0xB1, + 0xC4, 0xC3, 0xC2, 0xC1, + }); +} + +static void TestExperienceGain() +{ + MopProgressionPackets::ExperienceGain kill; + kill.sourceGuid = ObjectGuid(UINT64_C(0x8877665544332211)); + kill.totalExperience = 0x11223344u; + kill.type = 0; + kill.hasBaseExperience = true; + kill.baseExperience = 0x55667788u; + + WorldPacket killPacket; + MopProgressionPackets::BuildExperienceGain(killPacket, kill); + CHECK(killPacket.GetOpcode() == SMSG_LOG_XPGAIN); + CheckBytes(killPacket, { + 0x7D, 0xE0, + 0x54, 0x32, 0x00, 0x89, 0x23, 0x45, 0x76, + 0x44, 0x33, 0x22, 0x11, + 0x88, 0x77, 0x66, 0x55, + 0x10, 0x67, + }); + + MopProgressionPackets::ExperienceGain quest; + quest.totalExperience = 0xA1B2C3D4u; + quest.type = 1; + + WorldPacket questPacket; + MopProgressionPackets::BuildExperienceGain(questPacket, quest); + CheckBytes(questPacket, { + 0x80, 0x20, 0x01, + 0xD4, 0xC3, 0xB2, 0xA1, + }); +} + +int main() +{ + TestLevelUpInfo(); + TestExperienceGain(); + if (g_fail != 0) + return 1; + + std::printf("mop_player_progression_packets: all checks passed\n"); + return 0; +} diff --git a/src/game/Server/tests/mop_quest_npc_query_test.cpp b/src/game/Server/tests/mop_quest_npc_query_test.cpp new file mode 100644 index 000000000..46603009d --- /dev/null +++ b/src/game/Server/tests/mop_quest_npc_query_test.cpp @@ -0,0 +1,179 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MaNGOS is a full featured server for World of Warcraft, supporting + * the 5.4.8 client build 18414. + */ + +/** + * Byte-exact tests for the 5.4.8 quest-NPC request and response pair. + * + * The response grammar was recovered from client parser sub_6B8B3B -> + * sub_6B8A06 and then confirmed against real 18414 retail captures: every + * SMSG_QUEST_NPC_QUERY_RESPONSE in the sniff corpus decodes with this reader + * and leaves no residual byte. + */ + +#include "WorldSession.h" +#include "Opcodes.h" +#include "WorldPacket.h" + +#include +#include +#include + +static int g_fail = 0; +#define CHECK(c) do { if (!(c)) { std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #c); ++g_fail; } } while (0) + +static bool ExpectBytes(WorldPacket const& packet, + std::vector const& expected) +{ + if (packet.size() != expected.size()) + { + std::fprintf(stderr, " size %u, wanted %u\n", + unsigned(packet.size()), unsigned(expected.size())); + return false; + } + + for (size_t i = 0; i < expected.size(); ++i) + { + if (packet.contents()[i] != expected[i]) + { + std::fprintf(stderr, " byte %u = 0x%02X, wanted 0x%02X\n", + unsigned(i), packet.contents()[i], expected[i]); + return false; + } + } + return true; +} + +static void test_request_parser() +{ + // The live client always sends a 204-byte body, but only the leading + // uint32 is initialised. The remainder is client stack memory: on the + // 64-bit client it contains x64 image pointers that are byte-identical + // across every capture in one run. It must never be parsed. + { + WorldPacket request(CMSG_QUEST_NPC_QUERY, 204); + request << uint32(28766); + for (uint32 i = 0; i < 50; ++i) + request << uint32(0xDEADBEEFu); + + uint32 questId = 0; + CHECK(MopQueryPackets::ParseQuestNpcQueryRequest(request, questId)); + CHECK(questId == 28766); + } + + // A body carrying only the quest id is equally valid; the tail is not + // part of the contract, so its absence must not be treated as malformed. + { + WorldPacket minimal(CMSG_QUEST_NPC_QUERY, 4); + minimal << uint32(29080); + + uint32 questId = 0; + CHECK(MopQueryPackets::ParseQuestNpcQueryRequest(minimal, questId)); + CHECK(questId == 29080); + } + + // Short of one quest id there is nothing to answer. + { + WorldPacket truncated(CMSG_QUEST_NPC_QUERY, 3); + truncated << uint8(1) << uint8(2) << uint8(3); + + uint32 questId = 0xFFFFFFFFu; + CHECK(!MopQueryPackets::ParseQuestNpcQueryRequest(truncated, questId)); + } +} + +static void test_response_builder() +{ + // 21-bit quest count then one 22-bit NPC count per quest, flushed, then + // each quest id followed by its NPC ids. 1 and 2 here occupy 43 bits, so + // the bit phase is six bytes with five padding bits. + MopQueryPackets::QuestNpcResponse quest; + quest.questId = 0xE1E2E3E4u; + quest.npcIds.push_back(0x11121314u); + quest.npcIds.push_back(0x21222324u); + + WorldPacket packet; + CHECK(MopQueryPackets::BuildQuestNpcQueryResponse(packet, { quest })); + CHECK(packet.GetOpcode() == SMSG_QUEST_NPC_QUERY_RESPONSE); + CHECK(ExpectBytes(packet, { + 0x00, 0x00, 0x08, 0x00, 0x00, 0x40, + 0xE4, 0xE3, 0xE2, 0xE1, + 0x14, 0x13, 0x12, 0x11, + 0x24, 0x23, 0x22, 0x21 + })); + + // A quest whose ender set is empty is still a valid answer: the 22-bit + // count is simply zero. Our converted world database has such rows. + MopQueryPackets::QuestNpcResponse barren; + barren.questId = 33147; + + WorldPacket none; + CHECK(MopQueryPackets::BuildQuestNpcQueryResponse(none, { barren })); + CHECK(ExpectBytes(none, { + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x7B, 0x81, 0x00, 0x00 + })); + + // No quests at all: 21 bits of zero and nothing else. + WorldPacket empty; + CHECK(MopQueryPackets::BuildQuestNpcQueryResponse(empty, {})); + CHECK(empty.GetOpcode() == SMSG_QUEST_NPC_QUERY_RESPONSE); + CHECK(ExpectBytes(empty, { 0x00, 0x00, 0x00 })); +} + +static void test_retail_corpus_shape() +{ + // Reproduces a real 18414 retail response: seven quests with NPC counts + // [1,1,1,1,1,1,4]. Retail batches several quests into one reply even + // though the client requests them one at a time, so the builder must not + // assume a single-quest response. Bit phase is 21 + 7*22 = 175 bits = + // 22 bytes, matching the capture exactly. + std::vector response; + uint32 const quests[] = { 24586, 32374, 33333, 33338, 33374, 31926, 32863 }; + uint32 const singles[] = { 20735, 64616, 72870, 72870, 73303, 66557 }; + for (uint32 i = 0; i < 6; ++i) + { + MopQueryPackets::QuestNpcResponse entry; + entry.questId = quests[i]; + entry.npcIds.push_back(singles[i]); + response.push_back(entry); + } + MopQueryPackets::QuestNpcResponse last; + last.questId = 32863; + last.npcIds = { 64582, 64572, 63596, 63626 }; + response.push_back(last); + + WorldPacket packet; + CHECK(MopQueryPackets::BuildQuestNpcQueryResponse(packet, response)); + CHECK(packet.size() == 90); + CHECK(packet.contents()[0] == 0x00); + CHECK(packet.contents()[1] == 0x00); + CHECK(packet.contents()[2] == 0x38); // 21-bit count of seven +} + +static void test_opcode_values() +{ + CHECK(uint32_t(CMSG_QUEST_NPC_QUERY) == 0x1DAEu); + CHECK(uint32_t(SMSG_QUEST_NPC_QUERY_RESPONSE) == 0x036Du); + CHECK(uint32_t(CMSG_QUEST_NPC_QUERY) <= 0x1FFFu); + CHECK(uint32_t(SMSG_QUEST_NPC_QUERY_RESPONSE) <= 0x1FFFu); +} + +int main(int /*argc*/, char** /*argv*/) +{ + test_request_parser(); + test_response_builder(); + test_retail_corpus_shape(); + test_opcode_values(); + + if (g_fail) + { + std::fprintf(stderr, "%d check(s) failed\n", g_fail); + return 1; + } + std::printf("mop_quest_npc_query: all checks passed\n"); + return 0; +} diff --git a/src/game/Server/tests/mop_self_values_source_test.cmake b/src/game/Server/tests/mop_self_values_source_test.cmake index f0c32930d..d66419d06 100644 --- a/src/game/Server/tests/mop_self_values_source_test.cmake +++ b/src/game/Server/tests/mop_self_values_source_test.cmake @@ -27,6 +27,11 @@ elseif(MUTATION STREQUAL "buyback_feed") "for (uint16 i = MopUpdateObject::SelfBuybackSourceStart;" "for (uint16 i = 0; /* removed self buyback feed */" object_update "${object_update}") +elseif(MUTATION STREQUAL "questlog_feed") + string(REPLACE + "for (uint16 i = MopUpdateObject::SelfQuestLogSourceStart;" + "for (uint16 i = 0; /* removed self quest-log feed */" + object_update "${object_update}") endif() string(FIND "${object_update}" @@ -70,6 +75,8 @@ require_once("for \\(uint16 i = MopUpdateObject::SelfSkillSourceStart" "self skill feed") require_once("for \\(uint16 i = MopUpdateObject::SelfBuybackSourceStart" "self buyback price/timestamp feed") +require_once("for \\(uint16 i = MopUpdateObject::SelfQuestLogSourceStart" + "self quest-log feed") require_once("addIfChanged\\(PLAYER_FIELD_COINAGE\\)" "self coinage low-word feed") require_once("addIfChanged\\(PLAYER_FIELD_COINAGE \\+ 1\\)" diff --git a/src/game/Server/tests/mop_spell_cast_packets_source_test.cmake b/src/game/Server/tests/mop_spell_cast_packets_source_test.cmake index f75844643..4f5eece90 100644 --- a/src/game/Server/tests/mop_spell_cast_packets_source_test.cmake +++ b/src/game/Server/tests/mop_spell_cast_packets_source_test.cmake @@ -2,6 +2,7 @@ file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/Spell.h" spell_header) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/Spell.cpp" spell_source) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/SpellPackets.cpp" spell_packets) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/SpellHandler.cpp" spell_handler) +file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/SpellEffectTail.cpp" spell_effect_tail) file(READ "${SOURCE_ROOT}/src/game/Server/Opcodes.cpp" opcode_registry) file(READ "${SOURCE_ROOT}/src/game/Server/Opcodes.h" opcode_header) file(READ "${SOURCE_ROOT}/src/game/Server/WorldSession.cpp" world_session) @@ -348,6 +349,121 @@ elseif(MUTATION STREQUAL "pet_spellbook_reference") "SMSG_PET_LEARNED_SPELL 0x0282 ACTIVE" "SMSG_PET_LEARNED_SPELL 0x0282 DORMANT" opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "cooldown_event_mask_order") + string(REPLACE + "out.WriteGuidMask<4, 7, 1, 5, 6, 0, 2, 3>(ownerGuid);" + "out.WriteGuidMask<7, 4, 1, 5, 6, 0, 2, 3>(ownerGuid);" + spell_header "${spell_header}") +elseif(MUTATION STREQUAL "cooldown_event_byte_order") + string(REPLACE + "out.WriteGuidBytes<5, 7>(ownerGuid);" + "out.WriteGuidBytes<7, 5>(ownerGuid);" + spell_header "${spell_header}") +elseif(MUTATION STREQUAL "cooldown_event_sender") + string(REPLACE + "MopSpellPackets::BuildCooldownEvent(data, m_owner->GetObjectGuid(), spellInfo->ID);" + "/* removed cooldown-event sender */" + spell_cooldown_mgr_source "${spell_cooldown_mgr_source}") +elseif(MUTATION STREQUAL "cooldown_event_registration") + string(REPLACE + "DefS(SMSG_COOLDOWN_EVENT, \"SMSG_COOLDOWN_EVENT\");" + "/* removed SMSG_COOLDOWN_EVENT registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "cooldown_event_gate") + string(REPLACE + "case SMSG_COOLDOWN_EVENT:" + "case SMSG_UNKNOWN_0:" + world_session "${world_session}") +elseif(MUTATION STREQUAL "cooldown_event_reference") + string(REPLACE + "SMSG_COOLDOWN_EVENT 0x1163 ACTIVE" + "SMSG_COOLDOWN_EVENT 0x1163 DORMANT" + opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "item_cooldown_wire_order") + string(REPLACE + "out << itemGuid << spellId;" + "out << spellId << itemGuid;" + spell_header "${spell_header}") +elseif(MUTATION STREQUAL "item_cooldown_sender") + string(REPLACE + "MopSpellPackets::BuildItemCooldown(data, pItem->GetObjectGuid(), spellData.SpellId);" + "/* removed item cooldown sender */" + player_source "${player_source}") +elseif(MUTATION STREQUAL "item_cooldown_registration") + string(REPLACE + "DefS(SMSG_ITEM_COOLDOWN, \"SMSG_ITEM_COOLDOWN\");" + "/* removed SMSG_ITEM_COOLDOWN registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "item_cooldown_gate") + string(REPLACE + "case SMSG_ITEM_COOLDOWN:" + "case SMSG_UNKNOWN_0:" + world_session "${world_session}") +elseif(MUTATION STREQUAL "item_cooldown_reference") + string(REPLACE + "SMSG_ITEM_COOLDOWN 0x1904 ACTIVE" + "SMSG_ITEM_COOLDOWN 0x1904 DORMANT" + opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "clear_target_mask_order") + string(REPLACE + "out.WriteGuidMask<6, 2, 0, 4, 7, 1, 3, 5>(targetGuid);" + "out.WriteGuidMask<2, 6, 0, 4, 7, 1, 3, 5>(targetGuid);" + spell_header "${spell_header}") +elseif(MUTATION STREQUAL "clear_target_byte_order") + string(REPLACE + "out.WriteGuidBytes<4, 0, 3, 5, 2, 7, 6, 1>(targetGuid);" + "out.WriteGuidBytes<0, 4, 3, 5, 2, 7, 6, 1>(targetGuid);" + spell_header "${spell_header}") +elseif(MUTATION STREQUAL "clear_target_sender") + string(REPLACE + "MopSpellPackets::BuildClearTarget(data, unitTarget->GetObjectGuid());" + "/* removed clear-target sender */" + spell_effect_tail "${spell_effect_tail}") +elseif(MUTATION STREQUAL "clear_target_registration") + string(REPLACE + "DefS(SMSG_CLEAR_TARGET, \"SMSG_CLEAR_TARGET\");" + "/* removed SMSG_CLEAR_TARGET registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "clear_target_gate") + string(REPLACE + "case SMSG_CLEAR_TARGET:" + "case SMSG_UNKNOWN_0:" + world_session "${world_session}") +elseif(MUTATION STREQUAL "clear_target_reference") + string(REPLACE + "SMSG_CLEAR_TARGET 0x1061 ACTIVE" + "SMSG_CLEAR_TARGET 0x1061 DORMANT" + opcode_reference "${opcode_reference}") +elseif(MUTATION STREQUAL "channel_start_mask_order") + string(REPLACE + "out.WriteGuidMask<7, 5, 4, 1>(casterGuid);" + "out.WriteGuidMask<5, 7, 4, 1>(casterGuid);" + spell_header "${spell_header}") +elseif(MUTATION STREQUAL "channel_update_byte_order") + string(REPLACE + "out.WriteGuidBytes<4, 7, 1, 2, 6, 5>(casterGuid);" + "out.WriteGuidBytes<7, 4, 1, 2, 6, 5>(casterGuid);" + spell_header "${spell_header}") +elseif(MUTATION STREQUAL "channel_sender") + string(REPLACE + "MopSpellPackets::BuildChannelStart(" + "/* removed channel-start builder */ (" + spell_packets "${spell_packets}") +elseif(MUTATION STREQUAL "channel_registration") + string(REPLACE + "DefS(SMSG_CHANNEL_START, \"SMSG_CHANNEL_START\");" + "/* removed channel-start registration */" + opcode_registry "${opcode_registry}") +elseif(MUTATION STREQUAL "channel_gate") + string(REPLACE + "case SMSG_CHANNEL_START:" + "case SMSG_UNKNOWN_0:" + world_session "${world_session}") +elseif(MUTATION STREQUAL "channel_reference") + string(REPLACE + "SMSG_CHANNEL_START 0x10F9 ACTIVE" + "SMSG_CHANNEL_START 0x10F9 DORMANT" + opcode_reference "${opcode_reference}") endif() string(FIND "${spell_handler}" "void WorldSession::HandleCastSpellOpcode" cast_start) @@ -660,6 +776,105 @@ foreach(source IN ITEMS "${player_source}" "${spell_cooldown_mgr_source}") forbid("${source}" "WorldPacket data(SMSG_CLEAR_COOLDOWNS" "legacy clear-cooldowns body") endforeach() +require_once("${spell_header}" + "inline void BuildCooldownEvent(WorldPacket& out, ObjectGuid ownerGuid," + "18414 cooldown-event builder") +require_once("${spell_header}" + "out.WriteGuidMask<4, 7, 1, 5, 6, 0, 2, 3>(ownerGuid);" + "cooldown-event GUID mask order") +require_once("${spell_header}" + "out.WriteGuidBytes<5, 7>(ownerGuid);" + "cooldown-event leading GUID bytes") +require_once("${spell_header}" + "out.WriteGuidBytes<3, 1, 2, 4, 6, 0>(ownerGuid);" + "cooldown-event trailing GUID bytes") +require_once("${spell_cooldown_mgr_source}" + "MopSpellPackets::BuildCooldownEvent(data, m_owner->GetObjectGuid(), spellInfo->ID);" + "cooldown-event sender") +require_once("${opcode_registry}" + "DefS(SMSG_COOLDOWN_EVENT, \"SMSG_COOLDOWN_EVENT\");" + "cooldown-event registration") +require_once("${world_session}" + "case SMSG_COOLDOWN_EVENT:" + "cooldown-event send admission") +require_once("${opcode_reference}" + "SMSG_COOLDOWN_EVENT 0x1163 ACTIVE" + "active cooldown-event reference") +forbid("${spell_cooldown_mgr_source}" "WorldPacket data(SMSG_COOLDOWN_EVENT" + "legacy cooldown-event body") +require_once("${spell_header}" + "inline void BuildItemCooldown(WorldPacket& out, ObjectGuid itemGuid," + "18414 item-cooldown builder") +require_once("${spell_header}" + "out << itemGuid << spellId;" + "item-cooldown wire field order") +require_once("${player_source}" + "MopSpellPackets::BuildItemCooldown(data, pItem->GetObjectGuid(), spellData.SpellId);" + "item-cooldown sender") +require_once("${opcode_registry}" + "DefS(SMSG_ITEM_COOLDOWN, \"SMSG_ITEM_COOLDOWN\");" + "item-cooldown registration") +require_once("${world_session}" + "case SMSG_ITEM_COOLDOWN:" + "item-cooldown send admission") +require_once("${opcode_reference}" + "SMSG_ITEM_COOLDOWN 0x1904 ACTIVE" + "active direct-client item-cooldown reference") +forbid("${player_source}" "WorldPacket data(SMSG_ITEM_COOLDOWN" + "legacy item-cooldown body") +require_once("${spell_header}" + "out.WriteGuidMask<6, 2, 0, 4, 7, 1, 3, 5>(targetGuid);" + "clear-target GUID mask order") +require_once("${spell_header}" + "out.WriteGuidBytes<4, 0, 3, 5, 2, 7, 6, 1>(targetGuid);" + "clear-target GUID byte order") +require_once("${spell_effect_tail}" + "MopSpellPackets::BuildClearTarget(data, unitTarget->GetObjectGuid());" + "clear-target sender") +require_once("${opcode_registry}" + "DefS(SMSG_CLEAR_TARGET, \"SMSG_CLEAR_TARGET\");" + "clear-target registration") +require_once("${world_session}" + "case SMSG_CLEAR_TARGET:" + "clear-target send admission") +require_once("${opcode_reference}" + "SMSG_CLEAR_TARGET 0x1061 ACTIVE" + "active direct-client clear-target reference") +forbid("${spell_effect_tail}" "WorldPacket data(SMSG_CLEAR_TARGET" + "legacy clear-target body") +require_once("${spell_header}" + "out.WriteGuidMask<7, 5, 4, 1>(casterGuid);" + "channel-start leading caster mask") +require_once("${spell_header}" + "out.WriteGuidBytes<6, 7, 3, 1, 0>(casterGuid);" + "channel-start leading caster bytes") +require_once("${spell_header}" + "out.WriteGuidMask<0, 3, 4, 1, 5, 2, 6, 7>(casterGuid);" + "channel-update caster mask") +require_once("${spell_header}" + "out.WriteGuidBytes<4, 7, 1, 2, 6, 5>(casterGuid);" + "channel-update leading caster bytes") +foreach(builder IN ITEMS BuildChannelStart BuildChannelUpdate) + require_once("${spell_packets}" + "MopSpellPackets::${builder}(" + "${builder} owning sender") +endforeach() +foreach(opcode IN ITEMS SMSG_CHANNEL_START SMSG_CHANNEL_UPDATE) + require_once("${opcode_registry}" + "DefS(${opcode}, \"${opcode}\");" + "${opcode} registration") + require_once("${world_session}" + "case ${opcode}:" + "${opcode} send admission") + forbid("${spell_packets}" "WorldPacket data(${opcode}" + "legacy ${opcode} body") +endforeach() +require_once("${opcode_reference}" + "SMSG_CHANNEL_START 0x10F9 ACTIVE" + "active channel-start reference") +require_once("${opcode_reference}" + "SMSG_CHANNEL_UPDATE 0x11D9 ACTIVE" + "active channel-update reference") require_once("${spell_header}" "inline void BuildLearnedSpell(WorldPacket& out, uint32 spellId," "18414 learned-spell builder") diff --git a/src/game/Server/tests/mop_spell_cast_packets_test.cpp b/src/game/Server/tests/mop_spell_cast_packets_test.cpp index 7c0d08ef4..45950275e 100644 --- a/src/game/Server/tests/mop_spell_cast_packets_test.cpp +++ b/src/game/Server/tests/mop_spell_cast_packets_test.cpp @@ -39,7 +39,18 @@ namespace size_t index = 0; for (uint8 byte : expected) + { + uint8 actual = packet.contents()[index]; + if (actual != byte) + { + std::fprintf(stderr, "byte %zu: actual 0x%02X expected 0x%02X\n", index, actual, byte); + std::fprintf(stderr, "actual packet:"); + for (size_t i = 0; i < packet.size(); ++i) + std::fprintf(stderr, " 0x%02X", packet.contents()[i]); + std::fprintf(stderr, "\n"); + } CHECK(packet.contents()[index++] == byte); + } } struct MovementFixture @@ -1088,6 +1099,95 @@ static void test_pet_spellbook_mutation_wire_layouts() }); } +static void test_cooldown_event_wire_layout() +{ + WorldPacket dense; + MopSpellPackets::BuildCooldownEvent(dense, + ObjectGuid(UINT64_C(0x0807060504030201)), 0x11223344u); + CHECK(dense.GetOpcode() == SMSG_COOLDOWN_EVENT); + CheckBytes(dense, { + 0xFF, + 0x07, 0x09, + 0x44, 0x33, 0x22, 0x11, + 0x05, 0x03, 0x02, 0x04, 0x06, 0x00, + }); + + WorldPacket sparse; + MopSpellPackets::BuildCooldownEvent(sparse, + ObjectGuid(UINT64_C(0x08BB0000000000AA)), 0xA1B2C3D4u); + CHECK(sparse.GetOpcode() == SMSG_COOLDOWN_EVENT); + CheckBytes(sparse, { + 0x4C, + 0x09, + 0xD4, 0xC3, 0xB2, 0xA1, + 0xBA, 0xAB, + }); +} + +static void test_item_cooldown_wire_layout() +{ + WorldPacket packet; + MopSpellPackets::BuildItemCooldown(packet, + ObjectGuid(UINT64_C(0x0807060504030201)), 0x11223344u); + CHECK(packet.GetOpcode() == SMSG_ITEM_COOLDOWN); + CheckBytes(packet, { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x44, 0x33, 0x22, 0x11, + }); +} + +static void test_clear_target_wire_layout() +{ + WorldPacket packet; + MopSpellPackets::BuildClearTarget(packet, + ObjectGuid(UINT64_C(0x0807060504030201))); + CHECK(packet.GetOpcode() == SMSG_CLEAR_TARGET); + CheckBytes(packet, { + 0xFF, + 0x04, 0x00, 0x05, 0x07, 0x02, 0x09, 0x06, 0x03, + }); +} + +static void test_spell_channel_wire_layouts() +{ + ObjectGuid const denseGuid(UINT64_C(0x0807060504030201)); + + WorldPacket start; + MopSpellPackets::BuildChannelStart( + start, denseGuid, 0x11223344u, 0x55667788u); + CHECK(start.GetOpcode() == SMSG_CHANNEL_START); + CheckBytes(start, { + 0xF7, 0x80, + 0x06, 0x09, 0x05, 0x03, 0x00, + 0x88, 0x77, 0x66, 0x55, + 0x07, 0x04, 0x02, + 0x44, 0x33, 0x22, 0x11, + }); + + WorldPacket update; + MopSpellPackets::BuildChannelUpdate(update, denseGuid, 0x99AABBCCu); + CHECK(update.GetOpcode() == SMSG_CHANNEL_UPDATE); + CheckBytes(update, { + 0xFF, + 0x04, 0x09, 0x03, 0x02, 0x06, 0x07, + 0xCC, 0xBB, 0xAA, 0x99, + 0x00, 0x05, + }); + + WorldPacket sparseStart; + MopSpellPackets::BuildChannelStart( + sparseStart, ObjectGuid(), 0x01020304u, 0x05060708u); + CheckBytes(sparseStart, { + 0x00, 0x00, + 0x08, 0x07, 0x06, 0x05, + 0x04, 0x03, 0x02, 0x01, + }); + + WorldPacket sparseUpdate; + MopSpellPackets::BuildChannelUpdate(sparseUpdate, ObjectGuid(), 0); + CheckBytes(sparseUpdate, { 0x00, 0x00, 0x00, 0x00, 0x00 }); +} + int main(int, char**) { test_dense_and_guid_permutations(); @@ -1105,6 +1205,10 @@ int main(int, char**) test_category_cooldown_request_and_response(); test_spell_cooldown_wire_layout(); test_clear_cooldowns_wire_layout(); + test_cooldown_event_wire_layout(); + test_item_cooldown_wire_layout(); + test_clear_target_wire_layout(); + test_spell_channel_wire_layouts(); test_spellbook_mutation_wire_layouts(); test_pet_spellbook_mutation_wire_layouts(); diff --git a/src/game/Server/tests/mop_updateobject_test.cpp b/src/game/Server/tests/mop_updateobject_test.cpp index f2720e77c..e4fc7a958 100644 --- a/src/game/Server/tests/mop_updateobject_test.cpp +++ b/src/game/Server/tests/mop_updateobject_test.cpp @@ -450,6 +450,61 @@ int main(int /*argc*/, char** /*argv*/) CHECK(values.rpos() == values.size()); } + // The quest log is the one self range whose slot stride differs between + // Four and 18414. Four stores fifty five-word slots at 166..415; the + // client's CGPlayerData::questLog is fifty FIFTEEN-word slots at + // 171..920 (750 fields, binary-confirmed). Only the leading five words of + // each client slot carry Four's id/state/counts/timer, so the projection + // must re-stride per slot rather than copy the range flat. A flat copy + // would land legacy slot 1 on 176 instead of 186, inside client slot 0. + { + const MopUpdateObject::StaticField sourceFields[] = + { + { 166, 0x000004D2u }, // slot 0 quest id -> 171 + { 170, 0x00000E10u }, // slot 0 timer -> 175 + { 171, 0x000004D3u }, // slot 1 quest id -> 186 + { 415, 0u }, // slot 49 timer -> 910 + }; + ByteBuffer values; + MopUpdateObject::AppendSelfPlayerValuesBlock(values, 0x10, sourceFields, + sizeof(sourceFields) / sizeof(sourceFields[0])); + values.rpos(3); // VALUES + packed GUID + uint8 blockCount; + values >> blockCount; + CHECK(blockCount == 29); + uint32 masks[29]; + for (uint32& mask : masks) values >> mask; + auto hasBit = [&masks](uint16 index) + { + return (masks[index / 32] & (uint32(1) << (index % 32))) != 0; + }; + for (uint16 index : { uint16(171), uint16(175), uint16(186), uint16(910) }) + CHECK(hasBit(index)); + // the flat-copy targets, which must NOT be produced + CHECK(!hasBit(176)); + CHECK(!hasBit(420)); + // and the untranslated legacy indices + CHECK(!hasBit(166)); + CHECK(!hasBit(415)); + + for (uint32 expectedValue : { 0x000004D2u, 0x00000E10u, 0x000004D3u, 0u }) + { + uint32 actualValue; + values >> actualValue; + CHECK(actualValue == expectedValue); + } + uint8 dynamicCount; + values >> dynamicCount; + CHECK(dynamicCount == 0); + CHECK(values.rpos() == values.size()); + } + + // Per-slot re-striding, checked directly at both ends of the block. + CHECK(MopUpdateObject::TranslateSelfQuestLogIndex(166) == 171); + CHECK(MopUpdateObject::TranslateSelfQuestLogIndex(170) == 175); + CHECK(MopUpdateObject::TranslateSelfQuestLogIndex(171) == 186); + CHECK(MopUpdateObject::TranslateSelfQuestLogIndex(415) == 910); + CHECK(MopUpdateObject::RepackUnitBytes0(0x04030201u) == 0x03040201u); CHECK(MopUpdateObject::TranslateUnitDynamicFlags(0x000000A5u) == 0x0000014Au); CHECK(MopUpdateObject::TranslateUnitDynamicFlags(0xFFFF01A5u) == 0x0000014Au); diff --git a/src/game/Server/tests/mop_world_quest_interaction_packets_source_test.cmake b/src/game/Server/tests/mop_world_quest_interaction_packets_source_test.cmake index 78752d3a6..683768ebc 100644 --- a/src/game/Server/tests/mop_world_quest_interaction_packets_source_test.cmake +++ b/src/game/Server/tests/mop_world_quest_interaction_packets_source_test.cmake @@ -5,6 +5,8 @@ endif() file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/MiscHandler.cpp" misc_handler) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/QuestHandler.cpp" quest_handler) file(READ "${SOURCE_ROOT}/src/game/Object/PlayerQuest.cpp" player_quest) +file(READ "${SOURCE_ROOT}/src/game/Object/Player.h" player_header) +file(READ "${SOURCE_ROOT}/src/game/Object/Player.cpp" player_source) file(READ "${SOURCE_ROOT}/src/game/Object/ObjectMgrText.cpp" object_mgr_text) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/NPCHandler.h" npc_handler) file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/QueryHandler.cpp" query_handler) @@ -95,14 +97,39 @@ if(DEFINED MUTATION) gossip_def "${gossip_def}") elseif(MUTATION STREQUAL "npc_db_content") string(REPLACE - "WORLD_DB_CONTENT_NR \"2\"" - "WORLD_DB_CONTENT_NR \"1\"" + "WORLD_DB_CONTENT_NR \"31\"" + "WORLD_DB_CONTENT_NR \"30\"" revision_data "${revision_data}") elseif(MUTATION STREQUAL "npc_reference_status") string(REPLACE "CMSG_NPC_TEXT_QUERY 0x0287 ACTIVE" "CMSG_NPC_TEXT_QUERY 0x0287 DORMANT" opcode_reference "${opcode_reference}") + elseif(MUTATION STREQUAL "exploration_wire_order") + string(REPLACE + "out << areaId << experience;" + "out << experience << areaId;" + player_header "${player_header}") + elseif(MUTATION STREQUAL "exploration_sender") + string(REPLACE + "MopAreaTriggerPackets::BuildExplorationExperience(data, Area, Experience);" + "/* removed exploration-experience sender */" + player_source "${player_source}") + elseif(MUTATION STREQUAL "exploration_registration") + string(REPLACE + "DefS(SMSG_EXPLORATION_EXPERIENCE, \"SMSG_EXPLORATION_EXPERIENCE\");" + "/* removed exploration-experience registration */" + opcode_registry "${opcode_registry}") + elseif(MUTATION STREQUAL "exploration_admission") + string(REPLACE + "case SMSG_EXPLORATION_EXPERIENCE:" + "/* removed exploration-experience admission */" + world_session "${world_session}") + elseif(MUTATION STREQUAL "exploration_reference") + string(REPLACE + "SMSG_EXPLORATION_EXPERIENCE 0x189A ACTIVE" + "SMSG_EXPLORATION_EXPERIENCE 0x189A DORMANT" + opcode_reference "${opcode_reference}") else() message(FATAL_ERROR "unknown MUTATION=${MUTATION}") endif() @@ -125,6 +152,24 @@ require_once("${misc_handler}" require_once("${misc_handler}" "MopAreaTriggerPackets::BuildNoCorpse\\(data\\)" "no-corpse response builder") +require_once("${player_header}" + "out << areaId << experience" + "exploration-experience wire order") +require_once("${player_source}" + "MopAreaTriggerPackets::BuildExplorationExperience\\(data, Area, Experience\\)" + "exploration-experience sender") +require_once("${opcode_registry}" + "DefS\\(SMSG_EXPLORATION_EXPERIENCE,[ \t]*\"SMSG_EXPLORATION_EXPERIENCE\"\\)" + "exploration-experience registration") +require_once("${world_session}" + "case[ \t]+SMSG_EXPLORATION_EXPERIENCE:" + "exploration-experience admission") +require_once("${opcode_reference}" + "SMSG_EXPLORATION_EXPERIENCE[ \t]+0x189A[ \t]+ACTIVE" + "active direct-client exploration reference") +if("${player_source}" MATCHES "WorldPacket[ \t]+[A-Za-z0-9_]+\\(SMSG_EXPLORATION_EXPERIENCE") + message(FATAL_ERROR "legacy inline exploration-experience sender remains") +endif() require_once("${quest_handler}" "MopQuestStatusPackets::ParseMultipleStatusQuery\\(recvPacket\\)" "empty quest status query parser") @@ -216,10 +261,14 @@ foreach(reference IN ITEMS endif() endforeach() +# Tracks the tuple the server actually declares in revision_data.h.in. The +# requirement is that the world database be BroadcastText-aware, which first +# held at 23.1.2; the declared tuple has since advanced and this pin must move +# with it or the check silently asserts an obsolete database. foreach(requirement IN ITEMS "WORLD_DB_VERSION_NR[ \t]+\"23\"" - "WORLD_DB_STRUCTURE_NR[ \t]+\"1\"" - "WORLD_DB_CONTENT_NR[ \t]+\"2\"") + "WORLD_DB_STRUCTURE_NR[ \t]+\"2\"" + "WORLD_DB_CONTENT_NR[ \t]+\"31\"") require_once("${revision_data}" "${requirement}" "BroadcastText-aware world database requirement") diff --git a/src/game/Server/tests/mop_world_quest_interaction_packets_test.cpp b/src/game/Server/tests/mop_world_quest_interaction_packets_test.cpp index 01facdbd4..3e07e120c 100644 --- a/src/game/Server/tests/mop_world_quest_interaction_packets_test.cpp +++ b/src/game/Server/tests/mop_world_quest_interaction_packets_test.cpp @@ -97,6 +97,18 @@ static void TestAreaTriggerNoCorpse() CHECK(packet.empty()); } +static void TestExplorationExperience() +{ + WorldPacket packet; + MopAreaTriggerPackets::BuildExplorationExperience(packet, + 0x11223344u, 0xA1B2C3D4u); + CHECK(packet.GetOpcode() == SMSG_EXPLORATION_EXPERIENCE); + CheckBytes(packet, { + 0x44, 0x33, 0x22, 0x11, + 0xD4, 0xC3, 0xB2, 0xA1, + }); +} + static void TestQuestgiverStatusMultipleRequest() { WorldPacket empty(CMSG_QUESTGIVER_STATUS_MULTIPLE_QUERY, 0); @@ -228,6 +240,7 @@ static void TestOpcodeValues() { CHECK(uint32_t(CMSG_AREATRIGGER) == 0x1C44u); CHECK(uint32_t(SMSG_AREA_TRIGGER_NO_CORPSE) == 0x089Eu); + CHECK(uint32_t(SMSG_EXPLORATION_EXPERIENCE) == 0x189Au); CHECK(uint32_t(CMSG_NPC_TEXT_QUERY) == 0x0287u); CHECK(uint32_t(SMSG_NPC_TEXT_UPDATE) == 0x140Au); CHECK(uint32_t(CMSG_QUESTGIVER_STATUS_MULTIPLE_QUERY) == 0x02F1u); @@ -238,6 +251,7 @@ int main(int /*argc*/, char** /*argv*/) { TestAreaTriggerRequest(); TestAreaTriggerNoCorpse(); + TestExplorationExperience(); TestQuestgiverStatusMultipleRequest(); TestQuestgiverStatusMultipleResponse(); TestNpcTextRequest(); diff --git a/src/game/WorldHandlers/Chat.h b/src/game/WorldHandlers/Chat.h index 30a0315d8..823aa4285 100644 --- a/src/game/WorldHandlers/Chat.h +++ b/src/game/WorldHandlers/Chat.h @@ -106,6 +106,41 @@ typedef uint32 ChatTagFlags; namespace MopChatPackets { + inline bool BuildPlayerNameNotice(WorldPacket& out, uint16 opcode, + std::string const& name) + { + if (name.size() >= (size_t(1) << 9)) + return false; + + out.Initialize(opcode, name.size() + 2); + + // Wow.exe 18414 reader sub_70E692 combines one aligned byte and one + // MSB-first bit into a 9-bit length, then consumes raw bytes without + // a wire terminator. + out.WriteBits(name.size(), 9); + out.FlushBits(); + out.append(name.data(), name.size()); + return true; + } + + inline bool BuildPlayerNotFound(WorldPacket& out, std::string const& name) + { + return BuildPlayerNameNotice(out, SMSG_CHAT_PLAYER_NOT_FOUND, name); + } + + inline bool BuildPlayerAmbiguous(WorldPacket& out, std::string const& name) + { + return BuildPlayerNameNotice(out, SMSG_CHAT_PLAYER_AMBIGUOUS, name); + } + + inline void BuildChatRestrictedNotice(WorldPacket& out, uint8 restriction) + { + // Reader sub_6BB6E9 consumes exactly this byte; terminal sub_7AA5B8 + // maps values 0 through 3 to the four restricted-chat UI errors. + out.Initialize(SMSG_CHAT_RESTRICTED, 1); + out << restriction; + } + /** * Reads the message portion of an 18414 CMSG_MESSAGECHAT_SAY request. * diff --git a/src/game/WorldHandlers/ChatHandler.cpp b/src/game/WorldHandlers/ChatHandler.cpp index 156ddbf59..162d9417d 100644 --- a/src/game/WorldHandlers/ChatHandler.cpp +++ b/src/game/WorldHandlers/ChatHandler.cpp @@ -1193,15 +1193,17 @@ void WorldSession::HandleChatIgnoredOpcode(WorldPacket& recv_data) */ void WorldSession::SendPlayerNotFoundNotice(const std::string& name) { - WorldPacket data(SMSG_CHAT_PLAYER_NOT_FOUND, name.size() + 1); - data << name; + WorldPacket data; + if (!MopChatPackets::BuildPlayerNotFound(data, name)) + return; SendPacket(&data); } void WorldSession::SendPlayerAmbiguousNotice(const std::string& name) { - WorldPacket data(SMSG_CHAT_PLAYER_AMBIGUOUS, name.size() + 1); - data << name; + WorldPacket data; + if (!MopChatPackets::BuildPlayerAmbiguous(data, name)) + return; SendPacket(&data); } @@ -1210,7 +1212,7 @@ void WorldSession::SendPlayerAmbiguousNotice(const std::string& name) */ void WorldSession::SendChatRestrictedNotice(ChatRestrictionType restriction) { - WorldPacket data(SMSG_CHAT_RESTRICTED, 1); - data << uint8(restriction); + WorldPacket data; + MopChatPackets::BuildChatRestrictedNotice(data, uint8(restriction)); SendPacket(&data); } diff --git a/src/game/WorldHandlers/QueryHandler.cpp b/src/game/WorldHandlers/QueryHandler.cpp index 6f573b387..a6e2f24ca 100644 --- a/src/game/WorldHandlers/QueryHandler.cpp +++ b/src/game/WorldHandlers/QueryHandler.cpp @@ -548,6 +548,42 @@ void WorldSession::HandleCorpseMapPositionQueryOpcode(WorldPacket& recv_data) SendPacket(&data); } +void WorldSession::HandleQuestNpcQueryOpcode(WorldPacket& recv_data) +{ + uint32 questId = 0; + if (!MopQueryPackets::ParseQuestNpcQueryRequest(recv_data, questId)) + { + DEBUG_LOG("WORLD: Rejected malformed CMSG_QUEST_NPC_QUERY"); + return; + } + + // The client asks one quest at a time; retail batches several quests into + // a single reply, so a one-entry response is the degenerate case of the + // same grammar rather than a different shape. + MopQueryPackets::QuestNpcResponse entry; + entry.questId = questId; + + // Answer with the quest's ENDER creatures. Retail 18414 captures settle + // the rule: quest 28508 is a chain hand-off with disjoint sets and the + // retail server returned its ender (44452), never its giver (42898). + // Every other quest in the corpus has one creature in both roles, so no + // other hypothesis survives 28508. + QuestRelationsMapBounds const bounds = + sObjectMgr.GetQuestEnderCreaturesMapBounds(questId); + for (QuestRelationsMap::const_iterator itr = bounds.first; itr != bounds.second; ++itr) + { + entry.npcIds.push_back(itr->second); + } + + WorldPacket data; + if (!MopQueryPackets::BuildQuestNpcQueryResponse(data, { entry })) + { + return; + } + + SendPacket(&data); +} + void WorldSession::HandleQuestPOIQueryOpcode(WorldPacket& recv_data) { std::vector questIds; diff --git a/src/game/WorldHandlers/Spell.h b/src/game/WorldHandlers/Spell.h index 6c495077b..1b6b450f8 100644 --- a/src/game/WorldHandlers/Spell.h +++ b/src/game/WorldHandlers/Spell.h @@ -376,6 +376,75 @@ namespace MopSpellPackets out.WriteGuidBytes<2>(ownerGuid); } + inline void BuildCooldownEvent(WorldPacket& out, ObjectGuid ownerGuid, + uint32 spellId) + { + out.Initialize(SMSG_COOLDOWN_EVENT, 13); + + // Wow.exe 18414 reader sub_6D511A reconstructs the owner GUID around the + // spell ID; resolved terminal sub_77AECB applies its cooldown event. + out.WriteGuidMask<4, 7, 1, 5, 6, 0, 2, 3>(ownerGuid); + out.FlushBits(); + out.WriteGuidBytes<5, 7>(ownerGuid); + out << spellId; + out.WriteGuidBytes<3, 1, 2, 4, 6, 0>(ownerGuid); + } + + inline void BuildItemCooldown(WorldPacket& out, ObjectGuid itemGuid, + uint32 spellId) + { + out.Initialize(SMSG_ITEM_COOLDOWN, 12); + + // Dynamic 18414 handler sub_77D70B reads a raw uint64 item GUID and + // then a uint32 spell ID before installing the item's 30-second lock. + out << itemGuid << spellId; + } + + inline void BuildClearTarget(WorldPacket& out, ObjectGuid targetGuid) + { + out.Initialize(SMSG_CLEAR_TARGET, 9); + + // Wow.exe 18414 reader sub_6D4AFB reconstructs this packed GUID; + // terminal sub_85876E forwards it to the client target-clear routine. + out.WriteGuidMask<6, 2, 0, 4, 7, 1, 3, 5>(targetGuid); + out.FlushBits(); + out.WriteGuidBytes<4, 0, 3, 5, 2, 7, 6, 1>(targetGuid); + } + + inline void BuildChannelStart(WorldPacket& out, ObjectGuid casterGuid, + uint32 spellId, uint32 durationMs) + { + out.Initialize(SMSG_CHANNEL_START, 18); + + // Wow.exe 18414 reader sub_C6AFEC brackets the caster GUID with + // optional target and auxiliary-data bits. The current server path + // has neither optional structure, so both presence bits stay clear. + out.WriteGuidMask<7, 5, 4, 1>(casterGuid); + out.WriteBit(false); + out.WriteGuidMask<3, 2, 0, 6>(casterGuid); + out.WriteBit(false); + out.FlushBits(); + + out.WriteGuidBytes<6, 7, 3, 1, 0>(casterGuid); + out << durationMs; + out.WriteGuidBytes<5, 4, 2>(casterGuid); + out << spellId; + } + + inline void BuildChannelUpdate(WorldPacket& out, ObjectGuid casterGuid, + uint32 durationMs) + { + out.Initialize(SMSG_CHANNEL_UPDATE, 13); + + // Reader sub_C6915A reconstructs the caster before consuming the + // remaining duration; zero duration drives the channel-stop path. + out.WriteGuidMask<0, 3, 4, 1, 5, 2, 6, 7>(casterGuid); + out.FlushBits(); + out.WriteGuidBytes<4, 7, 1, 2, 6, 5>(casterGuid); + out << durationMs; + out.WriteGuidBytes<0, 3>(casterGuid); + } + inline void BuildLearnedSpell(WorldPacket& out, uint32 spellId, bool suppressMessaging) { @@ -1448,6 +1517,236 @@ namespace MopCombatLogPackets uint32 interruptedSpellId; }; + struct SpellInstakillLog + { + uint64 casterGuid; + uint64 victimGuid; + uint32 spellId; + }; + + struct SpellEnergizeLog + { + uint64 targetGuid; + uint64 casterGuid; + uint32 amount; + uint32 spellId; + uint32 powerType; + }; + + struct SpellHealLog + { + uint64 casterGuid; + uint64 targetGuid; + uint32 spellId; + uint32 heal; + uint32 overheal; + uint32 absorb; + bool critical; + }; + + struct SpellDamageShieldLog + { + uint64 casterGuid; + uint64 targetGuid; + uint32 spellId; + uint32 damage; + uint32 overkill; + uint32 schoolMask; + uint32 resist; + }; + + struct SpellMissTarget + { + uint64 guid; + uint8 missReason; + }; + + struct SpellMissLog + { + uint64 casterGuid; + uint32 spellId; + SpellMissTarget const* targets; + size_t targetCount; + }; + + inline void BuildSpellInstakillLog(WorldPacket& out, SpellInstakillLog const& log) + { + // The 18414 reader interleaves both packed-GUID masks before consuming + // their XOR bytes and the spell ID. + uint8 const casterMaskA[] = { 6, 7, 3, 1, 2, 0, 4 }; + uint8 const victimMaskA[] = { 0, 2 }; + uint8 const victimMaskB[] = { 4, 7, 1, 6, 5 }; + + out.WriteBit(GuidByte(log.casterGuid, casterMaskA[0]) != 0); + out.WriteBit(GuidByte(log.victimGuid, victimMaskA[0]) != 0); + out.WriteBit(GuidByte(log.casterGuid, casterMaskA[1]) != 0); + out.WriteBit(GuidByte(log.victimGuid, victimMaskA[1]) != 0); + for (size_t i = 2; i < sizeof(casterMaskA); ++i) + out.WriteBit(GuidByte(log.casterGuid, casterMaskA[i]) != 0); + WriteGuidMask(out, log.victimGuid, victimMaskB); + out.WriteBit(GuidByte(log.casterGuid, 5) != 0); + out.WriteBit(GuidByte(log.victimGuid, 3) != 0); + out.FlushBits(); + + out.WriteByteSeq(GuidByte(log.casterGuid, 0)); + out.WriteByteSeq(GuidByte(log.victimGuid, 1)); + out.WriteByteSeq(GuidByte(log.casterGuid, 3)); + out.WriteByteSeq(GuidByte(log.casterGuid, 4)); + out.WriteByteSeq(GuidByte(log.casterGuid, 5)); + out.WriteByteSeq(GuidByte(log.casterGuid, 7)); + out.WriteByteSeq(GuidByte(log.victimGuid, 0)); + out.WriteByteSeq(GuidByte(log.casterGuid, 6)); + out.WriteByteSeq(GuidByte(log.victimGuid, 2)); + out.WriteByteSeq(GuidByte(log.victimGuid, 4)); + out.WriteByteSeq(GuidByte(log.casterGuid, 1)); + out << uint32(log.spellId); + uint8 const victimBytes[] = { 3, 7, 6, 5 }; + out.WriteByteSeq(GuidByte(log.victimGuid, victimBytes[0])); + out.WriteByteSeq(GuidByte(log.casterGuid, 2)); + for (size_t i = 1; i < sizeof(victimBytes); ++i) + out.WriteByteSeq(GuidByte(log.victimGuid, victimBytes[i])); + } + + inline void BuildSpellEnergizeLog(WorldPacket& out, SpellEnergizeLog const& log) + { + // The optional bit controls a client cast-log extension that the + // server's energize event does not populate. + uint8 const mask[][2] = { + { 7, 0 }, { 3, 0 }, { 1, 1 }, { 4, 0 }, { 2, 0 }, { 3, 1 }, { 5, 0 }, + { 7, 1 }, { 0, 1 }, { 2, 1 }, { 4, 1 }, { 6, 1 }, { 6, 0 }, { 1, 0 }, + { 0, 0 }, { 5, 1 } + }; + for (size_t i = 0; i < 7; ++i) + out.WriteBit(GuidByte(mask[i][1] ? log.casterGuid : log.targetGuid, mask[i][0]) != 0); + out.WriteBit(false); + for (size_t i = 7; i < 16; ++i) + out.WriteBit(GuidByte(mask[i][1] ? log.casterGuid : log.targetGuid, mask[i][0]) != 0); + out.FlushBits(); + + uint8 const bytesA[][2] = { + { 0, 0 }, { 5, 1 }, { 6, 0 }, { 6, 1 }, { 2, 0 }, { 0, 1 }, { 1, 0 } + }; + for (auto const& byte : bytesA) + out.WriteByteSeq(GuidByte(byte[1] ? log.casterGuid : log.targetGuid, byte[0])); + out << uint32(log.amount); + uint8 const bytesB[][2] = { + { 4, 0 }, { 1, 1 }, { 7, 1 }, { 5, 0 }, { 2, 1 }, { 3, 1 }, + { 7, 0 }, { 4, 1 }, { 3, 0 } + }; + for (auto const& byte : bytesB) + out.WriteByteSeq(GuidByte(byte[1] ? log.casterGuid : log.targetGuid, byte[0])); + out << uint32(log.spellId); + out << uint32(log.powerType); + } + + inline void BuildSpellHealLog(WorldPacket& out, SpellHealLog const& log) + { + // The reader consumes the four fixed scalars before its packed-GUID + // bit section. The three false bits omit unsupported cast-log/floats. + out << uint32(log.spellId); + out << uint32(log.absorb); + out << uint32(log.heal); + out << uint32(log.overheal); + + uint8 const mask[][2] = { + { 0, 1 }, { 2, 0 }, { 6, 0 }, { 2, 1 }, { 3, 0 }, { 0, 0 }, { 5, 0 }, + { 3, 1 }, { 7, 1 }, { 5, 1 }, { 7, 0 }, { 4, 1 }, { 4, 0 }, { 1, 0 }, + { 1, 1 }, { 6, 1 } + }; + for (size_t i = 0; i < 4; ++i) + out.WriteBit(GuidByte(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]) != 0); + out.WriteBit(log.critical); + for (size_t i = 4; i < 8; ++i) + out.WriteBit(GuidByte(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]) != 0); + out.WriteBit(false); + for (size_t i = 8; i < 12; ++i) + out.WriteBit(GuidByte(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]) != 0); + out.WriteBit(false); + out.WriteBit(false); + for (size_t i = 12; i < 16; ++i) + out.WriteBit(GuidByte(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]) != 0); + out.FlushBits(); + + uint8 const bytes[][2] = { + { 2, 0 }, { 6, 1 }, { 5, 0 }, { 3, 0 }, { 7, 1 }, { 7, 0 }, + { 6, 0 }, { 1, 0 }, { 2, 1 }, { 4, 1 }, { 3, 1 }, { 0, 1 }, + { 5, 1 }, { 0, 0 }, { 1, 1 }, { 4, 0 } + }; + for (auto const& byte : bytes) + out.WriteByteSeq(GuidByte(byte[1] ? log.targetGuid : log.casterGuid, byte[0])); + } + + inline void BuildSpellDamageShieldLog(WorldPacket& out, SpellDamageShieldLog const& log) + { + // The false bit omits the optional client cast-log extension. All + // damage-shield scalars below are mandatory in the 18414 reader. + uint8 const mask[][2] = { + { 1, 1 }, { 2, 0 }, { 6, 0 }, { 3, 1 }, { 4, 0 }, { 2, 1 }, + { 5, 1 }, { 6, 1 }, { 3, 0 }, { 0, 1 }, { 5, 0 }, { 1, 0 }, + { 0, 0 }, { 7, 1 }, { 4, 1 }, { 7, 0 } + }; + for (size_t i = 0; i < 2; ++i) + out.WriteBit(GuidByte(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]) != 0); + out.WriteBit(false); + for (size_t i = 2; i < 16; ++i) + out.WriteBit(GuidByte(mask[i][1] ? log.targetGuid : log.casterGuid, mask[i][0]) != 0); + out.FlushBits(); + + out.WriteByteSeq(GuidByte(log.targetGuid, 2)); + out.WriteByteSeq(GuidByte(log.casterGuid, 6)); + out.WriteByteSeq(GuidByte(log.targetGuid, 6)); + out.WriteByteSeq(GuidByte(log.targetGuid, 4)); + out.WriteByteSeq(GuidByte(log.casterGuid, 3)); + out.WriteByteSeq(GuidByte(log.targetGuid, 7)); + out << uint32(log.resist); + out.WriteByteSeq(GuidByte(log.casterGuid, 4)); + out.WriteByteSeq(GuidByte(log.targetGuid, 1)); + out << uint32(log.damage); + out.WriteByteSeq(GuidByte(log.casterGuid, 7)); + out << uint32(log.spellId); + out << uint32(log.overkill); + out.WriteByteSeq(GuidByte(log.targetGuid, 5)); + out.WriteByteSeq(GuidByte(log.casterGuid, 5)); + out.WriteByteSeq(GuidByte(log.targetGuid, 0)); + out.WriteByteSeq(GuidByte(log.casterGuid, 1)); + out.WriteByteSeq(GuidByte(log.casterGuid, 0)); + out.WriteByteSeq(GuidByte(log.casterGuid, 2)); + out << uint32(log.schoolMask); + out.WriteByteSeq(GuidByte(log.targetGuid, 3)); + } + + inline bool BuildSpellMissLog(WorldPacket& out, SpellMissLog const& log) + { + if (log.targetCount >= (size_t(1) << 23) || (log.targetCount && !log.targets)) + return false; + + uint8 const casterMask[] = { 5, 1, 4, 0, 7, 3, 2, 6 }; + uint8 const targetMask[] = { 0, 1, 6, 2, 5, 3, 4, 7 }; + WriteGuidMask(out, log.casterGuid, casterMask); + out.WriteBits(uint32(log.targetCount), 23); + for (size_t i = 0; i < log.targetCount; ++i) + { + WriteGuidMask(out, log.targets[i].guid, targetMask); + // The client can carry two optional target-position floats; this + // server event identifies targets by GUID only. + out.WriteBit(false); + } + out.FlushBits(); + + uint8 const targetBytes[] = { 7, 5, 0, 6, 3, 2, 1, 4 }; + for (size_t i = 0; i < log.targetCount; ++i) + { + out << uint8(log.targets[i].missReason); + WriteGuidBytes(out, log.targets[i].guid, targetBytes); + } + uint8 const casterBytesA[] = { 6, 4, 2, 0, 1 }; + uint8 const casterBytesB[] = { 3, 7, 5 }; + WriteGuidBytes(out, log.casterGuid, casterBytesA); + out << uint32(log.spellId); + WriteGuidBytes(out, log.casterGuid, casterBytesB); + return true; + } + inline bool BuildSpellExecuteLog(WorldPacket& out, SpellExecuteLog const& log) { bool const extraAttacks = log.kind == ExecuteKind::ExtraAttacks; diff --git a/src/game/WorldHandlers/SpellEffectDamageTeleport.cpp b/src/game/WorldHandlers/SpellEffectDamageTeleport.cpp index 889bca784..bbf03451b 100644 --- a/src/game/WorldHandlers/SpellEffectDamageTeleport.cpp +++ b/src/game/WorldHandlers/SpellEffectDamageTeleport.cpp @@ -127,10 +127,12 @@ void Spell::EffectInstaKill(SpellEffectEntry const* /*effect*/) WorldObject* caster = GetCastingObject(); // we need the original casting object - WorldPacket data(SMSG_SPELLINSTAKILLLOG, (8 + 8 + 4)); - data << (caster && caster->GetTypeId() != TYPEID_GAMEOBJECT ? m_caster->GetObjectGuid() : ObjectGuid()); // Caster GUID - data << unitTarget->GetObjectGuid(); // Victim GUID - data << uint32(m_spellInfo->ID); + MopCombatLogPackets::SpellInstakillLog log = {}; + log.casterGuid = (caster && caster->GetTypeId() != TYPEID_GAMEOBJECT ? m_caster->GetObjectGuid() : ObjectGuid()).GetRawValue(); + log.victimGuid = unitTarget->GetObjectGuid().GetRawValue(); + log.spellId = m_spellInfo->ID; + WorldPacket data(SMSG_SPELLINSTAKILLLOG, 20); + MopCombatLogPackets::BuildSpellInstakillLog(data, log); m_caster->SendMessageToSet(&data, true); m_caster->DealDamage(unitTarget, unitTarget->GetHealth(), NULL, DIRECT_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, m_spellInfo, false); diff --git a/src/game/WorldHandlers/SpellEffectObjectCombat.cpp b/src/game/WorldHandlers/SpellEffectObjectCombat.cpp index e72603d66..9a0329856 100644 --- a/src/game/WorldHandlers/SpellEffectObjectCombat.cpp +++ b/src/game/WorldHandlers/SpellEffectObjectCombat.cpp @@ -360,9 +360,9 @@ void Spell::EffectDuel(SpellEffectEntry const* effect) // END // Send request - WorldPacket data(SMSG_DUEL_REQUESTED, 8 + 8); - data << pGameObj->GetObjectGuid(); - data << caster->GetObjectGuid(); + WorldPacket data; + MopDuelPackets::BuildRequested( + data, pGameObj->GetObjectGuid(), caster->GetObjectGuid()); caster->GetSession()->SendPacket(&data); target->GetSession()->SendPacket(&data); diff --git a/src/game/WorldHandlers/SpellEffectTail.cpp b/src/game/WorldHandlers/SpellEffectTail.cpp index aee25b588..198fde389 100644 --- a/src/game/WorldHandlers/SpellEffectTail.cpp +++ b/src/game/WorldHandlers/SpellEffectTail.cpp @@ -206,8 +206,8 @@ void Spell::EffectBreakPlayerTargeting (SpellEffectEntry const* /*effect*/) return; } - WorldPacket data(SMSG_CLEAR_TARGET, 8); - data << unitTarget->GetObjectGuid(); + WorldPacket data; + MopSpellPackets::BuildClearTarget(data, unitTarget->GetObjectGuid()); unitTarget->SendMessageToSet(&data, false); } diff --git a/src/game/WorldHandlers/SpellPackets.cpp b/src/game/WorldHandlers/SpellPackets.cpp index cc9ecba26..5b2ac16d6 100644 --- a/src/game/WorldHandlers/SpellPackets.cpp +++ b/src/game/WorldHandlers/SpellPackets.cpp @@ -1026,9 +1026,9 @@ void Spell::SendChannelUpdate(uint32 time) m_caster->SetUInt32Value(UNIT_CHANNEL_SPELL, 0); } - WorldPacket data(SMSG_CHANNEL_UPDATE, 8 + 4); - data << m_caster->GetPackGUID(); - data << uint32(time); + WorldPacket data; + MopSpellPackets::BuildChannelUpdate( + data, m_caster->GetObjectGuid(), uint32(time)); m_caster->SendMessageToSet(&data, true); } @@ -1071,25 +1071,9 @@ void Spell::SendChannelStart(uint32 duration) } } - WorldPacket data(SMSG_CHANNEL_START, (8 + 4 + 4)); - data << m_caster->GetPackGUID(); - data << uint32(m_spellInfo->ID); - data << uint32(duration); - data << uint8(0); // unk1 - //if (unk1) - //{ - // data << uint32(0); - // data << uint32(0); - //} - data << uint8(0); // unk2 - //if (unk2) - //{ - // data << ObjectGuid().WriteAsPacked(); - // data << uint32(0); - // data << uint8(0); // unk3 - // if (unk3 == 2) - // data << ObjectGuid().WriteAsPacked(); - //} + WorldPacket data; + MopSpellPackets::BuildChannelStart( + data, m_caster->GetObjectGuid(), m_spellInfo->ID, duration); m_caster->SendMessageToSet(&data, true); diff --git a/src/modules/Eluna b/src/modules/Eluna index 8eaa22bde..e5b40f0c9 160000 --- a/src/modules/Eluna +++ b/src/modules/Eluna @@ -1 +1 @@ -Subproject commit 8eaa22bde6361ce589147dbf3ee8a9b3a3596d6a +Subproject commit e5b40f0c9bd658fc269ba3187cdecde453c91428