mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
feat: parse Classic SMSG_INSPECT gear + implement temp weapon enchant timers
Classic 1.12 SMSG_INSPECT (wire 0x115): parse PackedGUID + 19×uint32 itemEntries to populate InspectResult and inspectedPlayerItemEntries_ cache, enabling gear inspection of other players on Classic servers. Triggers item queries for all filled slots so the inspect window shows names/ilevels. SMSG_ITEM_ENCHANT_TIME_UPDATE: parse itemGuid/slot/durationSec/playerGuid and store per-slot expire timestamps in tempEnchantTimers_. Fires 5min/1min chat warnings before expiry. getTempEnchantRemainingMs() helper queries live remaining time. Buff bar renders timed slot buttons (gold/teal/purple per slot) that pulse red below 60s — useful for Shaman imbues, Rogue poisons, whetstones and oils across all three expansions.
This commit is contained in:
parent
2f479c6230
commit
218d68e275
3 changed files with 178 additions and 4 deletions
|
|
@ -1451,6 +1451,17 @@ public:
|
|||
};
|
||||
const LevelUpDeltas& getLastLevelUpDeltas() const { return lastLevelUpDeltas_; }
|
||||
|
||||
// Temporary weapon enchant timers (from SMSG_ITEM_ENCHANT_TIME_UPDATE)
|
||||
// Slot: 0=main-hand, 1=off-hand, 2=ranged. Value: expire time (steady_clock ms).
|
||||
struct TempEnchantTimer {
|
||||
uint32_t slot = 0;
|
||||
uint64_t expireMs = 0; // std::chrono::steady_clock ms timestamp when it expires
|
||||
};
|
||||
const std::vector<TempEnchantTimer>& getTempEnchantTimers() const { return tempEnchantTimers_; }
|
||||
// Returns remaining ms for a given slot, or 0 if absent/expired.
|
||||
uint32_t getTempEnchantRemainingMs(uint32_t slot) const;
|
||||
static constexpr const char* kTempEnchantSlotNames[] = { "Main Hand", "Off Hand", "Ranged" };
|
||||
|
||||
// Other player level-up callback — fires when another player gains a level
|
||||
using OtherPlayerLevelUpCallback = std::function<void(uint64_t guid, uint32_t newLevel)>;
|
||||
void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); }
|
||||
|
|
@ -2808,6 +2819,7 @@ private:
|
|||
ChargeCallback chargeCallback_;
|
||||
LevelUpCallback levelUpCallback_;
|
||||
LevelUpDeltas lastLevelUpDeltas_;
|
||||
std::vector<TempEnchantTimer> tempEnchantTimers_;
|
||||
OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_;
|
||||
AchievementEarnedCallback achievementEarnedCallback_;
|
||||
AreaDiscoveryCallback areaDiscoveryCallback_;
|
||||
|
|
|
|||
|
|
@ -5698,9 +5698,56 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
|
||||
// ---- Misc consume ----
|
||||
case Opcode::SMSG_ITEM_ENCHANT_TIME_UPDATE: {
|
||||
// Format: uint64 itemGuid + uint32 slot + uint32 durationSec + uint64 playerGuid
|
||||
// slot: 0=main-hand, 1=off-hand, 2=ranged
|
||||
if (packet.getSize() - packet.getReadPos() < 24) {
|
||||
packet.setReadPos(packet.getSize()); break;
|
||||
}
|
||||
/*uint64_t itemGuid =*/ packet.readUInt64();
|
||||
uint32_t enchSlot = packet.readUInt32();
|
||||
uint32_t durationSec = packet.readUInt32();
|
||||
/*uint64_t playerGuid =*/ packet.readUInt64();
|
||||
|
||||
// Clamp to known slots (0-2)
|
||||
if (enchSlot > 2) { break; }
|
||||
|
||||
uint64_t nowMs = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
|
||||
if (durationSec == 0) {
|
||||
// Enchant expired / removed — erase the slot entry
|
||||
tempEnchantTimers_.erase(
|
||||
std::remove_if(tempEnchantTimers_.begin(), tempEnchantTimers_.end(),
|
||||
[enchSlot](const TempEnchantTimer& t) { return t.slot == enchSlot; }),
|
||||
tempEnchantTimers_.end());
|
||||
} else {
|
||||
uint64_t expireMs = nowMs + static_cast<uint64_t>(durationSec) * 1000u;
|
||||
bool found = false;
|
||||
for (auto& t : tempEnchantTimers_) {
|
||||
if (t.slot == enchSlot) { t.expireMs = expireMs; found = true; break; }
|
||||
}
|
||||
if (!found) tempEnchantTimers_.push_back({enchSlot, expireMs});
|
||||
|
||||
// Warn at important thresholds
|
||||
if (durationSec <= 60 && durationSec > 55) {
|
||||
const char* slotName = (enchSlot < 3) ? kTempEnchantSlotNames[enchSlot] : "weapon";
|
||||
char buf[80];
|
||||
std::snprintf(buf, sizeof(buf), "Weapon enchant (%s) expires in 1 minute!", slotName);
|
||||
addSystemChatMessage(buf);
|
||||
} else if (durationSec <= 300 && durationSec > 295) {
|
||||
const char* slotName = (enchSlot < 3) ? kTempEnchantSlotNames[enchSlot] : "weapon";
|
||||
char buf[80];
|
||||
std::snprintf(buf, sizeof(buf), "Weapon enchant (%s) expires in 5 minutes.", slotName);
|
||||
addSystemChatMessage(buf);
|
||||
}
|
||||
}
|
||||
LOG_DEBUG("SMSG_ITEM_ENCHANT_TIME_UPDATE: slot=", enchSlot, " dur=", durationSec, "s");
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_COMPLAIN_RESULT:
|
||||
case Opcode::SMSG_ITEM_REFUND_INFO_RESPONSE:
|
||||
case Opcode::SMSG_ITEM_ENCHANT_TIME_UPDATE:
|
||||
case Opcode::SMSG_LOOT_LIST:
|
||||
// Consume — not yet processed
|
||||
packet.setReadPos(packet.getSize());
|
||||
|
|
@ -6212,10 +6259,58 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
||||
// ---- Inspect (full character inspection) ----
|
||||
case Opcode::SMSG_INSPECT:
|
||||
packet.setReadPos(packet.getSize());
|
||||
// ---- Inspect (Classic 1.12 gear inspection) ----
|
||||
case Opcode::SMSG_INSPECT: {
|
||||
// Classic 1.12: PackedGUID + 19×uint32 itemEntries (EQUIPMENT_SLOT_END=19)
|
||||
// This opcode is only reachable on Classic servers; TBC/WotLK wire 0x115 maps to
|
||||
// SMSG_INSPECT_RESULTS_UPDATE which is handled separately.
|
||||
if (packet.getSize() - packet.getReadPos() < 2) {
|
||||
packet.setReadPos(packet.getSize()); break;
|
||||
}
|
||||
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (guid == 0) { packet.setReadPos(packet.getSize()); break; }
|
||||
|
||||
constexpr int kGearSlots = 19;
|
||||
size_t needed = kGearSlots * sizeof(uint32_t);
|
||||
if (packet.getSize() - packet.getReadPos() < needed) {
|
||||
packet.setReadPos(packet.getSize()); break;
|
||||
}
|
||||
|
||||
std::array<uint32_t, 19> items{};
|
||||
for (int s = 0; s < kGearSlots; ++s)
|
||||
items[s] = packet.readUInt32();
|
||||
|
||||
// Resolve player name
|
||||
auto ent = entityManager.getEntity(guid);
|
||||
std::string playerName = "Target";
|
||||
if (ent) {
|
||||
auto pl = std::dynamic_pointer_cast<Player>(ent);
|
||||
if (pl && !pl->getName().empty()) playerName = pl->getName();
|
||||
}
|
||||
|
||||
// Populate inspect result immediately (no talent data in Classic SMSG_INSPECT)
|
||||
inspectResult_.guid = guid;
|
||||
inspectResult_.playerName = playerName;
|
||||
inspectResult_.totalTalents = 0;
|
||||
inspectResult_.unspentTalents = 0;
|
||||
inspectResult_.talentGroups = 0;
|
||||
inspectResult_.activeTalentGroup = 0;
|
||||
inspectResult_.itemEntries = items;
|
||||
inspectResult_.enchantIds = {};
|
||||
|
||||
// Also cache for future talent-inspect cross-reference
|
||||
inspectedPlayerItemEntries_[guid] = items;
|
||||
|
||||
// Trigger item queries for non-empty slots
|
||||
for (int s = 0; s < kGearSlots; ++s) {
|
||||
if (items[s] != 0) queryItemInfo(items[s], 0);
|
||||
}
|
||||
|
||||
LOG_INFO("SMSG_INSPECT (Classic): ", playerName, " has gear in ",
|
||||
std::count_if(items.begin(), items.end(),
|
||||
[](uint32_t e) { return e != 0; }), "/19 slots");
|
||||
break;
|
||||
}
|
||||
|
||||
// ---- Multiple aggregated packets/moves ----
|
||||
case Opcode::SMSG_MULTIPLE_MOVES:
|
||||
|
|
@ -14383,6 +14478,19 @@ void GameHandler::cancelAura(uint32_t spellId) {
|
|||
socket->send(packet);
|
||||
}
|
||||
|
||||
uint32_t GameHandler::getTempEnchantRemainingMs(uint32_t slot) const {
|
||||
uint64_t nowMs = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
for (const auto& t : tempEnchantTimers_) {
|
||||
if (t.slot == slot) {
|
||||
return (t.expireMs > nowMs)
|
||||
? static_cast<uint32_t>(t.expireMs - nowMs) : 0u;
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
void GameHandler::handlePetSpells(network::Packet& packet) {
|
||||
const size_t remaining = packet.getSize() - packet.getReadPos();
|
||||
if (remaining < 8) {
|
||||
|
|
|
|||
|
|
@ -12036,6 +12036,60 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) {
|
|||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
// Temporary weapon enchant timers (Shaman imbues, Rogue poisons, whetstones, etc.)
|
||||
{
|
||||
const auto& timers = gameHandler.getTempEnchantTimers();
|
||||
if (!timers.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
static const ImVec4 kEnchantSlotColors[] = {
|
||||
ImVec4(0.9f, 0.6f, 0.1f, 1.0f), // main-hand: gold
|
||||
ImVec4(0.5f, 0.8f, 0.9f, 1.0f), // off-hand: teal
|
||||
ImVec4(0.7f, 0.5f, 0.9f, 1.0f), // ranged: purple
|
||||
};
|
||||
uint64_t enchNowMs = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
|
||||
for (const auto& t : timers) {
|
||||
if (t.slot > 2) continue;
|
||||
uint64_t remMs = (t.expireMs > enchNowMs) ? (t.expireMs - enchNowMs) : 0;
|
||||
if (remMs == 0) continue;
|
||||
|
||||
ImVec4 col = kEnchantSlotColors[t.slot];
|
||||
// Flash red when < 60s remaining
|
||||
if (remMs < 60000) {
|
||||
float pulse = 0.6f + 0.4f * std::sin(
|
||||
static_cast<float>(ImGui::GetTime()) * 4.0f);
|
||||
col = ImVec4(pulse, 0.2f, 0.1f, 1.0f);
|
||||
}
|
||||
|
||||
// Format remaining time
|
||||
uint32_t secs = static_cast<uint32_t>((remMs + 999) / 1000);
|
||||
char timeStr[16];
|
||||
if (secs >= 3600)
|
||||
snprintf(timeStr, sizeof(timeStr), "%dh%02dm", secs / 3600, (secs % 3600) / 60);
|
||||
else if (secs >= 60)
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", secs / 60, secs % 60);
|
||||
else
|
||||
snprintf(timeStr, sizeof(timeStr), "%ds", secs);
|
||||
|
||||
ImGui::PushID(static_cast<int>(t.slot) + 5000);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, col);
|
||||
char label[40];
|
||||
snprintf(label, sizeof(label), "~%s %s",
|
||||
game::GameHandler::kTempEnchantSlotNames[t.slot], timeStr);
|
||||
ImGui::Button(label, ImVec2(-1, 16));
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Temporary weapon enchant: %s\nRemaining: %s",
|
||||
game::GameHandler::kTempEnchantSlotNames[t.slot],
|
||||
timeStr);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue