Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fff72d7
[Spells] Encode 18414 cooldown events
MadMaxMangos Jul 26, 2026
edabc02
[Spells] Encode 18414 instakill logs
MadMaxMangos Jul 26, 2026
bcaf8bb
[Spells] Encode 18414 energize logs
MadMaxMangos Jul 26, 2026
bcc97a4
[Spells] Encode 18414 heal logs
MadMaxMangos Jul 26, 2026
9f35104
[Spells] Encode 18414 damage-shield logs
MadMaxMangos Jul 26, 2026
fcc5c08
[Spells] Encode 18414 spell-miss logs
MadMaxMangos Jul 26, 2026
520c8d5
[Spells] Encode 18414 item cooldowns
MadMaxMangos Jul 26, 2026
5abb909
[Death] Send 18414 durability loss notice
MadMaxMangos Jul 26, 2026
533014a
[Player] Send 18414 exploration experience
MadMaxMangos Jul 26, 2026
7a26140
[Spells] Encode 18414 clear target
MadMaxMangos Jul 26, 2026
95d6d5e
[Player] Encode 18414 level-up info
MadMaxMangos Jul 26, 2026
c2d6a78
[Player] Encode 18414 experience gain
MadMaxMangos Jul 26, 2026
8aa5697
[Combat] Encode 18414 party kill log
MadMaxMangos Jul 26, 2026
276a884
[Chat] Encode 18414 error notices
MadMaxMangos Jul 26, 2026
5082e31
[Player] Encode 18414 duel state
MadMaxMangos Jul 26, 2026
af2c1cb
[Player] Encode 18414 duel results
MadMaxMangos Jul 26, 2026
057963b
[Player] Encode 18414 mirror timers
MadMaxMangos Jul 27, 2026
95697cc
[Spells] Encode 18414 channel updates
MadMaxMangos Jul 27, 2026
057047e
[Player] Encode 18414 rune updates
MadMaxMangos Jul 27, 2026
a6e5d8c
[Combat] Encode 18414 threat updates
MadMaxMangos Jul 27, 2026
b8b86df
[Movement] Encode 18414 dismount notices
MadMaxMangos Jul 27, 2026
8deec78
[Player] Encode 18414 combo-point updates
MadMaxMangos Jul 27, 2026
f4762f2
Merge origin/master into feat/mop-opcode-campaign
MadMaxMangos Jul 27, 2026
a61d113
[MoP] Project quest-log fields to the client
MadMaxMangos Jul 27, 2026
8509800
[Tests] Track the declared world-database tuple
MadMaxMangos Jul 27, 2026
7ffa8c3
[Eluna] Advance to the restored Mists auction-hello routing
MadMaxMangos Jul 27, 2026
170559f
[Eluna] Re-point at the squash-merged auction-hello routing
MadMaxMangos Jul 27, 2026
f8a77ea
[Player] Encode 18414 pre-resurrect notices
MadMaxMangos Jul 27, 2026
306dba0
[Quests] Answer 18414 quest-NPC queries
MadMaxMangos Jul 27, 2026
8a702f9
Merge origin/master into feat/mop-opcode-campaign (Map spawn difficul…
MadMaxMangos Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/game/Object/ObjectMgr.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -1497,6 +1506,7 @@ class ObjectMgr

QuestRelationsMap m_CreatureQuestRelations;
QuestRelationsMap m_CreatureQuestInvolvedRelations;
QuestRelationsMap m_QuestEnderCreatures;
QuestRelationsMap m_GOQuestRelations;
QuestRelationsMap m_GOQuestInvolvedRelations;

Expand Down
10 changes: 10 additions & 0 deletions src/game/Object/ObjectMgrQuestRelations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
23 changes: 22 additions & 1 deletion src/game/Object/ObjectUpdate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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)
Comment on lines +622 to +624

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Seed projected quests during login

When a player logs in with active quests, this changed-value loop never sends their quest IDs: Player::AddToWorld() clears m_changedValues, and Map::SendInitSelf() seeds visible-item, inventory, and skill fields but not SelfQuestLogSourceStart..SelfQuestLogFieldCount; SendInitialQuestSetup() only sends completed-quest bits. Consequently, persisted active quests remain absent from the client quest log after every relog until their IDs happen to change, so the login self-values packet must also include the stored quest-log fields.

Useful? React with 👍 / 👎.

{
addIfChanged(i);
}

// Local equipment changes use the same public 18414 visible-item
// projection as updates sent to nearby observers.
for (uint16 i = MopUpdateObject::ObserverVisibleItemSourceStart;
Expand Down
54 changes: 23 additions & 31 deletions src/game/Object/Player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -2453,22 +2452,17 @@ void Player::GiveLevel(uint32 level)
uint32 basehp = 0, basemana = 0;
sObjectMgr.GetPlayerClassLevelInfo(getClass(), level, basehp, basemana);

// send levelup info to client
WorldPacket data(SMSG_LEVELUP_INFO, (4 + 4 + MAX_STORED_POWERS * 4 + MAX_STATS * 4));
data << uint32(level);
data << uint32(int32(basehp) - int32(GetCreateHealth()));
// for(int i = 0; i < MAX_POWERS; ++i) // Powers loop (0-4)
data << uint32(int32(basemana) - int32(GetCreateMana()));
data << uint32(0);
data << uint32(0);
data << uint32(0);
data << uint32(0);
// end for
for (int i = STAT_STRENGTH; i < MAX_STATS; ++i) // Stats loop (0-4)
{
data << uint32(int32(info.stats[i]) - GetCreateStat(Stats(i)));
}
MopProgressionPackets::LevelUpInfo packetInfo;
packetInfo.level = level;
packetInfo.healthDelta = uint32(int32(basehp) - int32(GetCreateHealth()));
packetInfo.powerDeltas[POWER_MANA] =
uint32(int32(basemana) - int32(GetCreateMana()));
for (int i = STAT_STRENGTH; i < MAX_STATS; ++i)
packetInfo.statDeltas[i] =
uint32(int32(info.stats[i]) - GetCreateStat(Stats(i)));

WorldPacket data;
MopProgressionPackets::BuildLevelUpInfo(data, packetInfo);
GetSession()->SendPacket(&data);

SetUInt32Value(PLAYER_NEXT_LEVEL_XP, sObjectMgr.GetXPForLevel(level));
Expand Down Expand Up @@ -4118,9 +4112,8 @@ void Player::Customize(ObjectGuid guid, uint8 gender, uint8 skin, uint8 face, ui
*/
void Player::SendExplorationExperience(uint32 Area, uint32 Experience)
{
WorldPacket data(SMSG_EXPLORATION_EXPERIENCE, 8);
data << uint32(Area);
data << uint32(Experience);
WorldPacket data;
MopAreaTriggerPackets::BuildExplorationExperience(data, Area, Experience);
GetSession()->SendPacket(&data);
}

Expand Down Expand Up @@ -4584,9 +4577,8 @@ void Player::ApplyEquipCooldown(Item* pItem)

AddSpellCooldown(spellData.SpellId, pItem->GetEntry(), time(NULL) + 30);

WorldPacket data(SMSG_ITEM_COOLDOWN, 12);
data << pItem->GetObjectGuid();
data << uint32(spellData.SpellId);
WorldPacket data;
MopSpellPackets::BuildItemCooldown(data, pItem->GetObjectGuid(), spellData.SpellId);
GetSession()->SendPacket(&data);
}
}
Expand Down
198 changes: 198 additions & 0 deletions src/game/Object/Player.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -701,6 +708,187 @@ namespace MopReputationPackets
}
}

namespace MopProgressionPackets
{
struct ExperienceGain
{
ObjectGuid sourceGuid;
uint32 totalExperience = 0;
uint8 type = 0;
bool hasBaseExperience = false;
uint32 baseExperience = 0;
};

inline void BuildExperienceGain(WorldPacket& out,
ExperienceGain const& info)
{
out.Initialize(SMSG_LOG_XPGAIN, 19);

// Wow.exe 18414 reader sub_6F7E25 uses an inverted presence bit for
// base XP. The current backend has no group-rate or recruit-a-friend
// inputs, so their directly verified neutral bits are emitted here.
out.WriteBit(!info.hasBaseExperience);
out.WriteGuidMask<1, 2, 7, 4, 3>(info.sourceGuid);
out.WriteBit(false); // no recruit-a-friend bonus
out.WriteGuidMask<0, 5, 6>(info.sourceGuid);
out.WriteBit(true); // default group rate 1.0; no float follows
out.FlushBits();

out.WriteGuidBytes<4, 2>(info.sourceGuid);
out << info.type;
out.WriteGuidBytes<7, 1, 3, 6>(info.sourceGuid);
out << info.totalExperience;
if (info.hasBaseExperience)
out << info.baseExperience;
out.WriteGuidBytes<0, 5>(info.sourceGuid);
}

struct LevelUpInfo
{
uint32 talentDelta = 0;
uint32 healthDelta = 0;
std::array<uint32, MAX_STATS> statDeltas = {{}};
uint32 level = 0;
std::array<uint32, MAX_STORED_POWERS> powerDeltas = {{}};
};

inline void BuildLevelUpInfo(WorldPacket& out, LevelUpInfo const& info)
{
out.Initialize(SMSG_LEVELUP_INFO, 13 * sizeof(uint32));

// Wow.exe 18414 reader sub_6BAC39 consumes thirteen uint32 values.
// Terminal sub_7B12E9 maps these slots to PLAYER_LEVEL_UP arguments.
out << info.talentDelta << info.healthDelta;
for (uint32 delta : info.statDeltas)
out << delta;
out << info.level;
for (uint32 delta : info.powerDeltas)
out << delta;
}
}

namespace MopComboPointPackets
{
inline void BuildUpdate(WorldPacket& out, ObjectGuid target, uint8 points)
{
out.Initialize(SMSG_UPDATE_COMBO_POINTS, 10);

// Wow.exe 18414 reader sub_6E2BC4 consumes six target-GUID bytes,
// the raw combo-point byte, then the final two target-GUID bytes.
// Terminal sub_CCA14B publishes the recovered GUID/value pair.
out.WriteGuidMask<0, 5, 6, 3, 7, 4, 1, 2>(target);
out.WriteGuidBytes<5, 6, 4, 7, 3, 0>(target);
out << points;
out.WriteGuidBytes<2, 1>(target);
}
}

namespace MopDuelPackets
{
inline void BuildRequested(WorldPacket& out, ObjectGuid arbiter,
ObjectGuid initiator)
{
out.Initialize(SMSG_DUEL_REQUESTED, 18);

// Reader sub_6CA34C interleaves the duel flag and initiator masks.
out.WriteGuidMask<5>(arbiter);
out.WriteGuidMask<4, 2, 7>(initiator);
out.WriteGuidMask<0>(arbiter);
out.WriteGuidMask<5>(initiator);
out.WriteGuidMask<4, 6>(arbiter);
out.WriteGuidMask<1, 3, 6>(initiator);
out.WriteGuidMask<7, 3, 2, 1>(arbiter);
out.WriteGuidMask<0>(initiator);
out.FlushBits();

out.WriteGuidBytes<5, 3>(arbiter);
out.WriteGuidBytes<7, 4>(initiator);
out.WriteGuidBytes<7>(arbiter);
out.WriteGuidBytes<3, 6, 0>(initiator);
out.WriteGuidBytes<4>(arbiter);
out.WriteGuidBytes<2, 1>(initiator);
out.WriteGuidBytes<0, 2, 6, 1>(arbiter);
out.WriteGuidBytes<5>(initiator);
}

inline void BuildOutOfBounds(WorldPacket& out)
{
// Wow.exe 18414 routes 0x001A through the fieldless reader sub_6BC12D.
out.Initialize(SMSG_DUEL_OUTOFBOUNDS, 0);
}

inline void BuildInBounds(WorldPacket& out)
{
// Wow.exe 18414 routes 0x163A through the same fieldless reader.
out.Initialize(SMSG_DUEL_INBOUNDS, 0);
}

inline void BuildComplete(WorldPacket& out, bool completed)
{
out.Initialize(SMSG_DUEL_COMPLETE, 1);

// Reader sub_6D3E7C consumes exactly one MSB-first completion bit.
out.WriteBit(completed);
out.FlushBits();
}

inline void BuildCountdown(WorldPacket& out, uint32 milliseconds)
{
out.Initialize(SMSG_DUEL_COUNTDOWN, sizeof(milliseconds));

// Reader sub_6D9F28 consumes one uint32; terminal sub_9BFFD4
// divides it by 1000 before updating the duel countdown UI.
out << milliseconds;
}

inline bool BuildWinner(WorldPacket& out, bool retreat,
std::string const& winnerName, uint32 winnerRealmAddress,
std::string const& loserName, uint32 loserRealmAddress)
{
if (winnerName.size() > 63 || loserName.size() > 63)
return false;

out.Initialize(SMSG_DUEL_WINNER,
2 + 2 * sizeof(uint32) + winnerName.size() + loserName.size());

// Reader sub_6CFDCC consumes the outcome followed by two six-bit
// lengths. Its realm-address fields are crossed around the names;
// terminal sub_9C0069 pairs them back as winner then loser.
out.WriteBit(retreat);
out.WriteBits(uint32(winnerName.size()), 6);
out.WriteBits(uint32(loserName.size()), 6);
out.FlushBits();

out << loserRealmAddress;
out.append(winnerName.c_str(), winnerName.size());
out << winnerRealmAddress;
out.append(loserName.c_str(), loserName.size());
return true;
}
}

namespace MopMirrorTimerPackets
{
inline void BuildStart(WorldPacket& out, uint32 type, uint32 maxValue,
uint32 currentValue, int32 regeneration, uint32 spellId, bool paused)
{
out.Initialize(SMSG_START_MIRROR_TIMER, 21);

// Wow.exe 18414 reader sub_6F16F9 consumes five uint32 values in
// max/spell/current/regen/type order, then one MSB-first pause bit.
out << maxValue << spellId << currentValue << uint32(regeneration) << type;
out.WriteBit(paused);
out.FlushBits();
}

inline void BuildStop(WorldPacket& out, uint32 type)
{
// Wow.exe 18414 reader sub_6D9F28 consumes only the timer type.
out.Initialize(SMSG_STOP_MIRROR_TIMER, sizeof(type));
out << type;
}
}

namespace MopAreaTriggerPackets
{
struct Request
Expand Down Expand Up @@ -749,6 +937,16 @@ namespace MopAreaTriggerPackets
// ERR_CORPSE_IS_NOT_IN_INSTANCE.
out.Initialize(SMSG_AREA_TRIGGER_NO_CORPSE, 0);
}

inline void BuildExplorationExperience(WorldPacket& out, uint32 areaId,
uint32 experience)
{
out.Initialize(SMSG_EXPLORATION_EXPERIENCE, 8);

// Wow.exe 18414 reader sub_6BB9C1 consumes the area-table key first;
// terminal sub_7B1384 then displays the second uint32 as awarded XP.
out << areaId << experience;
}
}

namespace MopQuestPackets
Expand Down
5 changes: 2 additions & 3 deletions src/game/Object/PlayerCombo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading