Handle missing WotLK packets: health/power updates, mirror timers, combo points, loot roll, titles, phase shift

- SMSG_HEALTH_UPDATE / SMSG_POWER_UPDATE: update entity HP/power via entityManager
- SMSG_UPDATE_WORLD_STATE: single world state variable update (companion to INIT)
- SMSG_UPDATE_COMBO_POINTS: store comboPoints_/comboTarget_ in GameHandler
- SMSG_START_MIRROR_TIMER / SMSG_STOP_MIRROR_TIMER / SMSG_PAUSE_MIRROR_TIMER: breath/fatigue/feign timer state
- MirrorTimer struct + getMirrorTimer() public getter; renderMirrorTimers() draws colored breath/fatigue bars above cast bar
- SMSG_CAST_RESULT: WotLK extended cast result; clear cast bar and show reason on failure (result != 0)
- SMSG_SPELL_FAILED_OTHER / SMSG_PROCRESIST: consume silently
- SMSG_LOOT_START_ROLL: correct trigger for Need/Greed popup (replaces rollType=128 heuristic)
- SMSG_STABLE_RESULT: show pet stable feedback in system chat (store/retrieve/buy slot/error)
- SMSG_TITLE_EARNED: system chat notification for title earned/removed
- SMSG_PLAYERBOUND / SMSG_BINDER_CONFIRM: hearthstone binding notification
- SMSG_SET_PHASE_SHIFT: consume (WotLK phasing, no client action needed)
- SMSG_TOGGLE_XP_GAIN: system chat notification
This commit is contained in:
Kelsi 2026-03-09 14:30:48 -07:00
parent 6df36f4588
commit bd3bd1b5a6
4 changed files with 301 additions and 0 deletions

View file

@ -865,6 +865,23 @@ public:
uint32_t getWorldStateMapId() const { return worldStateMapId_; }
uint32_t getWorldStateZoneId() const { return worldStateZoneId_; }
// Mirror timers (0=fatigue, 1=breath, 2=feigndeath)
struct MirrorTimer {
int32_t value = 0;
int32_t maxValue = 0;
int32_t scale = 0; // +1 = counting up, -1 = counting down
bool paused = false;
bool active = false;
};
const MirrorTimer& getMirrorTimer(int type) const {
static MirrorTimer empty;
return (type >= 0 && type < 3) ? mirrorTimers_[type] : empty;
}
// Combo points
uint8_t getComboPoints() const { return comboPoints_; }
uint64_t getComboTarget() const { return comboTarget_; }
struct FactionStandingInit {
uint8_t flags = 0;
int32_t standing = 0;
@ -1655,6 +1672,13 @@ private:
uint32_t instanceDifficulty_ = 0;
bool instanceIsHeroic_ = false;
// Mirror timers (0=fatigue, 1=breath, 2=feigndeath)
MirrorTimer mirrorTimers_[3];
// Combo points (rogues/druids)
uint8_t comboPoints_ = 0;
uint64_t comboTarget_ = 0;
// Instance / raid lockouts
std::vector<InstanceLockout> instanceLockouts_;

View file

@ -201,6 +201,7 @@ private:
void renderBagBar(game::GameHandler& gameHandler);
void renderXpBar(game::GameHandler& gameHandler);
void renderCastBar(game::GameHandler& gameHandler);
void renderMirrorTimers(game::GameHandler& gameHandler);
void renderCombatText(game::GameHandler& gameHandler);
void renderPartyFrames(game::GameHandler& gameHandler);
void renderGroupInvitePopup(game::GameHandler& gameHandler);

View file

@ -1596,6 +1596,229 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
}
// ---- Entity health/power delta updates ----
case Opcode::SMSG_HEALTH_UPDATE: {
// packed_guid + uint32 health
if (packet.getSize() - packet.getReadPos() < 2) break;
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 4) break;
uint32_t hp = packet.readUInt32();
auto entity = entityManager.getEntity(guid);
if (auto* unit = dynamic_cast<Unit*>(entity.get())) {
unit->setHealth(hp);
}
break;
}
case Opcode::SMSG_POWER_UPDATE: {
// packed_guid + uint8 powerType + uint32 value
if (packet.getSize() - packet.getReadPos() < 2) break;
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 5) break;
uint8_t powerType = packet.readUInt8();
uint32_t value = packet.readUInt32();
auto entity = entityManager.getEntity(guid);
if (auto* unit = dynamic_cast<Unit*>(entity.get())) {
unit->setPowerByType(powerType, value);
}
break;
}
// ---- World state single update ----
case Opcode::SMSG_UPDATE_WORLD_STATE: {
// uint32 field + uint32 value
if (packet.getSize() - packet.getReadPos() < 8) break;
uint32_t field = packet.readUInt32();
uint32_t value = packet.readUInt32();
worldStates_[field] = value;
LOG_DEBUG("SMSG_UPDATE_WORLD_STATE: field=", field, " value=", value);
break;
}
// ---- Combo points ----
case Opcode::SMSG_UPDATE_COMBO_POINTS: {
// packed_guid (target) + uint8 points
if (packet.getSize() - packet.getReadPos() < 2) break;
uint64_t target = UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 1) break;
comboPoints_ = packet.readUInt8();
comboTarget_ = target;
LOG_DEBUG("SMSG_UPDATE_COMBO_POINTS: target=0x", std::hex, target,
std::dec, " points=", static_cast<int>(comboPoints_));
break;
}
// ---- Mirror timers (breath/fatigue/feign death) ----
case Opcode::SMSG_START_MIRROR_TIMER: {
// uint32 type + int32 value + int32 maxValue + int32 scale + uint32 tracker + uint8 paused
if (packet.getSize() - packet.getReadPos() < 21) break;
uint32_t type = packet.readUInt32();
int32_t value = static_cast<int32_t>(packet.readUInt32());
int32_t maxV = static_cast<int32_t>(packet.readUInt32());
int32_t scale = static_cast<int32_t>(packet.readUInt32());
/*uint32_t tracker =*/ packet.readUInt32();
uint8_t paused = packet.readUInt8();
if (type < 3) {
mirrorTimers_[type].value = value;
mirrorTimers_[type].maxValue = maxV;
mirrorTimers_[type].scale = scale;
mirrorTimers_[type].paused = (paused != 0);
mirrorTimers_[type].active = true;
}
break;
}
case Opcode::SMSG_STOP_MIRROR_TIMER: {
// uint32 type
if (packet.getSize() - packet.getReadPos() < 4) break;
uint32_t type = packet.readUInt32();
if (type < 3) {
mirrorTimers_[type].active = false;
mirrorTimers_[type].value = 0;
}
break;
}
case Opcode::SMSG_PAUSE_MIRROR_TIMER: {
// uint32 type + uint8 paused
if (packet.getSize() - packet.getReadPos() < 5) break;
uint32_t type = packet.readUInt32();
uint8_t paused = packet.readUInt8();
if (type < 3) {
mirrorTimers_[type].paused = (paused != 0);
}
break;
}
// ---- Cast result (WotLK extended cast failed) ----
case Opcode::SMSG_CAST_RESULT:
// WotLK: uint8 castCount + uint32 spellId + uint8 result [+ optional extra]
// If result == 0, the spell successfully began; otherwise treat like SMSG_CAST_FAILED.
if (packet.getSize() - packet.getReadPos() >= 6) {
/*uint8_t castCount =*/ packet.readUInt8();
/*uint32_t spellId =*/ packet.readUInt32();
uint8_t result = packet.readUInt8();
if (result != 0) {
// Failure — clear cast bar and show message
casting = false;
currentCastSpellId = 0;
castTimeRemaining = 0.0f;
const char* reason = getSpellCastResultString(result, -1);
MessageChatData msg;
msg.type = ChatType::SYSTEM;
msg.language = ChatLanguage::UNIVERSAL;
msg.message = reason ? reason
: ("Spell cast failed (error " + std::to_string(result) + ")");
addLocalChatMessage(msg);
}
}
break;
// ---- Spell failed on another unit ----
case Opcode::SMSG_SPELL_FAILED_OTHER:
// packed_guid + uint8 castCount + uint32 spellId + uint8 reason — just consume
packet.setReadPos(packet.getSize());
break;
// ---- Spell proc resist log ----
case Opcode::SMSG_PROCRESIST:
// guid(8) + guid(8) + uint32 spellId + uint8 logSchoolMask — just consume
packet.setReadPos(packet.getSize());
break;
// ---- Loot start roll (Need/Greed popup trigger) ----
case Opcode::SMSG_LOOT_START_ROLL: {
// uint64 objectGuid + uint32 mapId + uint32 lootSlot + uint32 itemId
// + uint32 randomSuffix + uint32 randomPropId + uint32 countdown + uint8 voteMask
if (packet.getSize() - packet.getReadPos() < 33) break;
uint64_t objectGuid = packet.readUInt64();
/*uint32_t mapId =*/ packet.readUInt32();
uint32_t slot = packet.readUInt32();
uint32_t itemId = packet.readUInt32();
/*uint32_t randSuffix =*/ packet.readUInt32();
/*uint32_t randProp =*/ packet.readUInt32();
/*uint32_t countdown =*/ packet.readUInt32();
/*uint8_t voteMask =*/ packet.readUInt8();
// Trigger the roll popup for local player
pendingLootRollActive_ = true;
pendingLootRoll_.objectGuid = objectGuid;
pendingLootRoll_.slot = slot;
pendingLootRoll_.itemId = itemId;
auto* info = getItemInfo(itemId);
pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId);
pendingLootRoll_.itemQuality = info ? static_cast<uint8_t>(info->quality) : 0;
LOG_INFO("SMSG_LOOT_START_ROLL: item=", itemId, " (", pendingLootRoll_.itemName,
") slot=", slot);
break;
}
// ---- Pet stable result ----
case Opcode::SMSG_STABLE_RESULT: {
// uint8 result
if (packet.getSize() - packet.getReadPos() < 1) break;
uint8_t result = packet.readUInt8();
const char* msg = nullptr;
switch (result) {
case 0x01: msg = "Pet stored in stable."; break;
case 0x06: msg = "Pet retrieved from stable."; break;
case 0x07: msg = "Stable slot purchased."; break;
case 0x08: msg = "Stable list updated."; break;
case 0x09: msg = "Stable failed: not enough money or other error."; break;
default: break;
}
if (msg) addSystemChatMessage(msg);
LOG_INFO("SMSG_STABLE_RESULT: result=", static_cast<int>(result));
break;
}
// ---- Title earned ----
case Opcode::SMSG_TITLE_EARNED: {
// uint32 titleBitIndex + uint32 isLost
if (packet.getSize() - packet.getReadPos() < 8) break;
uint32_t titleBit = packet.readUInt32();
uint32_t isLost = packet.readUInt32();
char buf[128];
std::snprintf(buf, sizeof(buf),
isLost ? "Title removed (ID %u)." : "Title earned (ID %u)!",
titleBit);
addSystemChatMessage(buf);
LOG_INFO("SMSG_TITLE_EARNED: id=", titleBit, " lost=", isLost);
break;
}
// ---- Hearthstone binding ----
case Opcode::SMSG_PLAYERBOUND: {
// uint64 binderGuid + uint32 mapId + uint32 zoneId
if (packet.getSize() - packet.getReadPos() < 16) break;
/*uint64_t binderGuid =*/ packet.readUInt64();
uint32_t mapId = packet.readUInt32();
uint32_t zoneId = packet.readUInt32();
char buf[128];
std::snprintf(buf, sizeof(buf),
"Your home location has been set (map %u, zone %u).", mapId, zoneId);
addSystemChatMessage(buf);
break;
}
case Opcode::SMSG_BINDER_CONFIRM: {
// uint64 npcGuid — server asking client to confirm bind at innkeeper
packet.setReadPos(packet.getSize());
break;
}
// ---- Phase shift (WotLK phasing) ----
case Opcode::SMSG_SET_PHASE_SHIFT: {
// uint32 phaseFlags [+ packed guid + uint16 count + repeated uint16 phaseIds]
// Just consume; phasing doesn't require action from client in WotLK
packet.setReadPos(packet.getSize());
break;
}
// ---- XP gain toggle ----
case Opcode::SMSG_TOGGLE_XP_GAIN: {
// uint8 enabled
if (packet.getSize() - packet.getReadPos() < 1) break;
uint8_t enabled = packet.readUInt8();
addSystemChatMessage(enabled ? "XP gain enabled." : "XP gain disabled.");
break;
}
// ---- Creature Movement ----
case Opcode::SMSG_MONSTER_MOVE:
handleMonsterMove(packet);

View file

@ -393,6 +393,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
renderBagBar(gameHandler);
renderXpBar(gameHandler);
renderCastBar(gameHandler);
renderMirrorTimers(gameHandler);
renderCombatText(gameHandler);
renderPartyFrames(gameHandler);
renderGroupInvitePopup(gameHandler);
@ -4176,6 +4177,58 @@ void GameScreen::renderCastBar(game::GameHandler& gameHandler) {
ImGui::PopStyleVar();
}
// ============================================================
// Mirror Timers (breath / fatigue / feign death)
// ============================================================
void GameScreen::renderMirrorTimers(game::GameHandler& gameHandler) {
auto* window = core::Application::getInstance().getWindow();
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
static const struct { const char* label; ImVec4 color; } kTimerInfo[3] = {
{ "Fatigue", ImVec4(0.8f, 0.4f, 0.1f, 1.0f) },
{ "Breath", ImVec4(0.2f, 0.5f, 1.0f, 1.0f) },
{ "Feign", ImVec4(0.6f, 0.6f, 0.6f, 1.0f) },
};
float barW = 280.0f;
float barH = 36.0f;
float barX = (screenW - barW) / 2.0f;
float baseY = screenH - 160.0f; // Just above the cast bar slot
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoInputs;
for (int i = 0; i < 3; ++i) {
const auto& t = gameHandler.getMirrorTimer(i);
if (!t.active || t.maxValue <= 0) continue;
float frac = static_cast<float>(t.value) / static_cast<float>(t.maxValue);
frac = std::max(0.0f, std::min(1.0f, frac));
char winId[32];
std::snprintf(winId, sizeof(winId), "##MirrorTimer%d", i);
ImGui::SetNextWindowPos(ImVec2(barX, baseY - i * (barH + 4.0f)), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.88f));
if (ImGui::Begin(winId, nullptr, flags)) {
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, kTimerInfo[i].color);
char overlay[48];
float sec = static_cast<float>(t.value) / 1000.0f;
std::snprintf(overlay, sizeof(overlay), "%s %.0fs", kTimerInfo[i].label, sec);
ImGui::ProgressBar(frac, ImVec2(-1, 20), overlay);
ImGui::PopStyleColor();
}
ImGui::End();
ImGui::PopStyleColor();
ImGui::PopStyleVar();
}
}
// ============================================================
// Floating Combat Text (Phase 2)
// ============================================================