From fff72d764e0553f9dc319078a46eb0399cca07f2 Mon Sep 17 00:00:00 2001 From: MadMax Date: Sun, 26 Jul 2026 22:15:46 +0100 Subject: [PATCH 01/28] [Spells] Encode 18414 cooldown events Mechanical implementation of the binary-proven 18414 SMSG_COOLDOWN_EVENT wire format so the MoP client receives the cooldown event instead of a stale Cata body. This adds MopSpellPackets::BuildCooldownEvent, replaces the inherited SpellCooldownMgr body, registers the opcode, admits it through IsEnterWorldConverted, and updates Opcodes_reference.h. Byte-exact fixtures and hostile source mutations are included. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/game/Object/SpellCooldownMgr.cpp | 5 +- src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 4 +- .../mop_spell_cast_packets_source_test.cmake | 56 +++++++++++++++++++ .../tests/mop_spell_cast_packets_test.cpp | 37 ++++++++++++ src/game/WorldHandlers/Spell.h | 14 +++++ 8 files changed, 117 insertions(+), 7 deletions(-) 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/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 2c4dbcdd7..4afd0ea38 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -574,6 +574,7 @@ 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_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_reference.h b/src/game/Server/Opcodes_reference.h index d4a1df05c..c34060ee0 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * 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 + * STATUS TOTALS: ACTIVE=362, DOC=438, DORMANT=720 + * SMSG: ACTIVE=222, DOC=271, DORMANT=432 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -936,7 +936,7 @@ typedef uint16_t uint16; * SMSG_SPELL_START 0x107A ACTIVE * SMSG_SET_FLAT_SPELL_MODIFIER 0x10F2 DORMANT * SMSG_CHANNEL_START 0x10F9 DORMANT - * SMSG_COOLDOWN_EVENT 0x1163 DORMANT + * SMSG_COOLDOWN_EVENT 0x1163 ACTIVE * SMSG_UNKNOWN_0x117A 0x117A DOC * SMSG_CHANNEL_UPDATE 0x11D9 DORMANT * SMSG_PLAY_SPELL_VISUAL_KIT 0x11E3 DORMANT diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index ac17a10b2..d99b42ef3 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -341,6 +341,7 @@ static bool IsEnterWorldConverted(uint16 opcode) 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_LEARNED_SPELL: // MopSpellPackets::BuildLearnedSpell case SMSG_REMOVED_SPELL: // MopSpellPackets::BuildRemovedSpell case SMSG_SUPERCEDED_SPELL: // MopSpellPackets::BuildSupersededSpell diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index cca2df653..e75734146 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -1369,7 +1369,9 @@ 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) add_test(NAME mop_spell_cast_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} 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..706942b53 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 @@ -348,6 +348,36 @@ 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}") endif() string(FIND "${spell_handler}" "void WorldSession::HandleCastSpellOpcode" cast_start) @@ -660,6 +690,32 @@ 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 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..0a7622dc1 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,31 @@ 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, + }); +} + int main(int, char**) { test_dense_and_guid_permutations(); @@ -1105,6 +1141,7 @@ 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_spellbook_mutation_wire_layouts(); test_pet_spellbook_mutation_wire_layouts(); diff --git a/src/game/WorldHandlers/Spell.h b/src/game/WorldHandlers/Spell.h index 6c495077b..12f321b0d 100644 --- a/src/game/WorldHandlers/Spell.h +++ b/src/game/WorldHandlers/Spell.h @@ -376,6 +376,20 @@ 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 BuildLearnedSpell(WorldPacket& out, uint32 spellId, bool suppressMessaging) { From edabc02f05e30c537406d393a3c7f10e0578050a Mon Sep 17 00:00:00 2001 From: MadMax Date: Sun, 26 Jul 2026 23:32:12 +0100 Subject: [PATCH 02/28] [Spells] Encode 18414 instakill logs --- src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 6 ++ .../mop_combat_log_packets_source_test.cmake | 30 +++++++++ .../tests/mop_combat_log_packets_test.cpp | 65 +++++++++++++++++++ src/game/WorldHandlers/Spell.h | 45 +++++++++++++ .../SpellEffectDamageTeleport.cpp | 10 +-- 8 files changed, 157 insertions(+), 7 deletions(-) diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 4afd0ea38..4b0a12d7e 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -236,6 +236,7 @@ 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_AURA_UPDATE, "SMSG_AURA_UPDATE"); DefS(SMSG_UPDATE_OBJECT, "SMSG_UPDATE_OBJECT"); DefS(SMSG_DESTROY_OBJECT, "SMSG_DESTROY_OBJECT"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index c34060ee0..651d28b30 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=362, DOC=438, DORMANT=720 - * SMSG: ACTIVE=222, DOC=271, DORMANT=432 + * STATUS TOTALS: ACTIVE=363, DOC=438, DORMANT=719 + * SMSG: ACTIVE=223, DOC=271, DORMANT=431 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -767,7 +767,7 @@ 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_SPELLINSTAKILLLOG 0x09F8 ACTIVE * SMSG_SPELLHEALLOG 0x09FB DORMANT * SMSG_UNKNOWN_0x0A2F 0x0A2F DOC [medium-conf] * SMSG_UNKNOWN_0x0A3B 0x0A3B DOC diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index d99b42ef3..cd7fe9014 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -335,6 +335,7 @@ 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_CAST_FAILED: // MopSpellPackets::BuildCastFailed case SMSG_PET_CAST_FAILED: // MopSpellPackets::BuildCastFailed (pet bit order) case SMSG_SPELL_START: // MopSpellPackets::BuildSpellStart diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index e75734146..efe5d7fa8 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -226,6 +226,12 @@ 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) + 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 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..3c5e34fa6 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,21 @@ 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/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}") +endif() + if(spell_packets MATCHES "SMSG_SPELLLOGEXECUTE") message(FATAL_ERROR "legacy execute-log opcode remains in SpellPackets.cpp") endif() @@ -97,3 +110,20 @@ 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() 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..c9ad50b03 100644 --- a/src/game/Server/tests/mop_combat_log_packets_test.cpp +++ b/src/game/Server/tests/mop_combat_log_packets_test.cpp @@ -108,6 +108,42 @@ 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 ExpectedExecute(MopCombatLogPackets::SpellExecuteLog const& log) { static uint8 const casterMaskPre[] = { 0, 6, 5, 7, 2 }; @@ -376,6 +412,28 @@ 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 CheckPeriodic(MopCombatLogPackets::PeriodicAuraLog const& log) { WorldPacket packet(SMSG_SPELL_PERIODIC_AURA_LOG, 64); @@ -481,6 +539,7 @@ static void test_spell_interrupt_log() static void test_successor_opcodes_are_framable() { + CHECK(uint32(SMSG_SPELLINSTAKILLLOG) == 0x09F8u); CHECK(uint32(SMSG_SPELL_EXECUTE_LOG) == 0x00D8u); CHECK(uint32(SMSG_SPELL_PERIODIC_AURA_LOG) == 0x0CF2u); CHECK(uint32(SMSG_SPELLDISPELLOG) == 0x0DF9u); @@ -493,6 +552,11 @@ 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)); + CHECK(MopWire::BuildServerHeader(true, executePacket.size(), executePacket.GetOpcode(), header)); CHECK(header[0] == 0xD8 && header[1] == 0xA0 && header[2] == 0x03 && header[3] == 0x00); @@ -522,6 +586,7 @@ static void test_successor_opcodes_are_framable() int main(int, char**) { + test_spell_instakill_log(); test_execute_variants(); test_periodic_variants(); test_dispel_and_steal_variants(); diff --git a/src/game/WorldHandlers/Spell.h b/src/game/WorldHandlers/Spell.h index 12f321b0d..80398260f 100644 --- a/src/game/WorldHandlers/Spell.h +++ b/src/game/WorldHandlers/Spell.h @@ -1462,6 +1462,51 @@ namespace MopCombatLogPackets uint32 interruptedSpellId; }; + struct SpellInstakillLog + { + uint64 casterGuid; + uint64 victimGuid; + uint32 spellId; + }; + + 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 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); From bcaf8bbc2c0cdac1236d890d468638db83548837 Mon Sep 17 00:00:00 2001 From: MadMax Date: Sun, 26 Jul 2026 23:35:43 +0100 Subject: [PATCH 03/28] [Spells] Encode 18414 energize logs --- src/game/Object/Unit.cpp | 14 ++-- src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 3 +- .../mop_combat_log_packets_source_test.cmake | 27 ++++++++ .../tests/mop_combat_log_packets_test.cpp | 65 +++++++++++++++++++ src/game/WorldHandlers/Spell.h | 41 ++++++++++++ 8 files changed, 148 insertions(+), 10 deletions(-) diff --git a/src/game/Object/Unit.cpp b/src/game/Object/Unit.cpp index 9b5603864..dba01fa10 100644 --- a/src/game/Object/Unit.cpp +++ b/src/game/Object/Unit.cpp @@ -3544,12 +3544,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); } diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 4b0a12d7e..4f68622d0 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -237,6 +237,7 @@ void InitializeOpcodes() DefS(SMSG_SPELLDISPELLOG, "SMSG_SPELLDISPELLOG"); DefS(SMSG_SPELLINTERRUPTLOG, "SMSG_SPELLINTERRUPTLOG"); DefS(SMSG_SPELLINSTAKILLLOG, "SMSG_SPELLINSTAKILLLOG"); + DefS(SMSG_SPELLENERGIZELOG, "SMSG_SPELLENERGIZELOG"); DefS(SMSG_AURA_UPDATE, "SMSG_AURA_UPDATE"); DefS(SMSG_UPDATE_OBJECT, "SMSG_UPDATE_OBJECT"); DefS(SMSG_DESTROY_OBJECT, "SMSG_DESTROY_OBJECT"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 651d28b30..295f8c2db 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=363, DOC=438, DORMANT=719 - * SMSG: ACTIVE=223, DOC=271, DORMANT=431 + * STATUS TOTALS: ACTIVE=364, DOC=438, DORMANT=718 + * SMSG: ACTIVE=224, DOC=271, DORMANT=430 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -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 diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index cd7fe9014..fc888db89 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -336,6 +336,7 @@ static bool IsEnterWorldConverted(uint16 opcode) case SMSG_SPELLDISPELLOG: // MopCombatLogPackets::BuildDispelLog case SMSG_SPELLINTERRUPTLOG: // MopCombatLogPackets::BuildSpellInterruptLog case SMSG_SPELLINSTAKILLLOG: // MopCombatLogPackets::BuildSpellInstakillLog + case SMSG_SPELLENERGIZELOG: // MopCombatLogPackets::BuildSpellEnergizeLog case SMSG_CAST_FAILED: // MopSpellPackets::BuildCastFailed case SMSG_PET_CAST_FAILED: // MopSpellPackets::BuildCastFailed (pet bit order) case SMSG_SPELL_START: // MopSpellPackets::BuildSpellStart diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index efe5d7fa8..9e136ab29 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -226,7 +226,8 @@ 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) +foreach(mutation IN ITEMS instakill_builder instakill_sender instakill_registration instakill_gate instakill_reference + energize_builder energize_sender energize_registration energize_gate energize_reference) 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) 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 3c5e34fa6..977f5fb66 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 @@ -20,6 +20,16 @@ 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}") endif() if(spell_packets MATCHES "SMSG_SPELLLOGEXECUTE") @@ -127,3 +137,20 @@ 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() 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 c9ad50b03..d972ab4ae 100644 --- a/src/game/Server/tests/mop_combat_log_packets_test.cpp +++ b/src/game/Server/tests/mop_combat_log_packets_test.cpp @@ -144,6 +144,38 @@ static std::vector ExpectedInstakill(MopCombatLogPackets::SpellInstakillL 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 ExpectedExecute(MopCombatLogPackets::SpellExecuteLog const& log) { static uint8 const casterMaskPre[] = { 0, 6, 5, 7, 2 }; @@ -434,6 +466,32 @@ static void test_spell_instakill_log() 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 CheckPeriodic(MopCombatLogPackets::PeriodicAuraLog const& log) { WorldPacket packet(SMSG_SPELL_PERIODIC_AURA_LOG, 64); @@ -540,6 +598,7 @@ 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_SPELL_EXECUTE_LOG) == 0x00D8u); CHECK(uint32(SMSG_SPELL_PERIODIC_AURA_LOG) == 0x0CF2u); CHECK(uint32(SMSG_SPELLDISPELLOG) == 0x0DF9u); @@ -557,6 +616,11 @@ static void test_successor_opcodes_are_framable() 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)); + CHECK(MopWire::BuildServerHeader(true, executePacket.size(), executePacket.GetOpcode(), header)); CHECK(header[0] == 0xD8 && header[1] == 0xA0 && header[2] == 0x03 && header[3] == 0x00); @@ -587,6 +651,7 @@ static void test_successor_opcodes_are_framable() int main(int, char**) { test_spell_instakill_log(); + test_spell_energize_log(); test_execute_variants(); test_periodic_variants(); test_dispel_and_steal_variants(); diff --git a/src/game/WorldHandlers/Spell.h b/src/game/WorldHandlers/Spell.h index 80398260f..2eb1623b6 100644 --- a/src/game/WorldHandlers/Spell.h +++ b/src/game/WorldHandlers/Spell.h @@ -1469,6 +1469,15 @@ namespace MopCombatLogPackets uint32 spellId; }; + struct SpellEnergizeLog + { + uint64 targetGuid; + uint64 casterGuid; + uint32 amount; + uint32 spellId; + uint32 powerType; + }; + inline void BuildSpellInstakillLog(WorldPacket& out, SpellInstakillLog const& log) { // The 18414 reader interleaves both packed-GUID masks before consuming @@ -1507,6 +1516,38 @@ namespace MopCombatLogPackets 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 bool BuildSpellExecuteLog(WorldPacket& out, SpellExecuteLog const& log) { bool const extraAttacks = log.kind == ExecuteKind::ExtraAttacks; From bcc97a47c57f4c0c47312a23cc3b038ec3b1073e Mon Sep 17 00:00:00 2001 From: MadMax Date: Sun, 26 Jul 2026 23:38:57 +0100 Subject: [PATCH 04/28] [Spells] Encode 18414 heal logs --- src/game/Object/Unit.cpp | 20 +++--- src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 3 +- .../mop_combat_log_packets_source_test.cmake | 27 +++++++ .../tests/mop_combat_log_packets_test.cpp | 70 +++++++++++++++++++ src/game/WorldHandlers/Spell.h | 48 +++++++++++++ 8 files changed, 162 insertions(+), 14 deletions(-) diff --git a/src/game/Object/Unit.cpp b/src/game/Object/Unit.cpp index dba01fa10..5770dd137 100644 --- a/src/game/Object/Unit.cpp +++ b/src/game/Object/Unit.cpp @@ -3521,16 +3521,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); } diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 4f68622d0..d72481f81 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -238,6 +238,7 @@ void InitializeOpcodes() DefS(SMSG_SPELLINTERRUPTLOG, "SMSG_SPELLINTERRUPTLOG"); DefS(SMSG_SPELLINSTAKILLLOG, "SMSG_SPELLINSTAKILLLOG"); DefS(SMSG_SPELLENERGIZELOG, "SMSG_SPELLENERGIZELOG"); + DefS(SMSG_SPELLHEALLOG, "SMSG_SPELLHEALLOG"); DefS(SMSG_AURA_UPDATE, "SMSG_AURA_UPDATE"); DefS(SMSG_UPDATE_OBJECT, "SMSG_UPDATE_OBJECT"); DefS(SMSG_DESTROY_OBJECT, "SMSG_DESTROY_OBJECT"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 295f8c2db..4f2bf5a8c 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=364, DOC=438, DORMANT=718 - * SMSG: ACTIVE=224, DOC=271, DORMANT=430 + * STATUS TOTALS: ACTIVE=365, DOC=438, DORMANT=717 + * SMSG: ACTIVE=225, DOC=271, DORMANT=429 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -768,7 +768,7 @@ typedef uint16_t uint16; * SMSG_UNKNOWN_0x089F 0x089F ACTIVE [medium-conf] server-binding=SMSG_SAVE_GUILD_EMBLEM * SMSG_UNKNOWN_0x08FB 0x08FB DOC * SMSG_SPELLINSTAKILLLOG 0x09F8 ACTIVE - * SMSG_SPELLHEALLOG 0x09FB DORMANT + * SMSG_SPELLHEALLOG 0x09FB ACTIVE * SMSG_UNKNOWN_0x0A2F 0x0A2F DOC [medium-conf] * SMSG_UNKNOWN_0x0A3B 0x0A3B DOC * SMSG_GUILD_RANKS_UPDATE 0x0A60 ACTIVE [medium-conf] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index fc888db89..a395eb000 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -337,6 +337,7 @@ static bool IsEnterWorldConverted(uint16 opcode) case SMSG_SPELLINTERRUPTLOG: // MopCombatLogPackets::BuildSpellInterruptLog case SMSG_SPELLINSTAKILLLOG: // MopCombatLogPackets::BuildSpellInstakillLog case SMSG_SPELLENERGIZELOG: // MopCombatLogPackets::BuildSpellEnergizeLog + case SMSG_SPELLHEALLOG: // MopCombatLogPackets::BuildSpellHealLog case SMSG_CAST_FAILED: // MopSpellPackets::BuildCastFailed case SMSG_PET_CAST_FAILED: // MopSpellPackets::BuildCastFailed (pet bit order) case SMSG_SPELL_START: // MopSpellPackets::BuildSpellStart diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 9e136ab29..9977a547f 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -227,7 +227,8 @@ add_test(NAME mop_combat_log_packets_source -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) + energize_builder energize_sender energize_registration energize_gate energize_reference + heal_builder heal_sender heal_registration heal_gate heal_reference) 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) 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 977f5fb66..69f6b4632 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 @@ -30,6 +30,16 @@ 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}") endif() if(spell_packets MATCHES "SMSG_SPELLLOGEXECUTE") @@ -154,3 +164,20 @@ 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() 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 d972ab4ae..76e73524e 100644 --- a/src/game/Server/tests/mop_combat_log_packets_test.cpp +++ b/src/game/Server/tests/mop_combat_log_packets_test.cpp @@ -176,6 +176,42 @@ static std::vector ExpectedEnergize(MopCombatLogPackets::SpellEnergizeLog 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 ExpectedExecute(MopCombatLogPackets::SpellExecuteLog const& log) { static uint8 const casterMaskPre[] = { 0, 6, 5, 7, 2 }; @@ -492,6 +528,33 @@ static void test_spell_energize_log() 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 CheckPeriodic(MopCombatLogPackets::PeriodicAuraLog const& log) { WorldPacket packet(SMSG_SPELL_PERIODIC_AURA_LOG, 64); @@ -599,6 +662,7 @@ 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_SPELL_EXECUTE_LOG) == 0x00D8u); CHECK(uint32(SMSG_SPELL_PERIODIC_AURA_LOG) == 0x0CF2u); CHECK(uint32(SMSG_SPELLDISPELLOG) == 0x0DF9u); @@ -621,6 +685,11 @@ static void test_successor_opcodes_are_framable() 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)); + CHECK(MopWire::BuildServerHeader(true, executePacket.size(), executePacket.GetOpcode(), header)); CHECK(header[0] == 0xD8 && header[1] == 0xA0 && header[2] == 0x03 && header[3] == 0x00); @@ -652,6 +721,7 @@ int main(int, char**) { test_spell_instakill_log(); test_spell_energize_log(); + test_spell_heal_log(); test_execute_variants(); test_periodic_variants(); test_dispel_and_steal_variants(); diff --git a/src/game/WorldHandlers/Spell.h b/src/game/WorldHandlers/Spell.h index 2eb1623b6..24f7962fa 100644 --- a/src/game/WorldHandlers/Spell.h +++ b/src/game/WorldHandlers/Spell.h @@ -1478,6 +1478,17 @@ namespace MopCombatLogPackets uint32 powerType; }; + struct SpellHealLog + { + uint64 casterGuid; + uint64 targetGuid; + uint32 spellId; + uint32 heal; + uint32 overheal; + uint32 absorb; + bool critical; + }; + inline void BuildSpellInstakillLog(WorldPacket& out, SpellInstakillLog const& log) { // The 18414 reader interleaves both packed-GUID masks before consuming @@ -1548,6 +1559,43 @@ namespace MopCombatLogPackets 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 bool BuildSpellExecuteLog(WorldPacket& out, SpellExecuteLog const& log) { bool const extraAttacks = log.kind == ExecuteKind::ExtraAttacks; From 9f35104594329167e4f1a7ba6ad8633bfd2146ac Mon Sep 17 00:00:00 2001 From: MadMax Date: Sun, 26 Jul 2026 23:41:32 +0100 Subject: [PATCH 05/28] [Spells] Encode 18414 damage-shield logs --- src/game/Object/UnitMeleeDamage.cpp | 22 +++--- src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 3 +- .../mop_combat_log_packets_source_test.cmake | 28 +++++++ .../tests/mop_combat_log_packets_test.cpp | 74 +++++++++++++++++++ src/game/WorldHandlers/Spell.h | 50 +++++++++++++ 8 files changed, 169 insertions(+), 16 deletions(-) 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/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index d72481f81..6392b3242 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -239,6 +239,7 @@ void InitializeOpcodes() DefS(SMSG_SPELLINSTAKILLLOG, "SMSG_SPELLINSTAKILLLOG"); DefS(SMSG_SPELLENERGIZELOG, "SMSG_SPELLENERGIZELOG"); DefS(SMSG_SPELLHEALLOG, "SMSG_SPELLHEALLOG"); + DefS(SMSG_SPELLDAMAGESHIELD, "SMSG_SPELLDAMAGESHIELD"); DefS(SMSG_AURA_UPDATE, "SMSG_AURA_UPDATE"); DefS(SMSG_UPDATE_OBJECT, "SMSG_UPDATE_OBJECT"); DefS(SMSG_DESTROY_OBJECT, "SMSG_DESTROY_OBJECT"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 4f2bf5a8c..a3c27d496 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=365, DOC=438, DORMANT=717 - * SMSG: ACTIVE=225, DOC=271, DORMANT=429 + * STATUS TOTALS: ACTIVE=366, DOC=438, DORMANT=716 + * SMSG: ACTIVE=226, DOC=271, DORMANT=428 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -747,7 +747,7 @@ 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_SPELLDAMAGESHIELD 0x05F3 ACTIVE * SMSG_CHAT_PLAYER_AMBIGUOUS 0x061A DORMANT [medium-conf] * SMSG_PLAY_TIME_WARNING 0x062A DORMANT [medium-conf] * SMSG_DISMOUNTRESULT 0x062F DORMANT [medium-conf] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index a395eb000..031adf92a 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -338,6 +338,7 @@ static bool IsEnterWorldConverted(uint16 opcode) case SMSG_SPELLINSTAKILLLOG: // MopCombatLogPackets::BuildSpellInstakillLog case SMSG_SPELLENERGIZELOG: // MopCombatLogPackets::BuildSpellEnergizeLog case SMSG_SPELLHEALLOG: // MopCombatLogPackets::BuildSpellHealLog + case SMSG_SPELLDAMAGESHIELD: // MopCombatLogPackets::BuildSpellDamageShieldLog case SMSG_CAST_FAILED: // MopSpellPackets::BuildCastFailed case SMSG_PET_CAST_FAILED: // MopSpellPackets::BuildCastFailed (pet bit order) case SMSG_SPELL_START: // MopSpellPackets::BuildSpellStart diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 9977a547f..1b1009e30 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -228,7 +228,8 @@ add_test(NAME mop_combat_log_packets_source -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) + 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) 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) 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 69f6b4632..0fceb2be2 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 @@ -8,6 +8,7 @@ file(READ "${SOURCE_ROOT}/src/game/WorldHandlers/SpellEffectSkillEnchantPet.cpp" 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") @@ -40,6 +41,16 @@ 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}") endif() if(spell_packets MATCHES "SMSG_SPELLLOGEXECUTE") @@ -181,3 +192,20 @@ 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() 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 76e73524e..fa82787ec 100644 --- a/src/game/Server/tests/mop_combat_log_packets_test.cpp +++ b/src/game/Server/tests/mop_combat_log_packets_test.cpp @@ -212,6 +212,45 @@ static std::vector ExpectedHeal(MopCombatLogPackets::SpellHealLog const& 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 ExpectedExecute(MopCombatLogPackets::SpellExecuteLog const& log) { static uint8 const casterMaskPre[] = { 0, 6, 5, 7, 2 }; @@ -555,6 +594,34 @@ static void test_spell_heal_log() 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 CheckPeriodic(MopCombatLogPackets::PeriodicAuraLog const& log) { WorldPacket packet(SMSG_SPELL_PERIODIC_AURA_LOG, 64); @@ -663,6 +730,7 @@ 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_SPELL_EXECUTE_LOG) == 0x00D8u); CHECK(uint32(SMSG_SPELL_PERIODIC_AURA_LOG) == 0x0CF2u); CHECK(uint32(SMSG_SPELLDISPELLOG) == 0x0DF9u); @@ -690,6 +758,11 @@ static void test_successor_opcodes_are_framable() 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)); + CHECK(MopWire::BuildServerHeader(true, executePacket.size(), executePacket.GetOpcode(), header)); CHECK(header[0] == 0xD8 && header[1] == 0xA0 && header[2] == 0x03 && header[3] == 0x00); @@ -722,6 +795,7 @@ int main(int, char**) test_spell_instakill_log(); test_spell_energize_log(); test_spell_heal_log(); + test_spell_damage_shield_log(); test_execute_variants(); test_periodic_variants(); test_dispel_and_steal_variants(); diff --git a/src/game/WorldHandlers/Spell.h b/src/game/WorldHandlers/Spell.h index 24f7962fa..628b94dc7 100644 --- a/src/game/WorldHandlers/Spell.h +++ b/src/game/WorldHandlers/Spell.h @@ -1489,6 +1489,17 @@ namespace MopCombatLogPackets bool critical; }; + struct SpellDamageShieldLog + { + uint64 casterGuid; + uint64 targetGuid; + uint32 spellId; + uint32 damage; + uint32 overkill; + uint32 schoolMask; + uint32 resist; + }; + inline void BuildSpellInstakillLog(WorldPacket& out, SpellInstakillLog const& log) { // The 18414 reader interleaves both packed-GUID masks before consuming @@ -1596,6 +1607,45 @@ namespace MopCombatLogPackets 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 BuildSpellExecuteLog(WorldPacket& out, SpellExecuteLog const& log) { bool const extraAttacks = log.kind == ExecuteKind::ExtraAttacks; From fcc5c08a79d14fbd787c3d0a8e79567b87ec9928 Mon Sep 17 00:00:00 2001 From: MadMax Date: Sun, 26 Jul 2026 23:44:36 +0100 Subject: [PATCH 06/28] [Spells] Encode 18414 spell-miss logs --- src/game/Object/Unit.cpp | 19 ++--- src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 4 +- .../mop_combat_log_packets_source_test.cmake | 33 +++++++++ .../tests/mop_combat_log_packets_test.cpp | 73 ++++++++++++++++++- src/game/WorldHandlers/Spell.h | 46 ++++++++++++ 8 files changed, 169 insertions(+), 14 deletions(-) diff --git a/src/game/Object/Unit.cpp b/src/game/Object/Unit.cpp index 5770dd137..fc4796098 100644 --- a/src/game/Object/Unit.cpp +++ b/src/game/Object/Unit.cpp @@ -2487,15 +2487,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); } diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 6392b3242..9c534493d 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -240,6 +240,7 @@ void InitializeOpcodes() 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"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index a3c27d496..2afde8e79 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=366, DOC=438, DORMANT=716 - * SMSG: ACTIVE=226, DOC=271, DORMANT=428 + * STATUS TOTALS: ACTIVE=367, DOC=438, DORMANT=715 + * SMSG: ACTIVE=227, DOC=271, DORMANT=427 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -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] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 031adf92a..1130b0b4e 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -339,6 +339,7 @@ static bool IsEnterWorldConverted(uint16 opcode) 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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 1b1009e30..9a5c50ed3 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -229,7 +229,9 @@ add_test(NAME mop_combat_log_packets_source 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) + 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) 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 0fceb2be2..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 @@ -51,6 +51,20 @@ 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") @@ -209,3 +223,22 @@ 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 fa82787ec..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; @@ -251,6 +252,34 @@ static std::vector ExpectedDamageShield(MopCombatLogPackets::SpellDamageS 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 }; @@ -622,6 +651,41 @@ static void test_spell_damage_shield_log() 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); @@ -731,6 +795,7 @@ static void test_successor_opcodes_are_framable() 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); @@ -763,6 +828,11 @@ static void test_successor_opcodes_are_framable() 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); @@ -796,6 +866,7 @@ int main(int, char**) 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/WorldHandlers/Spell.h b/src/game/WorldHandlers/Spell.h index 628b94dc7..29f67723e 100644 --- a/src/game/WorldHandlers/Spell.h +++ b/src/game/WorldHandlers/Spell.h @@ -1500,6 +1500,20 @@ namespace MopCombatLogPackets 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 @@ -1646,6 +1660,38 @@ namespace MopCombatLogPackets 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; From 520c8d55a40229d1c39b809a7b6db3b8c94cb945 Mon Sep 17 00:00:00 2001 From: MadMax Date: Sun, 26 Jul 2026 23:53:00 +0100 Subject: [PATCH 07/28] [Spells] Encode 18414 item cooldowns --- src/game/Object/Player.cpp | 5 +-- src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +-- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 4 +- .../mop_spell_cast_packets_source_test.cmake | 45 +++++++++++++++++++ .../tests/mop_spell_cast_packets_test.cpp | 13 ++++++ src/game/WorldHandlers/Spell.h | 10 +++++ 8 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/game/Object/Player.cpp b/src/game/Object/Player.cpp index 08a395838..363422a0f 100644 --- a/src/game/Object/Player.cpp +++ b/src/game/Object/Player.cpp @@ -4584,9 +4584,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/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 9c534493d..30b1f928e 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -580,6 +580,7 @@ void InitializeOpcodes() 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_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_reference.h b/src/game/Server/Opcodes_reference.h index 2afde8e79..6f770c407 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=367, DOC=438, DORMANT=715 - * SMSG: ACTIVE=227, DOC=271, DORMANT=427 + * STATUS TOTALS: ACTIVE=368, DOC=438, DORMANT=714 + * SMSG: ACTIVE=228, DOC=271, DORMANT=426 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -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) diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 1130b0b4e..6917c57c5 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -347,6 +347,7 @@ static bool IsEnterWorldConverted(uint16 opcode) 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_LEARNED_SPELL: // MopSpellPackets::BuildLearnedSpell case SMSG_REMOVED_SPELL: // MopSpellPackets::BuildRemovedSpell case SMSG_SUPERCEDED_SPELL: // MopSpellPackets::BuildSupersededSpell diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 9a5c50ed3..97598f6b5 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -1382,7 +1382,9 @@ foreach(mutation IN ITEMS reader registration target_initializer target_mapping pet_spellbook_builder pet_spellbook_sender pet_spellbook_registration pet_spellbook_gate pet_spellbook_reference cooldown_event_mask_order cooldown_event_byte_order cooldown_event_sender - cooldown_event_registration cooldown_event_gate cooldown_event_reference) + 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) add_test(NAME mop_spell_cast_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} 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 706942b53..565be368a 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 @@ -378,6 +378,31 @@ elseif(MUTATION STREQUAL "cooldown_event_reference") "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}") endif() string(FIND "${spell_handler}" "void WorldSession::HandleCastSpellOpcode" cast_start) @@ -716,6 +741,26 @@ require_once("${opcode_reference}" "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}" "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 0a7622dc1..5722afe2a 100644 --- a/src/game/Server/tests/mop_spell_cast_packets_test.cpp +++ b/src/game/Server/tests/mop_spell_cast_packets_test.cpp @@ -1124,6 +1124,18 @@ static void test_cooldown_event_wire_layout() }); } +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, + }); +} + int main(int, char**) { test_dense_and_guid_permutations(); @@ -1142,6 +1154,7 @@ int main(int, char**) test_spell_cooldown_wire_layout(); test_clear_cooldowns_wire_layout(); test_cooldown_event_wire_layout(); + test_item_cooldown_wire_layout(); test_spellbook_mutation_wire_layouts(); test_pet_spellbook_mutation_wire_layouts(); diff --git a/src/game/WorldHandlers/Spell.h b/src/game/WorldHandlers/Spell.h index 29f67723e..76ef99a75 100644 --- a/src/game/WorldHandlers/Spell.h +++ b/src/game/WorldHandlers/Spell.h @@ -390,6 +390,16 @@ namespace MopSpellPackets 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 BuildLearnedSpell(WorldPacket& out, uint32 spellId, bool suppressMessaging) { From 5abb909cc4b357cb4252f2e9df96ce98b68a8d5e Mon Sep 17 00:00:00 2001 From: MadMax Date: Sun, 26 Jul 2026 23:58:51 +0100 Subject: [PATCH 08/28] [Death] Send 18414 durability loss notice --- src/game/Object/Player.h | 7 +++ src/game/Object/PlayerMirror.cpp | 3 +- src/game/Object/Unit.cpp | 3 +- src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 4 +- .../tests/mop_death_packets_source_test.cmake | 57 +++++++++++++++++++ .../Server/tests/mop_death_packets_test.cpp | 9 +++ 9 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/game/Object/Player.h b/src/game/Object/Player.h index 050faf499..3f5d91070 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) { diff --git a/src/game/Object/PlayerMirror.cpp b/src/game/Object/PlayerMirror.cpp index 88b57d5e0..c5843f63d 100644 --- a/src/game/Object/PlayerMirror.cpp +++ b/src/game/Object/PlayerMirror.cpp @@ -175,7 +175,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/Unit.cpp b/src/game/Object/Unit.cpp index fc4796098..f997adc44 100644 --- a/src/game/Object/Unit.cpp +++ b/src/game/Object/Unit.cpp @@ -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); } diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 30b1f928e..e76d76865 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -515,6 +515,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); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 6f770c407..c29f475ce 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=368, DOC=438, DORMANT=714 - * SMSG: ACTIVE=228, DOC=271, DORMANT=426 + * STATUS TOTALS: ACTIVE=369, DOC=438, DORMANT=713 + * SMSG: ACTIVE=229, DOC=271, DORMANT=425 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -389,7 +389,7 @@ 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_DURABILITY_DAMAGE_DEATH 0x1E3E ACTIVE [Wow.exe binary: empty route to retained DURABILITYDAMAGE_DEATH semantic] * SMSG_LOG_XPGAIN 0x1E9A DORMANT * * -- CheckExecutableSignature.cpp (1) -- diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 6917c57c5..f62c44dee 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -433,6 +433,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/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 97598f6b5..91bdd1da0 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -1190,7 +1190,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} 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(); From 533014a33612a31224f558c91283c169a50d2337 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 00:03:18 +0100 Subject: [PATCH 09/28] [Player] Send 18414 exploration experience --- src/game/Object/Player.cpp | 5 +-- src/game/Object/Player.h | 10 +++++ src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +-- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 4 +- ...uest_interaction_packets_source_test.cmake | 45 +++++++++++++++++++ ...p_world_quest_interaction_packets_test.cpp | 14 ++++++ 8 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/game/Object/Player.cpp b/src/game/Object/Player.cpp index 363422a0f..783f76b60 100644 --- a/src/game/Object/Player.cpp +++ b/src/game/Object/Player.cpp @@ -4118,9 +4118,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); } diff --git a/src/game/Object/Player.h b/src/game/Object/Player.h index 3f5d91070..801ebd9fb 100644 --- a/src/game/Object/Player.h +++ b/src/game/Object/Player.h @@ -756,6 +756,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/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index e76d76865..9482fc1f5 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -423,6 +423,7 @@ 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"); 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"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index c29f475ce..43e032a0f 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=369, DOC=438, DORMANT=713 - * SMSG: ACTIVE=229, DOC=271, DORMANT=425 + * STATUS TOTALS: ACTIVE=370, DOC=438, DORMANT=712 + * SMSG: ACTIVE=230, DOC=271, DORMANT=424 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -829,7 +829,7 @@ 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] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index f62c44dee..036fb6ce6 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -403,6 +403,7 @@ 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_QUESTGIVER_STATUS_MULTIPLE: // MopQuestStatusPackets::BuildMultipleStatus case SMSG_QUESTGIVER_QUEST_LIST: // MopQuestGiverPackets::BuildQuestList case SMSG_QUESTGIVER_QUEST_DETAILS: // MopQuestGiverPackets::BuildQuestDetails diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 91bdd1da0..1d094b731 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -1045,7 +1045,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} 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..1bd226fb5 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) @@ -103,6 +105,31 @@ if(DEFINED MUTATION) "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") 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(); From 7a261405094c99bcc2867a8e032ed5a1eec388c3 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 00:06:45 +0100 Subject: [PATCH 10/28] [Spells] Encode 18414 clear target --- src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +-- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 4 +- .../mop_spell_cast_packets_source_test.cmake | 51 +++++++++++++++++++ .../tests/mop_spell_cast_packets_test.cpp | 13 +++++ src/game/WorldHandlers/Spell.h | 11 ++++ src/game/WorldHandlers/SpellEffectTail.cpp | 4 +- 8 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 9482fc1f5..99f04a554 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -583,6 +583,7 @@ void InitializeOpcodes() 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_reference.h b/src/game/Server/Opcodes_reference.h index 43e032a0f..9d7139a61 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=370, DOC=438, DORMANT=712 - * SMSG: ACTIVE=230, DOC=271, DORMANT=424 + * STATUS TOTALS: ACTIVE=371, DOC=438, DORMANT=711 + * SMSG: ACTIVE=231, DOC=271, DORMANT=423 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -512,7 +512,7 @@ typedef uint16_t uint16; * SMSG_STOP_MIRROR_TIMER 0x1026 DORMANT [medium-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] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 036fb6ce6..4db538531 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -348,6 +348,7 @@ static bool IsEnterWorldConverted(uint16 opcode) 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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 1d094b731..ff23c82bf 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -1388,7 +1388,9 @@ foreach(mutation IN ITEMS reader registration target_initializer target_mapping 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) + 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) add_test(NAME mop_spell_cast_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} 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 565be368a..b5009efcd 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) @@ -403,6 +404,36 @@ elseif(MUTATION STREQUAL "item_cooldown_reference") "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}") endif() string(FIND "${spell_handler}" "void WorldSession::HandleCastSpellOpcode" cast_start) @@ -761,6 +792,26 @@ require_once("${opcode_reference}" "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}" "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 5722afe2a..a41edeefc 100644 --- a/src/game/Server/tests/mop_spell_cast_packets_test.cpp +++ b/src/game/Server/tests/mop_spell_cast_packets_test.cpp @@ -1136,6 +1136,18 @@ static void test_item_cooldown_wire_layout() }); } +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, + }); +} + int main(int, char**) { test_dense_and_guid_permutations(); @@ -1155,6 +1167,7 @@ int main(int, char**) test_clear_cooldowns_wire_layout(); test_cooldown_event_wire_layout(); test_item_cooldown_wire_layout(); + test_clear_target_wire_layout(); test_spellbook_mutation_wire_layouts(); test_pet_spellbook_mutation_wire_layouts(); diff --git a/src/game/WorldHandlers/Spell.h b/src/game/WorldHandlers/Spell.h index 76ef99a75..0f5d4dfdc 100644 --- a/src/game/WorldHandlers/Spell.h +++ b/src/game/WorldHandlers/Spell.h @@ -400,6 +400,17 @@ namespace MopSpellPackets 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 BuildLearnedSpell(WorldPacket& out, uint32 spellId, bool suppressMessaging) { 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); } From 95d6d5eee1cd0c6b291fe1121cb6f6406be5aa32 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 00:17:33 +0100 Subject: [PATCH 11/28] [Player] Encode 18414 level-up info --- src/game/Object/Player.cpp | 25 +++--- src/game/Object/Player.h | 26 ++++++ src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 20 +++++ ...ayer_progression_packets_source_test.cmake | 77 ++++++++++++++++++ .../mop_player_progression_packets_test.cpp | 81 +++++++++++++++++++ 8 files changed, 219 insertions(+), 18 deletions(-) create mode 100644 src/game/Server/tests/mop_player_progression_packets_source_test.cmake create mode 100644 src/game/Server/tests/mop_player_progression_packets_test.cpp diff --git a/src/game/Object/Player.cpp b/src/game/Object/Player.cpp index 783f76b60..7b2667553 100644 --- a/src/game/Object/Player.cpp +++ b/src/game/Object/Player.cpp @@ -2453,22 +2453,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)); diff --git a/src/game/Object/Player.h b/src/game/Object/Player.h index 801ebd9fb..44e99c351 100644 --- a/src/game/Object/Player.h +++ b/src/game/Object/Player.h @@ -708,6 +708,32 @@ namespace MopReputationPackets } } +namespace MopProgressionPackets +{ + 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 MopAreaTriggerPackets { struct Request diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 99f04a554..edf7a6761 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -424,6 +424,7 @@ void InitializeOpcodes() 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_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"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 9d7139a61..84c4ef817 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=371, DOC=438, DORMANT=711 - * SMSG: ACTIVE=231, DOC=271, DORMANT=423 + * STATUS TOTALS: ACTIVE=372, DOC=438, DORMANT=710 + * SMSG: ACTIVE=232, DOC=271, DORMANT=422 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -836,7 +836,7 @@ typedef uint16_t uint16; * 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_UNKNOWN_0x1A2B 0x1A2B DOC [medium-conf] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 4db538531..d685c68aa 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -405,6 +405,7 @@ static bool IsEnterWorldConverted(uint16 opcode) 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_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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index ff23c82bf..8fa00c910 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -1555,6 +1555,26 @@ 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 sender registration admission 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_player_progression_packets_source_test.cmake b/src/game/Server/tests/mop_player_progression_packets_source_test.cmake new file mode 100644 index 000000000..687757f08 --- /dev/null +++ b/src/game/Server/tests/mop_player_progression_packets_source_test.cmake @@ -0,0 +1,77 @@ +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 "sender") + string(REPLACE + "MopProgressionPackets::BuildLevelUpInfo(data, packetInfo);" + "/* removed level-up sender */" + player_source "${player_source}") + 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("${opcode_registry}" + "DefS\\(SMSG_LEVELUP_INFO,[ \\t]*\"SMSG_LEVELUP_INFO\"\\)" + "level-up registration") +require_once("${world_session}" + "case[ \t]+SMSG_LEVELUP_INFO:" + "level-up admission") +require_once("${opcode_reference}" + "SMSG_LEVELUP_INFO[ \t]+0x1961[ \t]+ACTIVE" + "active direct-client level-up reference") + +if("${player_source}" MATCHES "WorldPacket[ \\t]+[A-Za-z0-9_]+\\(SMSG_LEVELUP_INFO") + message(FATAL_ERROR "legacy inline level-up 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..d8f02fe3e --- /dev/null +++ b/src/game/Server/tests/mop_player_progression_packets_test.cpp @@ -0,0 +1,81 @@ +/* + * 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, + }); +} + +int main() +{ + TestLevelUpInfo(); + if (g_fail != 0) + return 1; + + std::printf("mop_player_progression_packets: all checks passed\n"); + return 0; +} From c2d6a782f27ab7180474c7a17ae250a21387f78d Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 00:26:08 +0100 Subject: [PATCH 12/28] [Player] Encode 18414 experience gain --- src/game/Object/Player.cpp | 19 ++++--- src/game/Object/Player.h | 33 ++++++++++++ src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +-- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 3 +- ...ayer_progression_packets_source_test.cmake | 52 +++++++++++++++++++ .../mop_player_progression_packets_test.cpp | 33 ++++++++++++ 8 files changed, 134 insertions(+), 14 deletions(-) diff --git a/src/game/Object/Player.cpp b/src/game/Object/Player.cpp index 7b2667553..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); } diff --git a/src/game/Object/Player.h b/src/game/Object/Player.h index 44e99c351..1069781f1 100644 --- a/src/game/Object/Player.h +++ b/src/game/Object/Player.h @@ -710,6 +710,39 @@ 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; diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index edf7a6761..8baa8fba9 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -424,6 +424,7 @@ void InitializeOpcodes() 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"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 84c4ef817..541dfdcff 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=372, DOC=438, DORMANT=710 - * SMSG: ACTIVE=232, DOC=271, DORMANT=422 + * STATUS TOTALS: ACTIVE=373, DOC=438, DORMANT=709 + * SMSG: ACTIVE=233, DOC=271, DORMANT=421 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -390,7 +390,7 @@ typedef uint16_t uint16; * SMSG_EXPECTED_SPAM_RECORDS 0x18C0 DORMANT [medium-conf] * SMSG_MESSAGECHAT 0x1A9A ACTIVE [medium-conf] * SMSG_DURABILITY_DAMAGE_DEATH 0x1E3E ACTIVE [Wow.exe binary: empty route to retained DURABILITYDAMAGE_DEATH semantic] - * SMSG_LOG_XPGAIN 0x1E9A DORMANT + * 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] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index d685c68aa..fe71e9be9 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -405,6 +405,7 @@ static bool IsEnterWorldConverted(uint16 opcode) 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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 8fa00c910..30978d4f4 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -1567,7 +1567,8 @@ 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 sender registration admission reference) +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} 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 index 687757f08..a87e0dc4c 100644 --- a/src/game/Server/tests/mop_player_progression_packets_source_test.cmake +++ b/src/game/Server/tests/mop_player_progression_packets_source_test.cmake @@ -14,11 +14,36 @@ if(DEFINED MUTATION) "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\");" @@ -62,16 +87,43 @@ require_once("${player_header}" 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 index d8f02fe3e..1960e8383 100644 --- a/src/game/Server/tests/mop_player_progression_packets_test.cpp +++ b/src/game/Server/tests/mop_player_progression_packets_test.cpp @@ -70,9 +70,42 @@ static void TestLevelUpInfo() }); } +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; From 8aa56977195719ee39dfcfa54516e7b93b7be000 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 00:31:31 +0100 Subject: [PATCH 13/28] [Combat] Encode 18414 party kill log --- src/game/Object/Unit.cpp | 6 +-- src/game/Object/Unit.h | 28 ++++++++++ src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +-- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 4 +- .../mop_compact_packets_source_test.cmake | 52 +++++++++++++++++++ .../Server/tests/mop_compact_packets_test.cpp | 20 +++++++ 8 files changed, 111 insertions(+), 7 deletions(-) diff --git a/src/game/Object/Unit.cpp b/src/game/Object/Unit.cpp index f997adc44..e7d8184f0 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) { diff --git a/src/game/Object/Unit.h b/src/game/Object/Unit.h index 7d2ba4197..bc30f3e0d 100644 --- a/src/game/Object/Unit.h +++ b/src/game/Object/Unit.h @@ -181,6 +181,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; diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 8baa8fba9..7584e6975 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -327,6 +327,7 @@ void InitializeOpcodes() DefS(SMSG_ATTACKSTART, "SMSG_ATTACKSTART"); DefS(SMSG_ATTACKSTOP, "SMSG_ATTACKSTOP"); DefS(SMSG_ATTACKERSTATEUPDATE, "SMSG_ATTACKERSTATEUPDATE"); + DefS(SMSG_PARTYKILLLOG, "SMSG_PARTYKILLLOG"); DefS(SMSG_CANCEL_COMBAT, "SMSG_CANCEL_COMBAT"); DefS(SMSG_CANCEL_AUTO_REPEAT, "SMSG_CANCEL_AUTO_REPEAT"); DefS(SMSG_AI_REACTION, "SMSG_AI_REACTION"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 541dfdcff..5af0ba657 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=373, DOC=438, DORMANT=709 - * SMSG: ACTIVE=233, DOC=271, DORMANT=421 + * STATUS TOTALS: ACTIVE=374, DOC=438, DORMANT=708 + * SMSG: ACTIVE=234, DOC=271, DORMANT=420 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -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 diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index fe71e9be9..523f46a6a 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -366,6 +366,7 @@ 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_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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 30978d4f4..1c003318e 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -188,7 +188,9 @@ 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) add_test(NAME mop_compact_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} 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..f4875c44f 100644 --- a/src/game/Server/tests/mop_compact_packets_source_test.cmake +++ b/src/game/Server/tests/mop_compact_packets_source_test.cmake @@ -114,6 +114,36 @@ 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}") endif() if(player_combat MATCHES "WorldPacket[ \t]+data\\(SMSG_ATTACKSWING_NOTINRANGE") @@ -256,3 +286,25 @@ 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() diff --git a/src/game/Server/tests/mop_compact_packets_test.cpp b/src/game/Server/tests/mop_compact_packets_test.cpp index 1533ea596..c11ea635e 100644 --- a/src/game/Server/tests/mop_compact_packets_test.cpp +++ b/src/game/Server/tests/mop_compact_packets_test.cpp @@ -352,6 +352,23 @@ 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_opcode_values_are_framable() { CHECK(uint32_t(SMSG_ATTACKSWING_ERROR) == 0x11E1u); @@ -364,6 +381,7 @@ 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_ATTACKSWING_ERROR) <= 0x1FFFu); CHECK(uint32_t(SMSG_MOVE_SET_SWIM_SPEED) <= 0x1FFFu); @@ -375,6 +393,7 @@ 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); } int main(int /*argc*/, char** /*argv*/) @@ -388,6 +407,7 @@ int main(int /*argc*/, char** /*argv*/) test_raid_difficulty(); test_dungeon_difficulty(); test_cancel_combat(); + test_party_kill_log(); test_opcode_values_are_framable(); if (g_fail) From 276a884912ce2abbbb0cc2f47c13d42643c53e6f Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 00:41:01 +0100 Subject: [PATCH 14/28] [Chat] Encode 18414 error notices --- src/game/Server/Opcodes.cpp | 3 + src/game/Server/Opcodes_reference.h | 10 +-- src/game/Server/WorldSession.cpp | 3 + src/game/Server/tests/CMakeLists.txt | 2 + .../tests/mop_chat_packets_source_test.cmake | 67 +++++++++++++++++++ .../Server/tests/mop_chat_packets_test.cpp | 42 ++++++++++++ src/game/WorldHandlers/Chat.h | 35 ++++++++++ src/game/WorldHandlers/ChatHandler.cpp | 14 ++-- 8 files changed, 165 insertions(+), 11 deletions(-) diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 7584e6975..f58e1fb19 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -245,6 +245,9 @@ void InitializeOpcodes() 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. diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 5af0ba657..d7ff942a8 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=374, DOC=438, DORMANT=708 - * SMSG: ACTIVE=234, DOC=271, DORMANT=420 + * STATUS TOTALS: ACTIVE=377, DOC=438, DORMANT=705 + * SMSG: ACTIVE=237, DOC=271, DORMANT=417 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -748,7 +748,7 @@ typedef uint16_t uint16; * SMSG_ARENA_ERROR 0x04BA DORMANT [medium-conf] * SMSG_VOICE_PARENTAL_CONTROLS 0x04BF DORMANT [medium-conf] * SMSG_SPELLDAMAGESHIELD 0x05F3 ACTIVE - * SMSG_CHAT_PLAYER_AMBIGUOUS 0x061A DORMANT [medium-conf] + * 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] @@ -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] @@ -840,7 +840,7 @@ typedef uint16_t uint16; * SMSG_UNKNOWN_0x19C2 0x19C2 DOC [medium-conf] * SMSG_CONVERT_RUNE 0x1A1B DORMANT [medium-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] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 523f46a6a..374d13300 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -355,6 +355,9 @@ static bool IsEnterWorldConverted(uint16 opcode) 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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 1c003318e..7b1c71439 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -673,6 +673,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} 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/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); } From 5082e31d5ac4951c79f0e113e6f5f8f3b1dbbd49 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 00:52:50 +0100 Subject: [PATCH 15/28] [Player] Encode 18414 duel state --- src/game/Object/Player.h | 33 ++++++++++ src/game/Object/PlayerDuel.cpp | 14 +++-- src/game/Server/Opcodes.cpp | 4 ++ src/game/Server/Opcodes_reference.h | 12 ++-- src/game/Server/WorldSession.cpp | 4 ++ src/game/Server/tests/CMakeLists.txt | 4 +- .../mop_compact_packets_source_test.cmake | 62 +++++++++++++++++++ .../Server/tests/mop_compact_packets_test.cpp | 36 +++++++++++ 8 files changed, 156 insertions(+), 13 deletions(-) diff --git a/src/game/Object/Player.h b/src/game/Object/Player.h index 1069781f1..26bf1aaad 100644 --- a/src/game/Object/Player.h +++ b/src/game/Object/Player.h @@ -767,6 +767,39 @@ namespace MopProgressionPackets } } +namespace MopDuelPackets +{ + 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; + } +} + namespace MopAreaTriggerPackets { struct Request diff --git a/src/game/Object/PlayerDuel.cpp b/src/game/Object/PlayerDuel.cpp index 99b494f0c..2ca9278e9 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,8 +155,8 @@ 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); @@ -253,7 +255,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/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index f58e1fb19..537698448 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -331,6 +331,10 @@ void InitializeOpcodes() 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_CANCEL_COMBAT, "SMSG_CANCEL_COMBAT"); DefS(SMSG_CANCEL_AUTO_REPEAT, "SMSG_CANCEL_AUTO_REPEAT"); DefS(SMSG_AI_REACTION, "SMSG_AI_REACTION"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index d7ff942a8..f2f5602f6 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=377, DOC=438, DORMANT=705 - * SMSG: ACTIVE=237, DOC=271, DORMANT=417 + * STATUS TOTALS: ACTIVE=381, DOC=438, DORMANT=701 + * SMSG: ACTIVE=241, DOC=271, DORMANT=413 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -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_OUTOFBOUNDS 0x001A ACTIVE [high-conf] * SMSG_DUEL_REQUESTED 0x0022 DORMANT * 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_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] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 374d13300..948d0862a 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -370,6 +370,10 @@ static bool IsEnterWorldConverted(uint16 opcode) 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_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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 7b1c71439..e20a0ce36 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -190,7 +190,9 @@ foreach(mutation IN ITEMS cancel_sender cancel_registration cancel_allowlist can attacker_sender attacker_registration attacker_allowlist attacker_opcode attacker_reference attacker_envelope party_kill_mask party_kill_bytes party_kill_sender party_kill_registration - party_kill_allowlist party_kill_reference) + party_kill_allowlist party_kill_reference + duel_complete_bit duel_countdown_width duel_sender duel_registration + duel_allowlist duel_reference) add_test(NAME mop_compact_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} 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 f4875c44f..c4a33c971 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,6 @@ 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/Player.h" player_header) 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) @@ -144,6 +146,36 @@ elseif(MUTATION STREQUAL "party_kill_reference") "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}") endif() if(player_combat MATCHES "WorldPacket[ \t]+data\\(SMSG_ATTACKSWING_NOTINRANGE") @@ -308,3 +340,33 @@ 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() diff --git a/src/game/Server/tests/mop_compact_packets_test.cpp b/src/game/Server/tests/mop_compact_packets_test.cpp index c11ea635e..183fb0c79 100644 --- a/src/game/Server/tests/mop_compact_packets_test.cpp +++ b/src/game/Server/tests/mop_compact_packets_test.cpp @@ -369,6 +369,33 @@ static void test_party_kill_log() })); } +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_opcode_values_are_framable() { CHECK(uint32_t(SMSG_ATTACKSWING_ERROR) == 0x11E1u); @@ -382,6 +409,10 @@ static void test_opcode_values_are_framable() 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_ATTACKSWING_ERROR) <= 0x1FFFu); CHECK(uint32_t(SMSG_MOVE_SET_SWIM_SPEED) <= 0x1FFFu); @@ -394,6 +425,10 @@ static void test_opcode_values_are_framable() 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); } int main(int /*argc*/, char** /*argv*/) @@ -408,6 +443,7 @@ int main(int /*argc*/, char** /*argv*/) test_dungeon_difficulty(); test_cancel_combat(); test_party_kill_log(); + test_duel_state_packets(); test_opcode_values_are_framable(); if (g_fail) From af2c1cbe2a92fab6b1d14fc1de2fa6c272222532 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 00:59:21 +0100 Subject: [PATCH 16/28] [Player] Encode 18414 duel results --- src/game/Object/Player.h | 51 ++++++++++++ src/game/Object/PlayerDuel.cpp | 8 +- src/game/Server/Opcodes.cpp | 2 + src/game/Server/Opcodes_reference.h | 8 +- src/game/Server/WorldSession.cpp | 2 + src/game/Server/tests/CMakeLists.txt | 5 +- .../mop_compact_packets_source_test.cmake | 82 +++++++++++++++++++ .../Server/tests/mop_compact_packets_test.cpp | 47 +++++++++++ .../WorldHandlers/SpellEffectObjectCombat.cpp | 6 +- 9 files changed, 198 insertions(+), 13 deletions(-) diff --git a/src/game/Object/Player.h b/src/game/Object/Player.h index 26bf1aaad..d3098c52e 100644 --- a/src/game/Object/Player.h +++ b/src/game/Object/Player.h @@ -769,6 +769,32 @@ namespace MopProgressionPackets 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. @@ -798,6 +824,31 @@ namespace MopDuelPackets // 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 MopAreaTriggerPackets diff --git a/src/game/Object/PlayerDuel.cpp b/src/game/Object/PlayerDuel.cpp index 2ca9278e9..39520dec5 100644 --- a/src/game/Object/PlayerDuel.cpp +++ b/src/game/Object/PlayerDuel.cpp @@ -162,11 +162,9 @@ void Player::DuelComplete(DuelCompleteType type) 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 diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 537698448..7276fa958 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -335,6 +335,8 @@ void InitializeOpcodes() 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_CANCEL_COMBAT, "SMSG_CANCEL_COMBAT"); DefS(SMSG_CANCEL_AUTO_REPEAT, "SMSG_CANCEL_AUTO_REPEAT"); DefS(SMSG_AI_REACTION, "SMSG_AI_REACTION"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index f2f5602f6..fdc6de9df 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=381, DOC=438, DORMANT=701 - * SMSG: ACTIVE=241, DOC=271, DORMANT=413 + * STATUS TOTALS: ACTIVE=383, DOC=438, DORMANT=699 + * SMSG: ACTIVE=243, DOC=271, DORMANT=411 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -443,9 +443,9 @@ typedef uint16_t uint16; * * -- DuelInfo.cpp (7) -- * SMSG_DUEL_OUTOFBOUNDS 0x001A ACTIVE [high-conf] - * SMSG_DUEL_REQUESTED 0x0022 DORMANT + * SMSG_DUEL_REQUESTED 0x0022 ACTIVE [high-conf] * SMSG_UNKNOWN_0x083F 0x083F DOC - * SMSG_DUEL_WINNER 0x10E1 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] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 948d0862a..ffcc2aeb9 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -374,6 +374,8 @@ static bool IsEnterWorldConverted(uint16 opcode) 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_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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index e20a0ce36..5f8ef89d0 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -192,7 +192,10 @@ foreach(mutation IN ITEMS cancel_sender cancel_registration cancel_allowlist can 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_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) add_test(NAME mop_compact_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} 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 c4a33c971..3c2e17fb2 100644 --- a/src/game/Server/tests/mop_compact_packets_source_test.cmake +++ b/src/game/Server/tests/mop_compact_packets_source_test.cmake @@ -1,6 +1,7 @@ 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/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) @@ -176,6 +177,51 @@ elseif(MUTATION STREQUAL "duel_reference") "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}") endif() if(player_combat MATCHES "WorldPacket[ \t]+data\\(SMSG_ATTACKSWING_NOTINRANGE") @@ -370,3 +416,39 @@ 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() diff --git a/src/game/Server/tests/mop_compact_packets_test.cpp b/src/game/Server/tests/mop_compact_packets_test.cpp index 183fb0c79..d1912e746 100644 --- a/src/game/Server/tests/mop_compact_packets_test.cpp +++ b/src/game/Server/tests/mop_compact_packets_test.cpp @@ -396,6 +396,48 @@ static void test_duel_state_packets() 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_opcode_values_are_framable() { CHECK(uint32_t(SMSG_ATTACKSWING_ERROR) == 0x11E1u); @@ -413,6 +455,8 @@ static void test_opcode_values_are_framable() 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_ATTACKSWING_ERROR) <= 0x1FFFu); CHECK(uint32_t(SMSG_MOVE_SET_SWIM_SPEED) <= 0x1FFFu); @@ -429,6 +473,8 @@ static void test_opcode_values_are_framable() 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); } int main(int /*argc*/, char** /*argv*/) @@ -444,6 +490,7 @@ int main(int /*argc*/, char** /*argv*/) test_cancel_combat(); test_party_kill_log(); test_duel_state_packets(); + test_duel_request_and_winner_packets(); test_opcode_values_are_framable(); if (g_fail) 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); From 057963b776a8f74e4a5c94a5ab7674c4508f6067 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 01:08:20 +0100 Subject: [PATCH 17/28] [Player] Encode 18414 mirror timers --- src/game/Object/Player.h | 22 +++++++ src/game/Object/PlayerMirror.cpp | 14 ++--- src/game/Server/Opcodes.cpp | 2 + src/game/Server/Opcodes_reference.h | 8 +-- src/game/Server/WorldSession.cpp | 2 + src/game/Server/tests/CMakeLists.txt | 4 +- .../mop_compact_packets_source_test.cmake | 61 +++++++++++++++++++ .../Server/tests/mop_compact_packets_test.cpp | 32 ++++++++++ 8 files changed, 131 insertions(+), 14 deletions(-) diff --git a/src/game/Object/Player.h b/src/game/Object/Player.h index d3098c52e..2028ce31e 100644 --- a/src/game/Object/Player.h +++ b/src/game/Object/Player.h @@ -851,6 +851,28 @@ namespace MopDuelPackets } } +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 diff --git a/src/game/Object/PlayerMirror.cpp b/src/game/Object/PlayerMirror.cpp index c5843f63d..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); } diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 7276fa958..23550517e 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -337,6 +337,8 @@ void InitializeOpcodes() 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_CANCEL_COMBAT, "SMSG_CANCEL_COMBAT"); DefS(SMSG_CANCEL_AUTO_REPEAT, "SMSG_CANCEL_AUTO_REPEAT"); DefS(SMSG_AI_REACTION, "SMSG_AI_REACTION"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index fdc6de9df..3d7c4046c 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=383, DOC=438, DORMANT=699 - * SMSG: ACTIVE=243, DOC=271, DORMANT=411 + * STATUS TOTALS: ACTIVE=385, DOC=438, DORMANT=697 + * SMSG: ACTIVE=245, DOC=271, DORMANT=409 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -503,13 +503,13 @@ typedef uint16_t uint16; * 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 ACTIVE [Wow.exe binary: sub_6D4AFB packed GUID reader; sub_85876E target-clear terminal] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index ffcc2aeb9..98d82be70 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -376,6 +376,8 @@ static bool IsEnterWorldConverted(uint16 opcode) 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_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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 5f8ef89d0..1e994f382 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -195,7 +195,9 @@ foreach(mutation IN ITEMS cancel_sender cancel_registration cancel_allowlist can 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) + duel_pair_allowlist duel_pair_reference + mirror_start_order mirror_pause_width mirror_sender mirror_registration + mirror_allowlist mirror_reference) add_test(NAME mop_compact_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} 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 3c2e17fb2..1e96071a0 100644 --- a/src/game/Server/tests/mop_compact_packets_source_test.cmake +++ b/src/game/Server/tests/mop_compact_packets_source_test.cmake @@ -1,5 +1,6 @@ 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/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) @@ -222,6 +223,36 @@ elseif(MUTATION STREQUAL "duel_pair_reference") "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}") endif() if(player_combat MATCHES "WorldPacket[ \t]+data\\(SMSG_ATTACKSWING_NOTINRANGE") @@ -452,3 +483,33 @@ 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() diff --git a/src/game/Server/tests/mop_compact_packets_test.cpp b/src/game/Server/tests/mop_compact_packets_test.cpp index d1912e746..d9dee6c55 100644 --- a/src/game/Server/tests/mop_compact_packets_test.cpp +++ b/src/game/Server/tests/mop_compact_packets_test.cpp @@ -438,6 +438,33 @@ static void test_duel_request_and_winner_packets() 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_opcode_values_are_framable() { CHECK(uint32_t(SMSG_ATTACKSWING_ERROR) == 0x11E1u); @@ -457,6 +484,8 @@ static void test_opcode_values_are_framable() 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_ATTACKSWING_ERROR) <= 0x1FFFu); CHECK(uint32_t(SMSG_MOVE_SET_SWIM_SPEED) <= 0x1FFFu); @@ -475,6 +504,8 @@ static void test_opcode_values_are_framable() 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); } int main(int /*argc*/, char** /*argv*/) @@ -491,6 +522,7 @@ int main(int /*argc*/, char** /*argv*/) test_party_kill_log(); test_duel_state_packets(); test_duel_request_and_winner_packets(); + test_mirror_timer_packets(); test_opcode_values_are_framable(); if (g_fail) From 95697ccf3934045586fc7bd90f0552c73b422aa6 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 01:13:20 +0100 Subject: [PATCH 18/28] [Spells] Encode 18414 channel updates --- src/game/Server/Opcodes.cpp | 2 + src/game/Server/Opcodes_reference.h | 8 +-- src/game/Server/WorldSession.cpp | 2 + src/game/Server/tests/CMakeLists.txt | 4 +- .../mop_spell_cast_packets_source_test.cmake | 63 +++++++++++++++++++ .../tests/mop_spell_cast_packets_test.cpp | 41 ++++++++++++ src/game/WorldHandlers/Spell.h | 34 ++++++++++ src/game/WorldHandlers/SpellPackets.cpp | 28 ++------- 8 files changed, 155 insertions(+), 27 deletions(-) diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 23550517e..ac4efb8ee 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -339,6 +339,8 @@ void InitializeOpcodes() 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_CANCEL_COMBAT, "SMSG_CANCEL_COMBAT"); DefS(SMSG_CANCEL_AUTO_REPEAT, "SMSG_CANCEL_AUTO_REPEAT"); DefS(SMSG_AI_REACTION, "SMSG_AI_REACTION"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 3d7c4046c..10ce3e7b1 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=385, DOC=438, DORMANT=697 - * SMSG: ACTIVE=245, DOC=271, DORMANT=409 + * STATUS TOTALS: ACTIVE=387, DOC=438, DORMANT=695 + * SMSG: ACTIVE=247, DOC=271, DORMANT=407 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -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_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 diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 98d82be70..d047eb839 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -378,6 +378,8 @@ static bool IsEnterWorldConverted(uint16 opcode) 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_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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 1e994f382..42172d43b 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -1401,7 +1401,9 @@ foreach(mutation IN ITEMS reader registration target_initializer target_mapping 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) + 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} 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 b5009efcd..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 @@ -434,6 +434,36 @@ elseif(MUTATION STREQUAL "clear_target_reference") "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) @@ -812,6 +842,39 @@ require_once("${opcode_reference}" "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 a41edeefc..45950275e 100644 --- a/src/game/Server/tests/mop_spell_cast_packets_test.cpp +++ b/src/game/Server/tests/mop_spell_cast_packets_test.cpp @@ -1148,6 +1148,46 @@ static void test_clear_target_wire_layout() }); } +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(); @@ -1168,6 +1208,7 @@ int main(int, char**) 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/WorldHandlers/Spell.h b/src/game/WorldHandlers/Spell.h index 0f5d4dfdc..1b6b450f8 100644 --- a/src/game/WorldHandlers/Spell.h +++ b/src/game/WorldHandlers/Spell.h @@ -411,6 +411,40 @@ namespace MopSpellPackets 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) { 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); From 057047e5cfb2c3934943aae20b009d703dec594b Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 01:19:11 +0100 Subject: [PATCH 19/28] [Player] Encode 18414 rune updates --- src/game/Object/RuneMgr.cpp | 18 ++--- src/game/Object/RuneMgr.h | 42 ++++++++++++ src/game/Server/Opcodes.cpp | 3 + src/game/Server/Opcodes_reference.h | 10 +-- src/game/Server/WorldSession.cpp | 3 + src/game/Server/tests/CMakeLists.txt | 4 +- .../mop_compact_packets_source_test.cmake | 66 +++++++++++++++++++ .../Server/tests/mop_compact_packets_test.cpp | 39 +++++++++++ 8 files changed, 170 insertions(+), 15 deletions(-) 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/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index ac4efb8ee..bc3f14372 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -341,6 +341,9 @@ void InitializeOpcodes() 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_CANCEL_COMBAT, "SMSG_CANCEL_COMBAT"); DefS(SMSG_CANCEL_AUTO_REPEAT, "SMSG_CANCEL_AUTO_REPEAT"); DefS(SMSG_AI_REACTION, "SMSG_AI_REACTION"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 10ce3e7b1..107a14b21 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=387, DOC=438, DORMANT=695 - * SMSG: ACTIVE=247, DOC=271, DORMANT=407 + * STATUS TOTALS: ACTIVE=390, DOC=438, DORMANT=692 + * SMSG: ACTIVE=250, DOC=271, DORMANT=404 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -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] @@ -838,7 +838,7 @@ typedef uint16_t uint16; * SMSG_SUPERCEDED_SPELL 0x1943 ACTIVE [high-conf] * 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 ACTIVE [medium-conf] * SMSG_TIME_SYNC_REQ 0x1A8F ACTIVE diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index d047eb839..aa87b8c97 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -380,6 +380,9 @@ static bool IsEnterWorldConverted(uint16 opcode) 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_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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 42172d43b..7a19f6277 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -197,7 +197,9 @@ foreach(mutation IN ITEMS cancel_sender cancel_registration cancel_allowlist can 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) + mirror_allowlist mirror_reference + rune_count_width rune_record_order rune_convert_order rune_sender + rune_registration rune_allowlist rune_reference) add_test(NAME mop_compact_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} 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 1e96071a0..b54010888 100644 --- a/src/game/Server/tests/mop_compact_packets_source_test.cmake +++ b/src/game/Server/tests/mop_compact_packets_source_test.cmake @@ -1,6 +1,8 @@ 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/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) @@ -253,6 +255,41 @@ elseif(MUTATION STREQUAL "mirror_reference") "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}") endif() if(player_combat MATCHES "WorldPacket[ \t]+data\\(SMSG_ATTACKSWING_NOTINRANGE") @@ -513,3 +550,32 @@ foreach(server_name IN ITEMS SMSG_START_MIRROR_TIMER SMSG_STOP_MIRROR_TIMER) 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() diff --git a/src/game/Server/tests/mop_compact_packets_test.cpp b/src/game/Server/tests/mop_compact_packets_test.cpp index d9dee6c55..8e98c253d 100644 --- a/src/game/Server/tests/mop_compact_packets_test.cpp +++ b/src/game/Server/tests/mop_compact_packets_test.cpp @@ -465,6 +465,38 @@ static void test_mirror_timer_packets() 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_opcode_values_are_framable() { CHECK(uint32_t(SMSG_ATTACKSWING_ERROR) == 0x11E1u); @@ -486,6 +518,9 @@ static void test_opcode_values_are_framable() 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_ATTACKSWING_ERROR) <= 0x1FFFu); CHECK(uint32_t(SMSG_MOVE_SET_SWIM_SPEED) <= 0x1FFFu); @@ -506,6 +541,9 @@ static void test_opcode_values_are_framable() 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); } int main(int /*argc*/, char** /*argv*/) @@ -523,6 +561,7 @@ int main(int /*argc*/, char** /*argv*/) test_duel_state_packets(); test_duel_request_and_winner_packets(); test_mirror_timer_packets(); + test_rune_packets(); test_opcode_values_are_framable(); if (g_fail) From a6e5d8cde712aeb81497674c658c60315d7b98c0 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 01:30:30 +0100 Subject: [PATCH 20/28] [Combat] Encode 18414 threat updates --- src/game/Object/Unit.h | 109 ++++++++++++++++++ src/game/Object/UnitThreat.cpp | 38 +++--- src/game/Server/Opcodes.cpp | 4 + src/game/Server/Opcodes_reference.h | 12 +- src/game/Server/WorldSession.cpp | 4 + src/game/Server/tests/CMakeLists.txt | 4 +- .../mop_compact_packets_source_test.cmake | 94 +++++++++++++++ .../Server/tests/mop_compact_packets_test.cpp | 54 +++++++++ 8 files changed, 292 insertions(+), 27 deletions(-) diff --git a/src/game/Object/Unit.h b/src/game/Object/Unit.h index bc30f3e0d..f7f998028 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) @@ -377,6 +379,113 @@ namespace MopCompactPackets } } +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/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/Opcodes.cpp b/src/game/Server/Opcodes.cpp index bc3f14372..b9e2d085c 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -344,6 +344,10 @@ void InitializeOpcodes() 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_CANCEL_COMBAT, "SMSG_CANCEL_COMBAT"); DefS(SMSG_CANCEL_AUTO_REPEAT, "SMSG_CANCEL_AUTO_REPEAT"); DefS(SMSG_AI_REACTION, "SMSG_AI_REACTION"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 107a14b21..16a43ec44 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=390, DOC=438, DORMANT=692 - * SMSG: ACTIVE=250, DOC=271, DORMANT=404 + * STATUS TOTALS: ACTIVE=394, DOC=438, DORMANT=688 + * SMSG: ACTIVE=254, DOC=271, DORMANT=400 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -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 @@ -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,7 +1085,7 @@ 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_THREAT_REMOVE 0x1960 ACTIVE [high-conf] * SMSG_PRE_RESURRECT 0x19C0 DORMANT * SMSG_MONSTER_MOVE 0x1A07 ACTIVE * SMSG_PLAYER_MOVE 0x1A32 ACTIVE diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index aa87b8c97..bc8fb5f47 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -383,6 +383,10 @@ static bool IsEnterWorldConverted(uint16 opcode) 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_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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 7a19f6277..a1ec54192 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -199,7 +199,9 @@ foreach(mutation IN ITEMS cancel_sender cancel_registration cancel_allowlist can 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) + 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) add_test(NAME mop_compact_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} 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 b54010888..0ca85ebfb 100644 --- a/src/game/Server/tests/mop_compact_packets_source_test.cmake +++ b/src/game/Server/tests/mop_compact_packets_source_test.cmake @@ -16,6 +16,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) @@ -290,6 +291,51 @@ elseif(MUTATION STREQUAL "rune_reference") "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}") endif() if(player_combat MATCHES "WorldPacket[ \t]+data\\(SMSG_ATTACKSWING_NOTINRANGE") @@ -579,3 +625,51 @@ foreach(server_name IN ITEMS SMSG_RESYNC_RUNES SMSG_ADD_RUNE_POWER SMSG_CONVERT_ 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() diff --git a/src/game/Server/tests/mop_compact_packets_test.cpp b/src/game/Server/tests/mop_compact_packets_test.cpp index 8e98c253d..b6834b510 100644 --- a/src/game/Server/tests/mop_compact_packets_test.cpp +++ b/src/game/Server/tests/mop_compact_packets_test.cpp @@ -497,6 +497,51 @@ static void test_rune_packets() 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_opcode_values_are_framable() { CHECK(uint32_t(SMSG_ATTACKSWING_ERROR) == 0x11E1u); @@ -521,6 +566,10 @@ static void test_opcode_values_are_framable() 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_ATTACKSWING_ERROR) <= 0x1FFFu); CHECK(uint32_t(SMSG_MOVE_SET_SWIM_SPEED) <= 0x1FFFu); @@ -544,6 +593,10 @@ static void test_opcode_values_are_framable() 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); } int main(int /*argc*/, char** /*argv*/) @@ -562,6 +615,7 @@ int main(int /*argc*/, char** /*argv*/) test_duel_request_and_winner_packets(); test_mirror_timer_packets(); test_rune_packets(); + test_threat_packets(); test_opcode_values_are_framable(); if (g_fail) From b8b86dfd54417e73ea9b27893a31205f8559c8df Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 01:36:52 +0100 Subject: [PATCH 21/28] [Movement] Encode 18414 dismount notices --- src/game/Object/Unit.cpp | 4 +- src/game/Object/Unit.h | 9 +++ src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 4 +- .../mop_compact_packets_source_test.cmake | 59 +++++++++++++++++++ .../Server/tests/mop_compact_packets_test.cpp | 14 +++++ 8 files changed, 92 insertions(+), 6 deletions(-) diff --git a/src/game/Object/Unit.cpp b/src/game/Object/Unit.cpp index e7d8184f0..8ccdd3db0 100644 --- a/src/game/Object/Unit.cpp +++ b/src/game/Object/Unit.cpp @@ -3743,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 f7f998028..c0fd33339 100644 --- a/src/game/Object/Unit.h +++ b/src/game/Object/Unit.h @@ -377,6 +377,15 @@ 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); + } } namespace MopThreatPackets diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index b9e2d085c..a166cde18 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -348,6 +348,7 @@ void InitializeOpcodes() 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_CANCEL_COMBAT, "SMSG_CANCEL_COMBAT"); DefS(SMSG_CANCEL_AUTO_REPEAT, "SMSG_CANCEL_AUTO_REPEAT"); DefS(SMSG_AI_REACTION, "SMSG_AI_REACTION"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 16a43ec44..f5a3425f8 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=394, DOC=438, DORMANT=688 - * SMSG: ACTIVE=254, DOC=271, DORMANT=400 + * STATUS TOTALS: ACTIVE=395, DOC=438, DORMANT=687 + * SMSG: ACTIVE=255, DOC=271, DORMANT=399 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -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 diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index bc8fb5f47..44fc68601 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -387,6 +387,7 @@ static bool IsEnterWorldConverted(uint16 opcode) 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_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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index a1ec54192..cab516764 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -201,7 +201,9 @@ foreach(mutation IN ITEMS cancel_sender cancel_registration cancel_allowlist can 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) + threat_remove_mask threat_sender threat_registration threat_allowlist threat_reference + dismount_mask dismount_bytes dismount_sender dismount_registration + dismount_allowlist dismount_reference) add_test(NAME mop_compact_packets_source_mutation_${mutation} COMMAND ${CMAKE_COMMAND} -DSOURCE_ROOT=${CMAKE_SOURCE_DIR} -DMUTATION=${mutation} 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 0ca85ebfb..3cd1fc206 100644 --- a/src/game/Server/tests/mop_compact_packets_source_test.cmake +++ b/src/game/Server/tests/mop_compact_packets_source_test.cmake @@ -336,6 +336,36 @@ elseif(MUTATION STREQUAL "threat_reference") "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}") endif() if(player_combat MATCHES "WorldPacket[ \t]+data\\(SMSG_ATTACKSWING_NOTINRANGE") @@ -673,3 +703,32 @@ foreach(server_name IN ITEMS 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() diff --git a/src/game/Server/tests/mop_compact_packets_test.cpp b/src/game/Server/tests/mop_compact_packets_test.cpp index b6834b510..f6773224a 100644 --- a/src/game/Server/tests/mop_compact_packets_test.cpp +++ b/src/game/Server/tests/mop_compact_packets_test.cpp @@ -542,6 +542,17 @@ static void test_threat_packets() })); } +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_opcode_values_are_framable() { CHECK(uint32_t(SMSG_ATTACKSWING_ERROR) == 0x11E1u); @@ -570,6 +581,7 @@ static void test_opcode_values_are_framable() 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_ATTACKSWING_ERROR) <= 0x1FFFu); CHECK(uint32_t(SMSG_MOVE_SET_SWIM_SPEED) <= 0x1FFFu); @@ -597,6 +609,7 @@ static void test_opcode_values_are_framable() 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); } int main(int /*argc*/, char** /*argv*/) @@ -616,6 +629,7 @@ int main(int /*argc*/, char** /*argv*/) test_mirror_timer_packets(); test_rune_packets(); test_threat_packets(); + test_dismount_packet(); test_opcode_values_are_framable(); if (g_fail) From 8deec785c13f80d5ec89694bb1ef141805c79fc4 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 01:44:23 +0100 Subject: [PATCH 22/28] [Player] Encode 18414 combo-point updates --- src/game/Object/Player.h | 16 +++++ src/game/Object/PlayerCombo.cpp | 5 +- src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 3 +- .../mop_compact_packets_source_test.cmake | 68 +++++++++++++++++++ .../Server/tests/mop_compact_packets_test.cpp | 14 ++++ 8 files changed, 107 insertions(+), 7 deletions(-) diff --git a/src/game/Object/Player.h b/src/game/Object/Player.h index 2028ce31e..da77cc33d 100644 --- a/src/game/Object/Player.h +++ b/src/game/Object/Player.h @@ -767,6 +767,22 @@ namespace MopProgressionPackets } } +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, 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/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index a166cde18..f80be14e7 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -349,6 +349,7 @@ void InitializeOpcodes() DefS(SMSG_THREAT_CLEAR, "SMSG_THREAT_CLEAR"); DefS(SMSG_THREAT_REMOVE, "SMSG_THREAT_REMOVE"); DefS(SMSG_DISMOUNT, "SMSG_DISMOUNT"); + 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"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index f5a3425f8..66cd55d16 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=395, DOC=438, DORMANT=687 - * SMSG: ACTIVE=255, DOC=271, DORMANT=399 + * STATUS TOTALS: ACTIVE=396, DOC=438, DORMANT=686 + * SMSG: ACTIVE=256, DOC=271, DORMANT=398 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -496,7 +496,7 @@ 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] diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 44fc68601..65ec9c00d 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -388,6 +388,7 @@ static bool IsEnterWorldConverted(uint16 opcode) case SMSG_THREAT_CLEAR: // MopThreatPackets::BuildClear case SMSG_THREAT_REMOVE: // MopThreatPackets::BuildRemove case SMSG_DISMOUNT: // MopCompactPackets::BuildDismount + 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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index cab516764..7c58d29ab 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -203,7 +203,8 @@ foreach(mutation IN ITEMS cancel_sender cancel_registration cancel_allowlist can 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) + dismount_allowlist dismount_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} 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 3cd1fc206..cd00a6ab0 100644 --- a/src/game/Server/tests/mop_compact_packets_source_test.cmake +++ b/src/game/Server/tests/mop_compact_packets_source_test.cmake @@ -1,6 +1,7 @@ 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/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) @@ -366,6 +367,36 @@ elseif(MUTATION STREQUAL "dismount_reference") "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}") endif() if(player_combat MATCHES "WorldPacket[ \t]+data\\(SMSG_ATTACKSWING_NOTINRANGE") @@ -732,3 +763,40 @@ string(REGEX MATCH "SMSG_DISMOUNT[ \t]+0x0E3A[ \t]+ACTIVE" dismount_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() diff --git a/src/game/Server/tests/mop_compact_packets_test.cpp b/src/game/Server/tests/mop_compact_packets_test.cpp index f6773224a..00b2875cc 100644 --- a/src/game/Server/tests/mop_compact_packets_test.cpp +++ b/src/game/Server/tests/mop_compact_packets_test.cpp @@ -553,6 +553,17 @@ static void test_dismount_packet() })); } +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_opcode_values_are_framable() { CHECK(uint32_t(SMSG_ATTACKSWING_ERROR) == 0x11E1u); @@ -582,6 +593,7 @@ static void test_opcode_values_are_framable() CHECK(uint32_t(SMSG_THREAT_CLEAR) == 0x180Bu); CHECK(uint32_t(SMSG_THREAT_REMOVE) == 0x1960u); CHECK(uint32_t(SMSG_DISMOUNT) == 0x0E3Au); + CHECK(uint32_t(SMSG_UPDATE_COMBO_POINTS) == 0x082Fu); CHECK(uint32_t(SMSG_ATTACKSWING_ERROR) <= 0x1FFFu); CHECK(uint32_t(SMSG_MOVE_SET_SWIM_SPEED) <= 0x1FFFu); @@ -610,6 +622,7 @@ static void test_opcode_values_are_framable() CHECK(uint32_t(SMSG_THREAT_CLEAR) <= 0x1FFFu); CHECK(uint32_t(SMSG_THREAT_REMOVE) <= 0x1FFFu); CHECK(uint32_t(SMSG_DISMOUNT) <= 0x1FFFu); + CHECK(uint32_t(SMSG_UPDATE_COMBO_POINTS) <= 0x1FFFu); } int main(int /*argc*/, char** /*argv*/) @@ -630,6 +643,7 @@ int main(int /*argc*/, char** /*argv*/) test_rune_packets(); test_threat_packets(); test_dismount_packet(); + test_combo_points_packet(); test_opcode_values_are_framable(); if (g_fail) From a61d113e7a0c4eb14e422bcc3dc1e04a05baa1ce Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 09:40:04 +0100 Subject: [PATCH 23/28] [MoP] Project quest-log fields to the client Accepting a quest updated server state but never reached the client's quest log: MopUpdateObject projected only the inventory, skill and buyback self ranges, so the quest-log fields were never sent and QuestLogFrame had nothing to render. Unlike every other self range, this one cannot be shifted by a constant. Four stores fifty five-word quest slots at 166..415 (MAX_QUEST_OFFSET 5), while 18414's CGPlayerData::questLog is fifty FIFTEEN-word slots at 171..920 -- 750 fields, confirmed both by the client descriptor initializers and by the retained 5.4.8 reference table. Only the leading five words of each client slot carry Four's id/state/counts/timer; the remaining ten are MoP objective storage Four does not populate. A flat range copy would therefore land legacy slot 1 on index 176, inside client slot 0, and scramble the log. TranslateSelfQuestLogIndex re-strides per slot instead. Three static_asserts tie the projection to the legacy PLAYER_QUEST_LOG_* constants and to MAX_QUEST_OFFSET so the two layouts cannot silently drift apart. The feed loop is ordered ahead of the visible-item feed because AppendSelfPlayerValuesBlock requires ascending legacy indices. Byte-exact test covers both ends of the block and asserts the flat-copy targets (176, 420) are absent; verified to fail when the translation is replaced with a flat copy. Adds a questlog_feed source mutation. ctest -R ^mop_(updateobject|self_values): 9/9 passed Release mangosd: built successfully Runtime validation remains pending: accept a quest from Marshal McBride (creature 197, Northshire), confirm it renders in the client quest log, and confirm it survives a relog. --- src/game/Object/ObjectUpdate.cpp | 23 +++++++- src/game/Server/MopUpdateObject.cpp | 21 +++++++ src/game/Server/MopUpdateObject.h | 16 ++++++ src/game/Server/tests/CMakeLists.txt | 2 +- .../tests/mop_self_values_source_test.cmake | 7 +++ .../Server/tests/mop_updateobject_test.cpp | 55 +++++++++++++++++++ 6 files changed, 122 insertions(+), 2 deletions(-) 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/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/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index 7c58d29ab..e32403465 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} 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_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); From 8509800b580372e0f7bdcb4997facb7307a606a4 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 09:46:21 +0100 Subject: [PATCH 24/28] [Tests] Track the declared world-database tuple mop_world_quest_interaction_packets_source pinned the BroadcastText-aware world-database requirement to 23.1.2, but revision_data.h.in has declared 23.2.31 since the database revision alignment. The check therefore asserted an obsolete tuple and failed on a clean tree; it predates the current upstream merge rather than being caused by it. Moves the pin, and the paired npc_db_content mutation, onto the declared tuple. The requirement is unchanged -- the database must be BroadcastText aware -- but the pin has to move with revision_data.h.in or the check silently guards the wrong version. ctest: 1076/1076 passed --- ...world_quest_interaction_packets_source_test.cmake | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 1bd226fb5..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 @@ -97,8 +97,8 @@ 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 @@ -261,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") From 7ffa8c3806839d1d14ccb76dec6a6b67f24a2160 Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 11:30:54 +0100 Subject: [PATCH 25/28] [Eluna] Advance to the restored Mists auction-hello routing Points the Eluna submodule at dc5059b, which restores WorldSession::SendAuctionHello routing in the Mists arm of SendAuctionMenu. Eluna master advanced to 8eaa22b along a line that never contained fd0b208a3, the commit this repository previously referenced, so the Four specific routing was lost rather than superseded. 8eaa22b also switched the inline packet to SMSG_AUCTION_HELLO, which emits the pre-18414 body under the 18414 opcode and is malformed to a real client. mop_auction_packets_source asserts the routing and fails without this. Upstream review is MangosServer/Eluna#9. ctest: 1076/1076 passed --- src/modules/Eluna | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Eluna b/src/modules/Eluna index 8eaa22bde..dc5059b7c 160000 --- a/src/modules/Eluna +++ b/src/modules/Eluna @@ -1 +1 @@ -Subproject commit 8eaa22bde6361ce589147dbf3ee8a9b3a3596d6a +Subproject commit dc5059b7cb9e26b169b5872fbec6f9e3cebfb2a8 From 170559ff7030e5837a142582014744a61b916afe Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 11:31:56 +0100 Subject: [PATCH 26/28] [Eluna] Re-point at the squash-merged auction-hello routing MangosServer/Eluna#9 was squash merged as e5b40f0, so the branch commit dc5059b this repository referenced is no longer an ancestor of Eluna master and its branch has been deleted upstream. The pointer would not have resolved from a fresh clone. Re-points at e5b40f0. Tree content is identical: git diff dc5059b e5b40f0 is empty. ctest: 1076/1076 passed --- src/modules/Eluna | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Eluna b/src/modules/Eluna index dc5059b7c..e5b40f0c9 160000 --- a/src/modules/Eluna +++ b/src/modules/Eluna @@ -1 +1 @@ -Subproject commit dc5059b7cb9e26b169b5872fbec6f9e3cebfb2a8 +Subproject commit e5b40f0c9bd658fc269ba3187cdecde453c91428 From f8a77ea4137a2e924539a57aca6c9fe71dfccf9f Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 11:51:58 +0100 Subject: [PATCH 27/28] [Player] Encode 18414 pre-resurrect notices Replaces the inherited legacy packed-GUID body for SMSG_PRE_RESURRECT 0x19C0 with the directly proved 18414 masked GUID layout in the owning Player::BuildPlayerRepop path; registers and admits the packet. Wire grammar, proved twice over. Dispatcher sub_659694 case 696 constructs the message via parser sub_709F6B, which reads one packed GUID and nothing else. Two independent readers agree on the order: the constructor helper sub_6E7875 and the class virtual deserialize slot sub_6D6EF4. mask bits [1, 7, 5, 2, 6, 0, 3, 4] bytes XOR [5, 1, 7, 0, 6, 4, 2, 3] Opcode identity is derived, not asserted. IDA labels the block calling sub_709F6B as jumptable case 696; the same number falls out of the selector arithmetic independently, since 0x19C0 & 0x125C == 0x1040 selects the +608 family and the free bits ~0x125C & 0x1FFF compact to 88. The model reproduces every neighbouring case in that table exactly (0x0CBF->319, 0x1E0E->578, 0x1E2E->586), and sub_709F6B inverts to exactly one opcode, so there is no shared-handler ambiguity. The NAME remains reference-consensus, not binary. KNOWN EVIDENCE GAP: the semantic terminal is unresolved. The consumer is reachable only through sub_6D1F55, a per-message Arxan guard trampoline whose target is assembled at runtime as (dword_1096FB4 - (v2 ^ v3)). This is not a shared stub that could be resolved once -- there are 881 distinct trampolines across 881 accepted opcodes, this one used exactly once. The GUID therefore carries the role Four already gave it rather than a binary-proved role; this wave changes the encoding, not the semantics. The inherited body could not have been read by an 18414 client at all, so this cannot regress behaviour. RED: build failed only because MopCompactPackets::BuildPreResurrect was absent GREEN: dense and sparse byte fixtures passed, including the shortened body when zero GUID bytes clear their mask bits ctest full suite: 1082/1082 passed (6 new hostile source mutations) Release mangosd: built successfully reference overlay: ACTIVE=397, DOC=438, DORMANT=685, idempotent git diff --check: clean canonical Wow.exe.i64 sha256 unchanged across all three IDA passes Runtime validation pending: die and release spirit, retain 0x19C0, and confirm the client reaches the ghost/pre-resurrect state with no malformed-packet disconnect. --- src/game/Object/PlayerDeath.cpp | 4 +- src/game/Object/Unit.h | 18 ++++++ src/game/Server/Opcodes.cpp | 1 + src/game/Server/Opcodes_reference.h | 6 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/tests/CMakeLists.txt | 2 + .../mop_compact_packets_source_test.cmake | 59 +++++++++++++++++++ .../Server/tests/mop_compact_packets_test.cpp | 23 ++++++++ 8 files changed, 109 insertions(+), 5 deletions(-) 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/Unit.h b/src/game/Object/Unit.h index c0fd33339..f6bd7ccb0 100644 --- a/src/game/Object/Unit.h +++ b/src/game/Object/Unit.h @@ -386,6 +386,24 @@ namespace MopCompactPackets 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 diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index f80be14e7..1053b8243 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -349,6 +349,7 @@ void InitializeOpcodes() 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"); diff --git a/src/game/Server/Opcodes_reference.h b/src/game/Server/Opcodes_reference.h index 66cd55d16..98755dac5 100644 --- a/src/game/Server/Opcodes_reference.h +++ b/src/game/Server/Opcodes_reference.h @@ -175,8 +175,8 @@ * TOTAL SMSG rows 925 * * SUBSYSTEM CONFIDENCE: high=365, low=221, medium=182, none=157 - * STATUS TOTALS: ACTIVE=396, DOC=438, DORMANT=686 - * SMSG: ACTIVE=256, DOC=271, DORMANT=398 + * STATUS TOTALS: ACTIVE=397, DOC=438, DORMANT=685 + * SMSG: ACTIVE=257, DOC=271, DORMANT=397 * CMSG: ACTIVE=140, DOC=167, DORMANT=288 */ @@ -1086,7 +1086,7 @@ typedef uint16_t uint16; * SMSG_SPLINE_MOVE_SET_FEATHER_FALL 0x1893 ACTIVE * SMSG_SPLINE_MOVE_SET_LAND_WALK 0x18B6 ACTIVE * SMSG_THREAT_REMOVE 0x1960 ACTIVE [high-conf] - * SMSG_PRE_RESURRECT 0x19C0 DORMANT + * SMSG_PRE_RESURRECT 0x19C0 ACTIVE [high-conf] * SMSG_MONSTER_MOVE 0x1A07 ACTIVE * SMSG_PLAYER_MOVE 0x1A32 ACTIVE * SMSG_PET_DISMISS_SOUND 0x1ABB DORMANT diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 65ec9c00d..89d46bc3f 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -388,6 +388,7 @@ static bool IsEnterWorldConverted(uint16 opcode) 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 diff --git a/src/game/Server/tests/CMakeLists.txt b/src/game/Server/tests/CMakeLists.txt index e32403465..a2aca7884 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -204,6 +204,8 @@ foreach(mutation IN ITEMS cancel_sender cancel_registration cancel_allowlist can 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} 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 cd00a6ab0..ec5f45899 100644 --- a/src/game/Server/tests/mop_compact_packets_source_test.cmake +++ b/src/game/Server/tests/mop_compact_packets_source_test.cmake @@ -2,6 +2,7 @@ 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) @@ -397,6 +398,36 @@ elseif(MUTATION STREQUAL "combo_reference") "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") @@ -800,3 +831,31 @@ string(REGEX MATCH "SMSG_UPDATE_COMBO_POINTS[ \t]+0x082F[ \t]+ACTIVE" combo_refe 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 00b2875cc..43f48dae5 100644 --- a/src/game/Server/tests/mop_compact_packets_test.cpp +++ b/src/game/Server/tests/mop_compact_packets_test.cpp @@ -564,6 +564,26 @@ static void test_combo_points_packet() })); } +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); @@ -593,6 +613,7 @@ static void test_opcode_values_are_framable() 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); @@ -622,6 +643,7 @@ static void test_opcode_values_are_framable() 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); } @@ -643,6 +665,7 @@ int main(int /*argc*/, char** /*argv*/) test_rune_packets(); test_threat_packets(); test_dismount_packet(); + test_pre_resurrect_packet(); test_combo_points_packet(); test_opcode_values_are_framable(); From 306dba043c23ff34e24d68fa3cf0a5943e61e4df Mon Sep 17 00:00:00 2001 From: MadMax Date: Mon, 27 Jul 2026 12:44:24 +0100 Subject: [PATCH 28/28] [Quests] Answer 18414 quest-NPC queries The 18414 client sends CMSG_QUEST_NPC_QUERY 0x1DAE on every quest accept, once per quest per session. The opcode was never promoted out of Opcodes_reference.h, so the server logged it as unhandled and answered nothing, even though the reply half SMSG_QUEST_NPC_QUERY_RESPONSE 0x036D was already carried in Opcodes.h. Request. Operator captures show a fixed 204-byte body of which only the leading uint32 quest id is initialised; the remainder is uninitialised client stack memory, byte-identical across captures within one run and containing x64 image pointers on the 64-bit client. The parser reads the one defined field and deliberately discards the rest rather than validating a length the client never populates. Three captures confirm the client queries one quest at a time: the body did not grow when a second quest was held. Response grammar, from client parser sub_6B8B3B -> sub_6B8A06: bits21 questCount x questCount: bits22 npcCount (flush) x questCount: uint32 questId, then npcCount x uint32 npcId Widths are read from the composing shifts: sub_6A29A8 shift 13 gives 21 bits, sub_695EBC shift 14 gives 22. Confirmed against real 18414 retail captures -- every SMSG_QUEST_NPC_QUERY_RESPONSE in the sniff corpus (1,296 quest entries) decodes under this reader leaving no residual byte, and the builder now reproduces one of those replies byte-for-byte at 90 bytes. NPC selection is ENDERS, established from retail rather than assumed. Quest 28508 is a chain hand-off with disjoint sets -- giver 42898, ender 44452 -- and retail returned 44452. Givers-only and deduplicated-union both fail that case outright; enders-only matches all fifteen corpus quests for which the world database has ender data. Answering by quest needs an inverse of m_CreatureQuestInvolvedRelations, built from the same rows at load so it cannot drift and adds no query or table-name coupling. Retail batches several quests into one reply although the client asks one at a time, so the builder takes a vector and a single-quest answer is the degenerate case of the same grammar, not a special form. RED: build failed only because ParseQuestNpcQueryRequest and BuildQuestNpcQueryResponse were absent GREEN: dense, empty-ender, no-quest and retail-shape fixtures passed ctest full suite: 1083/1083 passed Release mangosd: built successfully reference overlay: ACTIVE=399, DOC=437, DORMANT=684, idempotent git diff --check: clean Known limits, recorded rather than papered over. The corpus is entirely actor 0, so whether gameobject questgivers belong in this response is untested and deliberately not implemented. Quests whose ender rows are missing or wrong in the converted world database will return an empty list; 33147 is a known instance and the server already flags its giver row at startup. That is a content gap, not a protocol one. Runtime validation pending: accept quest 29080 (giver 197 Marshal McBride, ender 823) and confirm the reply carries 823; quest 29083 inverts the pair as a second confirmation. --- src/game/Object/ObjectMgr.h | 10 + src/game/Object/ObjectMgrQuestRelations.cpp | 10 + src/game/Server/Opcodes.cpp | 2 + src/game/Server/Opcodes.h | 1 + src/game/Server/Opcodes_reference.h | 10 +- src/game/Server/WorldSession.cpp | 1 + src/game/Server/WorldSession.h | 61 ++++++ src/game/Server/tests/CMakeLists.txt | 8 + .../Server/tests/mop_quest_npc_query_test.cpp | 179 ++++++++++++++++++ src/game/WorldHandlers/QueryHandler.cpp | 36 ++++ 10 files changed, 313 insertions(+), 5 deletions(-) create mode 100644 src/game/Server/tests/mop_quest_npc_query_test.cpp 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/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 1053b8243..23a70f91d 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -479,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 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 98755dac5..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=397, DOC=438, DORMANT=685 - * SMSG: ACTIVE=257, DOC=271, DORMANT=397 - * 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: @@ -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] @@ -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 89d46bc3f..3c04fa026 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -445,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; 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 a2aca7884..c8b22b299 100644 --- a/src/game/Server/tests/CMakeLists.txt +++ b/src/game/Server/tests/CMakeLists.txt @@ -508,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} 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/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;