mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
26 commits
0830215b31
...
e24c39f4be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e24c39f4be | ||
|
|
ebc7d66dfe | ||
|
|
5172c07e15 | ||
|
|
533831e18d | ||
|
|
72993121ab | ||
|
|
22742fedb8 | ||
|
|
a6fe5662c8 | ||
|
|
fa82d32a9f | ||
|
|
114478271e | ||
|
|
a9e0a99f2b | ||
|
|
d7059c66dc | ||
|
|
6b7975107e | ||
|
|
120c2967eb | ||
|
|
bc2085b0fc | ||
|
|
bda5bb0a2b | ||
|
|
90edb3bc07 | ||
|
|
29c938dec2 | ||
|
|
9d1fb39363 | ||
|
|
5230815353 | ||
|
|
595ea466c2 | ||
|
|
e68a1fa2ec | ||
|
|
9600dd40e3 | ||
|
|
1ae4cfaf3f | ||
|
|
f4d705738b | ||
|
|
ae56f2eb80 | ||
|
|
f88d90ee88 |
13 changed files with 555 additions and 46 deletions
|
|
@ -37,6 +37,8 @@
|
|||
"PLAYER_FIELD_BANKBAG_SLOT_1": 784,
|
||||
"PLAYER_SKILL_INFO_START": 928,
|
||||
"PLAYER_EXPLORED_ZONES_START": 1312,
|
||||
"PLAYER_FIELD_HONOR_CURRENCY": 1505,
|
||||
"PLAYER_FIELD_ARENA_CURRENCY": 1506,
|
||||
"GAMEOBJECT_DISPLAYID": 8,
|
||||
"ITEM_FIELD_STACK_COUNT": 14,
|
||||
"ITEM_FIELD_DURABILITY": 60,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@
|
|||
"PLAYER_RANGED_CRIT_PERCENTAGE": 1030,
|
||||
"PLAYER_SPELL_CRIT_PERCENTAGE1": 1032,
|
||||
"PLAYER_FIELD_COMBAT_RATING_1": 1231,
|
||||
"PLAYER_FIELD_HONOR_CURRENCY": 1422,
|
||||
"PLAYER_FIELD_ARENA_CURRENCY": 1423,
|
||||
"GAMEOBJECT_DISPLAYID": 8,
|
||||
"ITEM_FIELD_STACK_COUNT": 14,
|
||||
"ITEM_FIELD_DURABILITY": 60,
|
||||
|
|
|
|||
|
|
@ -299,6 +299,10 @@ public:
|
|||
// Money (copper)
|
||||
uint64_t getMoneyCopper() const { return playerMoneyCopper_; }
|
||||
|
||||
// PvP currency (TBC/WotLK only)
|
||||
uint32_t getHonorPoints() const { return playerHonorPoints_; }
|
||||
uint32_t getArenaPoints() const { return playerArenaPoints_; }
|
||||
|
||||
// Server-authoritative armor (UNIT_FIELD_RESISTANCES[0])
|
||||
int32_t getArmorRating() const { return playerArmorRating_; }
|
||||
|
||||
|
|
@ -1530,7 +1534,11 @@ public:
|
|||
std::string iconName;
|
||||
};
|
||||
const std::vector<EquipmentSetInfo>& getEquipmentSets() const { return equipmentSetInfo_; }
|
||||
bool supportsEquipmentSets() const;
|
||||
void useEquipmentSet(uint32_t setId);
|
||||
void saveEquipmentSet(const std::string& name, const std::string& iconName = "INV_Misc_QuestionMark",
|
||||
uint64_t existingGuid = 0, uint32_t setIndex = 0xFFFFFFFF);
|
||||
void deleteEquipmentSet(uint64_t setGuid);
|
||||
|
||||
// NPC Gossip
|
||||
void interactWithNpc(uint64_t guid);
|
||||
|
|
@ -1793,7 +1801,7 @@ public:
|
|||
|
||||
const std::string& getFactionNamePublic(uint32_t factionId) const;
|
||||
uint32_t getWatchedFactionId() const { return watchedFactionId_; }
|
||||
void setWatchedFactionId(uint32_t id) { watchedFactionId_ = id; }
|
||||
void setWatchedFactionId(uint32_t factionId);
|
||||
uint32_t getLastContactListMask() const { return lastContactListMask_; }
|
||||
uint32_t getLastContactListCount() const { return lastContactListCount_; }
|
||||
bool isServerMovementAllowed() const { return serverMovementAllowed_; }
|
||||
|
|
@ -3067,6 +3075,8 @@ private:
|
|||
float pendingLootMoneyNotifyTimer_ = 0.0f;
|
||||
std::unordered_map<uint64_t, float> recentLootMoneyAnnounceCooldowns_;
|
||||
uint64_t playerMoneyCopper_ = 0;
|
||||
uint32_t playerHonorPoints_ = 0;
|
||||
uint32_t playerArenaPoints_ = 0;
|
||||
int32_t playerArmorRating_ = 0;
|
||||
int32_t playerResistances_[6] = {}; // [0]=Holy,[1]=Fire,[2]=Nature,[3]=Frost,[4]=Shadow,[5]=Arcane
|
||||
// Server-authoritative primary stats: [0]=STR [1]=AGI [2]=STA [3]=INT [4]=SPI; -1 = not received yet
|
||||
|
|
@ -3469,6 +3479,8 @@ private:
|
|||
std::array<uint64_t, 19> itemGuids{};
|
||||
};
|
||||
std::vector<EquipmentSet> equipmentSets_;
|
||||
std::string pendingSaveSetName_; // Saved between CMSG_EQUIPMENT_SET_SAVE and SMSG_EQUIPMENT_SET_SAVED
|
||||
std::string pendingSaveSetIcon_;
|
||||
std::vector<EquipmentSetInfo> equipmentSetInfo_; // public-facing copy
|
||||
|
||||
// ---- Forced faction reactions (SMSG_SET_FORCED_REACTIONS) ----
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@ enum class UF : uint16_t {
|
|||
PLAYER_SPELL_CRIT_PERCENTAGE1, // Spell crit chance % (first school; 7 consecutive float fields)
|
||||
PLAYER_FIELD_COMBAT_RATING_1, // First of 25 int32 combat rating slots (CR_* indices)
|
||||
|
||||
// Player PvP currency (TBC/WotLK only — Classic uses the old weekly honor system)
|
||||
PLAYER_FIELD_HONOR_CURRENCY, // Accumulated honor points (uint32)
|
||||
PLAYER_FIELD_ARENA_CURRENCY, // Accumulated arena points (uint32)
|
||||
|
||||
// GameObject fields
|
||||
GAMEOBJECT_DISPLAYID,
|
||||
|
||||
|
|
|
|||
|
|
@ -323,6 +323,7 @@ public:
|
|||
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
|
||||
void setInstanceTransform(uint32_t instanceId, const glm::mat4& transform);
|
||||
void setInstanceAnimationFrozen(uint32_t instanceId, bool frozen);
|
||||
float getInstanceAnimDuration(uint32_t instanceId) const;
|
||||
void removeInstance(uint32_t instanceId);
|
||||
void removeInstances(const std::vector<uint32_t>& instanceIds);
|
||||
void setSkipCollision(uint32_t instanceId, bool skip);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <future>
|
||||
#include <cstddef>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <glm/glm.hpp>
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
|
|
@ -138,6 +139,7 @@ public:
|
|||
QuestMarkerRenderer* getQuestMarkerRenderer() const { return questMarkerRenderer.get(); }
|
||||
SkySystem* getSkySystem() const { return skySystem.get(); }
|
||||
const std::string& getCurrentZoneName() const { return currentZoneName; }
|
||||
bool isPlayerIndoors() const { return playerIndoors_; }
|
||||
VkContext* getVkContext() const { return vkCtx; }
|
||||
VkDescriptorSetLayout getPerFrameSetLayout() const { return perFrameSetLayout; }
|
||||
VkRenderPass getShadowRenderPass() const { return shadowRenderPass; }
|
||||
|
|
@ -334,21 +336,28 @@ private:
|
|||
pipeline::AssetManager* cachedAssetManager = nullptr;
|
||||
|
||||
// Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL/IMPACT
|
||||
struct SpellVisualInstance { uint32_t instanceId; float elapsed; };
|
||||
struct SpellVisualInstance {
|
||||
uint32_t instanceId;
|
||||
float elapsed;
|
||||
float duration; // per-instance lifetime in seconds (from M2 anim or default)
|
||||
};
|
||||
std::vector<SpellVisualInstance> activeSpellVisuals_;
|
||||
std::unordered_map<uint32_t, std::string> spellVisualCastPath_; // visualId → cast M2 path
|
||||
std::unordered_map<uint32_t, std::string> spellVisualImpactPath_; // visualId → impact M2 path
|
||||
std::unordered_map<std::string, uint32_t> spellVisualModelIds_; // M2 path → M2Renderer modelId
|
||||
std::unordered_set<uint32_t> spellVisualFailedModels_; // modelIds that failed to load (negative cache)
|
||||
uint32_t nextSpellVisualModelId_ = 999000; // Reserved range 999000-999799
|
||||
bool spellVisualDbcLoaded_ = false;
|
||||
void loadSpellVisualDbc();
|
||||
void updateSpellVisuals(float deltaTime);
|
||||
static constexpr float SPELL_VISUAL_DURATION = 3.5f;
|
||||
static constexpr float SPELL_VISUAL_MAX_DURATION = 5.0f;
|
||||
static constexpr float SPELL_VISUAL_DEFAULT_DURATION = 2.0f;
|
||||
|
||||
uint32_t currentZoneId = 0;
|
||||
std::string currentZoneName;
|
||||
bool inTavern_ = false;
|
||||
bool inBlacksmith_ = false;
|
||||
bool playerIndoors_ = false; // Cached WMO inside state for macro conditionals
|
||||
float musicSwitchCooldown_ = 0.0f;
|
||||
bool deferredWorldInitEnabled_ = true;
|
||||
bool deferredWorldInitPending_ = false;
|
||||
|
|
|
|||
|
|
@ -423,6 +423,18 @@ private:
|
|||
bool spellIconDbLoaded_ = false;
|
||||
VkDescriptorSet getSpellIcon(uint32_t spellId, pipeline::AssetManager* am);
|
||||
|
||||
// ItemExtendedCost.dbc cache: extendedCostId -> cost details
|
||||
struct ExtendedCostEntry {
|
||||
uint32_t honorPoints = 0;
|
||||
uint32_t arenaPoints = 0;
|
||||
uint32_t itemId[5] = {};
|
||||
uint32_t itemCount[5] = {};
|
||||
};
|
||||
std::unordered_map<uint32_t, ExtendedCostEntry> extendedCostCache_;
|
||||
bool extendedCostDbLoaded_ = false;
|
||||
void loadExtendedCostDBC();
|
||||
std::string formatExtendedCost(uint32_t extendedCostId, game::GameHandler& gameHandler);
|
||||
|
||||
// Death Knight rune bar: client-predicted fill (0.0=depleted, 1.0=ready) for smooth animation
|
||||
float runeClientFill_[6] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f};
|
||||
|
||||
|
|
@ -569,6 +581,7 @@ private:
|
|||
uint32_t auctionBrowseOffset_ = 0; // Pagination offset for browse results
|
||||
int auctionItemClass_ = -1; // Item class filter (-1 = All)
|
||||
int auctionItemSubClass_ = -1; // Item subclass filter (-1 = All)
|
||||
bool auctionUsableOnly_ = false; // Filter to items usable by current class/level
|
||||
|
||||
// Guild bank money input
|
||||
int guildBankMoneyInput_[3] = {0, 0, 0}; // gold, silver, copper
|
||||
|
|
|
|||
|
|
@ -4218,19 +4218,52 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (packet.getSize() - packet.getReadPos() >= 12) {
|
||||
uint32_t setIndex = packet.readUInt32();
|
||||
uint64_t setGuid = packet.readUInt64();
|
||||
for (const auto& es : equipmentSets_) {
|
||||
if (es.setGuid == setGuid ||
|
||||
(es.setGuid == 0 && es.setId == setIndex)) {
|
||||
// Update the local set's GUID so subsequent "Update" calls
|
||||
// use the server-assigned GUID instead of 0 (which would
|
||||
// create a duplicate instead of updating).
|
||||
bool found = false;
|
||||
for (auto& es : equipmentSets_) {
|
||||
if (es.setGuid == setGuid || es.setId == setIndex) {
|
||||
es.setGuid = setGuid;
|
||||
setName = es.name;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
(void)setIndex;
|
||||
// Also update public-facing info
|
||||
for (auto& info : equipmentSetInfo_) {
|
||||
if (info.setGuid == setGuid || info.setId == setIndex) {
|
||||
info.setGuid = setGuid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If the set doesn't exist locally yet (new save), add a
|
||||
// placeholder entry so it shows up in the UI immediately.
|
||||
if (!found && setGuid != 0) {
|
||||
EquipmentSet newEs;
|
||||
newEs.setGuid = setGuid;
|
||||
newEs.setId = setIndex;
|
||||
newEs.name = pendingSaveSetName_;
|
||||
newEs.iconName = pendingSaveSetIcon_;
|
||||
for (int s = 0; s < 19; ++s)
|
||||
newEs.itemGuids[s] = getEquipSlotGuid(s);
|
||||
equipmentSets_.push_back(std::move(newEs));
|
||||
EquipmentSetInfo newInfo;
|
||||
newInfo.setGuid = setGuid;
|
||||
newInfo.setId = setIndex;
|
||||
newInfo.name = pendingSaveSetName_;
|
||||
newInfo.iconName = pendingSaveSetIcon_;
|
||||
equipmentSetInfo_.push_back(std::move(newInfo));
|
||||
setName = pendingSaveSetName_;
|
||||
}
|
||||
pendingSaveSetName_.clear();
|
||||
pendingSaveSetIcon_.clear();
|
||||
LOG_INFO("SMSG_EQUIPMENT_SET_SAVED: index=", setIndex,
|
||||
" guid=", setGuid, " name=", setName);
|
||||
}
|
||||
addSystemChatMessage(setName.empty()
|
||||
? std::string("Equipment set saved.")
|
||||
: "Equipment set \"" + setName + "\" saved.");
|
||||
LOG_DEBUG("Equipment set saved");
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_PERIODICAURALOG: {
|
||||
|
|
@ -4459,6 +4492,18 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
actionBar[i] = slot;
|
||||
}
|
||||
// Apply any pending cooldowns from spellCooldowns to newly populated slots.
|
||||
// SMSG_SPELL_COOLDOWN often arrives before SMSG_ACTION_BUTTONS during login,
|
||||
// so the per-slot cooldownRemaining would be 0 without this sync.
|
||||
for (auto& slot : actionBar) {
|
||||
if (slot.type == ActionBarSlot::SPELL && slot.id != 0) {
|
||||
auto cdIt = spellCooldowns.find(slot.id);
|
||||
if (cdIt != spellCooldowns.end() && cdIt->second > 0.0f) {
|
||||
slot.cooldownRemaining = cdIt->second;
|
||||
slot.cooldownTotal = cdIt->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_INFO("SMSG_ACTION_BUTTONS: populated action bar from server");
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
|
@ -4554,11 +4599,16 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_TRIGGER_CINEMATIC:
|
||||
// uint32 cinematicId — we don't play cinematics; consume and skip.
|
||||
case Opcode::SMSG_TRIGGER_CINEMATIC: {
|
||||
// uint32 cinematicId — we don't play cinematics; acknowledge immediately.
|
||||
packet.setReadPos(packet.getSize());
|
||||
LOG_DEBUG("SMSG_TRIGGER_CINEMATIC: skipped");
|
||||
// Send CMSG_NEXT_CINEMATIC_CAMERA to signal cinematic completion;
|
||||
// servers may block further packets until this is received.
|
||||
network::Packet ack(wireOpcode(Opcode::CMSG_NEXT_CINEMATIC_CAMERA));
|
||||
socket->send(ack);
|
||||
LOG_DEBUG("SMSG_TRIGGER_CINEMATIC: skipped, sent CMSG_NEXT_CINEMATIC_CAMERA");
|
||||
break;
|
||||
}
|
||||
|
||||
case Opcode::SMSG_LOOT_MONEY_NOTIFY: {
|
||||
// Format: uint32 money + uint8 soleLooter
|
||||
|
|
@ -6298,9 +6348,19 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
|
||||
// ---- Movie trigger ----
|
||||
case Opcode::SMSG_TRIGGER_MOVIE:
|
||||
case Opcode::SMSG_TRIGGER_MOVIE: {
|
||||
// uint32 movieId — we don't play movies; acknowledge immediately.
|
||||
packet.setReadPos(packet.getSize());
|
||||
// WotLK servers expect CMSG_COMPLETE_MOVIE after the movie finishes;
|
||||
// without it, the server may hang or disconnect the client.
|
||||
uint16_t wire = wireOpcode(Opcode::CMSG_COMPLETE_MOVIE);
|
||||
if (wire != 0xFFFF) {
|
||||
network::Packet ack(wire);
|
||||
socket->send(ack);
|
||||
LOG_DEBUG("SMSG_TRIGGER_MOVIE: skipped, sent CMSG_COMPLETE_MOVIE");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ---- Equipment sets ----
|
||||
case Opcode::SMSG_EQUIPMENT_SET_LIST:
|
||||
|
|
@ -9381,6 +9441,14 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
LOG_INFO("Skipping CMSG_QUERY_QUESTS_COMPLETED: opcode not mapped for current expansion");
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-request played time on login so the character Stats tab is
|
||||
// populated immediately without requiring /played.
|
||||
if (socket) {
|
||||
auto ptPkt = RequestPlayedTimePacket::build(false); // false = don't show in chat
|
||||
socket->send(ptPkt);
|
||||
LOG_INFO("Auto-requested played time on login");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -10660,12 +10728,115 @@ void GameHandler::sendRequestVehicleExit() {
|
|||
vehicleId_ = 0; // Optimistically clear; server will confirm via SMSG_PLAYER_VEHICLE_DATA(0)
|
||||
}
|
||||
|
||||
bool GameHandler::supportsEquipmentSets() const {
|
||||
return wireOpcode(Opcode::CMSG_EQUIPMENT_SET_SAVE) != 0xFFFF;
|
||||
}
|
||||
|
||||
void GameHandler::useEquipmentSet(uint32_t setId) {
|
||||
if (state != WorldState::IN_WORLD) return;
|
||||
// CMSG_EQUIPMENT_SET_USE: uint32 setId
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_EQUIPMENT_SET_USE));
|
||||
pkt.writeUInt32(setId);
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
uint16_t wire = wireOpcode(Opcode::CMSG_EQUIPMENT_SET_USE);
|
||||
if (wire == 0xFFFF) { addUIError("Equipment sets not supported."); return; }
|
||||
// Find the equipment set to get target item GUIDs per slot
|
||||
const EquipmentSet* es = nullptr;
|
||||
for (const auto& s : equipmentSets_) {
|
||||
if (s.setId == setId) { es = &s; break; }
|
||||
}
|
||||
if (!es) {
|
||||
addUIError("Equipment set not found.");
|
||||
return;
|
||||
}
|
||||
// CMSG_EQUIPMENT_SET_USE: 19 × (PackedGuid itemGuid + uint8 srcBag + uint8 srcSlot)
|
||||
network::Packet pkt(wire);
|
||||
for (int slot = 0; slot < 19; ++slot) {
|
||||
uint64_t itemGuid = es->itemGuids[slot];
|
||||
MovementPacket::writePackedGuid(pkt, itemGuid);
|
||||
uint8_t srcBag = 0xFF;
|
||||
uint8_t srcSlot = 0;
|
||||
if (itemGuid != 0) {
|
||||
bool found = false;
|
||||
// Check if item is already in an equipment slot
|
||||
for (int eq = 0; eq < 19 && !found; ++eq) {
|
||||
if (getEquipSlotGuid(eq) == itemGuid) {
|
||||
srcBag = 0xFF; // INVENTORY_SLOT_BAG_0
|
||||
srcSlot = static_cast<uint8_t>(eq);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
// Check backpack (slots 23-38 in the body container)
|
||||
for (int bp = 0; bp < 16 && !found; ++bp) {
|
||||
if (getBackpackItemGuid(bp) == itemGuid) {
|
||||
srcBag = 0xFF;
|
||||
srcSlot = static_cast<uint8_t>(23 + bp);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
// Check extra bags (bag indices 19-22)
|
||||
for (int bag = 0; bag < 4 && !found; ++bag) {
|
||||
int bagSize = inventory.getBagSize(bag);
|
||||
for (int s = 0; s < bagSize && !found; ++s) {
|
||||
if (getBagItemGuid(bag, s) == itemGuid) {
|
||||
srcBag = static_cast<uint8_t>(19 + bag);
|
||||
srcSlot = static_cast<uint8_t>(s);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pkt.writeUInt8(srcBag);
|
||||
pkt.writeUInt8(srcSlot);
|
||||
}
|
||||
socket->send(pkt);
|
||||
LOG_INFO("CMSG_EQUIPMENT_SET_USE: setId=", setId);
|
||||
}
|
||||
|
||||
void GameHandler::saveEquipmentSet(const std::string& name, const std::string& iconName,
|
||||
uint64_t existingGuid, uint32_t setIndex) {
|
||||
if (state != WorldState::IN_WORLD) return;
|
||||
uint16_t wire = wireOpcode(Opcode::CMSG_EQUIPMENT_SET_SAVE);
|
||||
if (wire == 0xFFFF) { addUIError("Equipment sets not supported."); return; }
|
||||
// CMSG_EQUIPMENT_SET_SAVE: uint64 setGuid + uint32 setIndex + string name + string iconName
|
||||
// + 19 × PackedGuid itemGuid (one per equipment slot, 0–18)
|
||||
if (setIndex == 0xFFFFFFFF) {
|
||||
// Auto-assign next free index
|
||||
setIndex = 0;
|
||||
for (const auto& es : equipmentSets_) {
|
||||
if (es.setId >= setIndex) setIndex = es.setId + 1;
|
||||
}
|
||||
}
|
||||
network::Packet pkt(wire);
|
||||
pkt.writeUInt64(existingGuid); // 0 = create new, nonzero = update
|
||||
pkt.writeUInt32(setIndex);
|
||||
pkt.writeString(name);
|
||||
pkt.writeString(iconName);
|
||||
for (int slot = 0; slot < 19; ++slot) {
|
||||
uint64_t guid = getEquipSlotGuid(slot);
|
||||
MovementPacket::writePackedGuid(pkt, guid);
|
||||
}
|
||||
// Track pending save so SMSG_EQUIPMENT_SET_SAVED can add the new set locally
|
||||
pendingSaveSetName_ = name;
|
||||
pendingSaveSetIcon_ = iconName;
|
||||
socket->send(pkt);
|
||||
LOG_INFO("CMSG_EQUIPMENT_SET_SAVE: name=\"", name, "\" guid=", existingGuid, " index=", setIndex);
|
||||
}
|
||||
|
||||
void GameHandler::deleteEquipmentSet(uint64_t setGuid) {
|
||||
if (state != WorldState::IN_WORLD || setGuid == 0) return;
|
||||
uint16_t wire = wireOpcode(Opcode::CMSG_DELETEEQUIPMENT_SET);
|
||||
if (wire == 0xFFFF) { addUIError("Equipment sets not supported."); return; }
|
||||
// CMSG_DELETEEQUIPMENT_SET: uint64 setGuid
|
||||
network::Packet pkt(wire);
|
||||
pkt.writeUInt64(setGuid);
|
||||
socket->send(pkt);
|
||||
// Remove locally so UI updates immediately
|
||||
equipmentSets_.erase(
|
||||
std::remove_if(equipmentSets_.begin(), equipmentSets_.end(),
|
||||
[setGuid](const EquipmentSet& es) { return es.setGuid == setGuid; }),
|
||||
equipmentSets_.end());
|
||||
equipmentSetInfo_.erase(
|
||||
std::remove_if(equipmentSetInfo_.begin(), equipmentSetInfo_.end(),
|
||||
[setGuid](const EquipmentSetInfo& es) { return es.setGuid == setGuid; }),
|
||||
equipmentSetInfo_.end());
|
||||
LOG_INFO("CMSG_DELETEEQUIPMENT_SET: guid=", setGuid);
|
||||
}
|
||||
|
||||
void GameHandler::sendMinimapPing(float wowX, float wowY) {
|
||||
|
|
@ -11781,6 +11952,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
const uint16_t ufPlayerRestedXp = fieldIndex(UF::PLAYER_REST_STATE_EXPERIENCE);
|
||||
const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
|
||||
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
|
||||
const uint16_t ufHonor = fieldIndex(UF::PLAYER_FIELD_HONOR_CURRENCY);
|
||||
const uint16_t ufArena = fieldIndex(UF::PLAYER_FIELD_ARENA_CURRENCY);
|
||||
const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES);
|
||||
const uint16_t ufPBytes2 = fieldIndex(UF::PLAYER_BYTES_2);
|
||||
const uint16_t ufChosenTitle = fieldIndex(UF::PLAYER_CHOSEN_TITLE);
|
||||
|
|
@ -11814,6 +11987,14 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
playerMoneyCopper_ = val;
|
||||
LOG_DEBUG("Money set from update fields: ", val, " copper");
|
||||
}
|
||||
else if (ufHonor != 0xFFFF && key == ufHonor) {
|
||||
playerHonorPoints_ = val;
|
||||
LOG_DEBUG("Honor points from update fields: ", val);
|
||||
}
|
||||
else if (ufArena != 0xFFFF && key == ufArena) {
|
||||
playerArenaPoints_ = val;
|
||||
LOG_DEBUG("Arena points from update fields: ", val);
|
||||
}
|
||||
else if (ufArmor != 0xFFFF && key == ufArmor) {
|
||||
playerArmorRating_ = static_cast<int32_t>(val);
|
||||
LOG_DEBUG("Armor rating from update fields: ", playerArmorRating_);
|
||||
|
|
@ -12207,6 +12388,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
const uint16_t ufPlayerRestedXpV = fieldIndex(UF::PLAYER_REST_STATE_EXPERIENCE);
|
||||
const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
|
||||
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
|
||||
const uint16_t ufHonorV = fieldIndex(UF::PLAYER_FIELD_HONOR_CURRENCY);
|
||||
const uint16_t ufArenaV = fieldIndex(UF::PLAYER_FIELD_ARENA_CURRENCY);
|
||||
const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS);
|
||||
const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES);
|
||||
const uint16_t ufPBytesV = fieldIndex(UF::PLAYER_BYTES);
|
||||
|
|
@ -12254,6 +12437,14 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
playerMoneyCopper_ = val;
|
||||
LOG_DEBUG("Money updated via VALUES: ", val, " copper");
|
||||
}
|
||||
else if (ufHonorV != 0xFFFF && key == ufHonorV) {
|
||||
playerHonorPoints_ = val;
|
||||
LOG_DEBUG("Honor points updated: ", val);
|
||||
}
|
||||
else if (ufArenaV != 0xFFFF && key == ufArenaV) {
|
||||
playerArenaPoints_ = val;
|
||||
LOG_DEBUG("Arena points updated: ", val);
|
||||
}
|
||||
else if (ufArmor != 0xFFFF && key == ufArmor) {
|
||||
playerArmorRating_ = static_cast<int32_t>(val);
|
||||
}
|
||||
|
|
@ -12608,7 +12799,9 @@ void GameHandler::handleCompressedUpdateObject(network::Packet& packet) {
|
|||
uint32_t decompressedSize = packet.readUInt32();
|
||||
LOG_DEBUG(" Decompressed size: ", decompressedSize);
|
||||
|
||||
if (decompressedSize == 0 || decompressedSize > 1024 * 1024) {
|
||||
// Capital cities and large raids can produce very large update packets.
|
||||
// The real WoW client handles up to ~10MB; 5MB covers all practical cases.
|
||||
if (decompressedSize == 0 || decompressedSize > 5 * 1024 * 1024) {
|
||||
LOG_WARNING("Invalid decompressed size: ", decompressedSize);
|
||||
return;
|
||||
}
|
||||
|
|
@ -18560,10 +18753,12 @@ void GameHandler::handleInitialSpells(network::Packet& packet) {
|
|||
knownSpells.insert(6603u);
|
||||
knownSpells.insert(8690u);
|
||||
|
||||
// Set initial cooldowns
|
||||
// Set initial cooldowns — use the longer of individual vs category cooldown.
|
||||
// Spells like potions have cooldownMs=0 but categoryCooldownMs=120000.
|
||||
for (const auto& cd : data.cooldowns) {
|
||||
if (cd.cooldownMs > 0) {
|
||||
spellCooldowns[cd.spellId] = cd.cooldownMs / 1000.0f;
|
||||
uint32_t effectiveMs = std::max(cd.cooldownMs, cd.categoryCooldownMs);
|
||||
if (effectiveMs > 0) {
|
||||
spellCooldowns[cd.spellId] = effectiveMs / 1000.0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -25674,6 +25869,21 @@ uint32_t GameHandler::getRepListIdByFactionId(uint32_t factionId) const {
|
|||
return (it != factionIdToRepList_.end()) ? it->second : 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
void GameHandler::setWatchedFactionId(uint32_t factionId) {
|
||||
watchedFactionId_ = factionId;
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
// CMSG_SET_WATCHED_FACTION: int32 repListId (-1 = unwatch)
|
||||
int32_t repListId = -1;
|
||||
if (factionId != 0) {
|
||||
uint32_t rl = getRepListIdByFactionId(factionId);
|
||||
if (rl != 0xFFFFFFFFu) repListId = static_cast<int32_t>(rl);
|
||||
}
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_SET_WATCHED_FACTION));
|
||||
pkt.writeUInt32(static_cast<uint32_t>(repListId));
|
||||
socket->send(pkt);
|
||||
LOG_DEBUG("CMSG_SET_WATCHED_FACTION: repListId=", repListId, " (factionId=", factionId, ")");
|
||||
}
|
||||
|
||||
std::string GameHandler::getFactionName(uint32_t factionId) const {
|
||||
auto it = factionNameCache_.find(factionId);
|
||||
if (it != factionNameCache_.end()) return it->second;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ static const UFNameEntry kUFNames[] = {
|
|||
{"UNIT_FIELD_DISPLAYID", UF::UNIT_FIELD_DISPLAYID},
|
||||
{"UNIT_FIELD_MOUNTDISPLAYID", UF::UNIT_FIELD_MOUNTDISPLAYID},
|
||||
{"UNIT_FIELD_AURAS", UF::UNIT_FIELD_AURAS},
|
||||
{"UNIT_FIELD_AURAFLAGS", UF::UNIT_FIELD_AURAFLAGS},
|
||||
{"UNIT_NPC_FLAGS", UF::UNIT_NPC_FLAGS},
|
||||
{"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS},
|
||||
{"UNIT_FIELD_RESISTANCES", UF::UNIT_FIELD_RESISTANCES},
|
||||
|
|
@ -74,6 +75,8 @@ static const UFNameEntry kUFNames[] = {
|
|||
{"PLAYER_RANGED_CRIT_PERCENTAGE", UF::PLAYER_RANGED_CRIT_PERCENTAGE},
|
||||
{"PLAYER_SPELL_CRIT_PERCENTAGE1", UF::PLAYER_SPELL_CRIT_PERCENTAGE1},
|
||||
{"PLAYER_FIELD_COMBAT_RATING_1", UF::PLAYER_FIELD_COMBAT_RATING_1},
|
||||
{"PLAYER_FIELD_HONOR_CURRENCY", UF::PLAYER_FIELD_HONOR_CURRENCY},
|
||||
{"PLAYER_FIELD_ARENA_CURRENCY", UF::PLAYER_FIELD_ARENA_CURRENCY},
|
||||
{"CONTAINER_FIELD_NUM_SLOTS", UF::CONTAINER_FIELD_NUM_SLOTS},
|
||||
{"CONTAINER_FIELD_SLOT_1", UF::CONTAINER_FIELD_SLOT_1},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3956,6 +3956,18 @@ void M2Renderer::setInstanceAnimationFrozen(uint32_t instanceId, bool frozen) {
|
|||
}
|
||||
}
|
||||
|
||||
float M2Renderer::getInstanceAnimDuration(uint32_t instanceId) const {
|
||||
auto idxIt = instanceIndexById.find(instanceId);
|
||||
if (idxIt == instanceIndexById.end()) return 0.0f;
|
||||
const auto& inst = instances[idxIt->second];
|
||||
if (!inst.cachedModel) return 0.0f;
|
||||
const auto& seqs = inst.cachedModel->sequences;
|
||||
if (seqs.empty()) return 0.0f;
|
||||
int seqIdx = inst.currentSequenceIndex;
|
||||
if (seqIdx < 0 || seqIdx >= static_cast<int>(seqs.size())) seqIdx = 0;
|
||||
return seqs[seqIdx].duration; // in milliseconds
|
||||
}
|
||||
|
||||
void M2Renderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& transform) {
|
||||
auto idxIt = instanceIndexById.find(instanceId);
|
||||
if (idxIt == instanceIndexById.end()) return;
|
||||
|
|
|
|||
|
|
@ -2860,16 +2860,21 @@ void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition
|
|||
spellVisualModelIds_[modelPath] = modelId;
|
||||
}
|
||||
|
||||
// Skip models that have previously failed to load (avoid repeated I/O)
|
||||
if (spellVisualFailedModels_.count(modelId)) return;
|
||||
|
||||
// Load the M2 model if not already loaded
|
||||
if (!m2Renderer->hasModel(modelId)) {
|
||||
auto m2Data = cachedAssetManager->readFile(modelPath);
|
||||
if (m2Data.empty()) {
|
||||
LOG_DEBUG("SpellVisual: could not read model: ", modelPath);
|
||||
spellVisualFailedModels_.insert(modelId);
|
||||
return;
|
||||
}
|
||||
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||
if (model.vertices.empty() && model.particleEmitters.empty()) {
|
||||
LOG_DEBUG("SpellVisual: empty model: ", modelPath);
|
||||
spellVisualFailedModels_.insert(modelId);
|
||||
return;
|
||||
}
|
||||
// Load skin file for WotLK-format M2s
|
||||
|
|
@ -2880,6 +2885,7 @@ void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition
|
|||
}
|
||||
if (!m2Renderer->loadModel(model, modelId)) {
|
||||
LOG_WARNING("SpellVisual: failed to load model to GPU: ", modelPath);
|
||||
spellVisualFailedModels_.insert(modelId);
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("SpellVisual: loaded model id=", modelId, " path=", modelPath);
|
||||
|
|
@ -2892,16 +2898,21 @@ void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition
|
|||
LOG_WARNING("SpellVisual: failed to create instance for visualId=", visualId);
|
||||
return;
|
||||
}
|
||||
activeSpellVisuals_.push_back({instanceId, 0.0f});
|
||||
// Determine lifetime from M2 animation duration (clamp to reasonable range)
|
||||
float animDurMs = m2Renderer->getInstanceAnimDuration(instanceId);
|
||||
float duration = (animDurMs > 100.0f)
|
||||
? std::clamp(animDurMs / 1000.0f, 0.5f, SPELL_VISUAL_MAX_DURATION)
|
||||
: SPELL_VISUAL_DEFAULT_DURATION;
|
||||
activeSpellVisuals_.push_back({instanceId, 0.0f, duration});
|
||||
LOG_DEBUG("SpellVisual: spawned visualId=", visualId, " instanceId=", instanceId,
|
||||
" model=", modelPath);
|
||||
" duration=", duration, "s model=", modelPath);
|
||||
}
|
||||
|
||||
void Renderer::updateSpellVisuals(float deltaTime) {
|
||||
if (activeSpellVisuals_.empty() || !m2Renderer) return;
|
||||
for (auto it = activeSpellVisuals_.begin(); it != activeSpellVisuals_.end(); ) {
|
||||
it->elapsed += deltaTime;
|
||||
if (it->elapsed >= SPELL_VISUAL_DURATION) {
|
||||
if (it->elapsed >= it->duration) {
|
||||
m2Renderer->removeInstance(it->instanceId);
|
||||
it = activeSpellVisuals_.erase(it);
|
||||
} else {
|
||||
|
|
@ -3465,6 +3476,7 @@ void Renderer::update(float deltaTime) {
|
|||
uint32_t insideWmoId = 0;
|
||||
const bool insideWmo = canQueryWmo &&
|
||||
wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &insideWmoId);
|
||||
playerIndoors_ = insideWmo;
|
||||
|
||||
// Ambient environmental sounds: fireplaces, water, birds, etc.
|
||||
if (ambientSoundManager && camera && wmoRenderer && cameraController) {
|
||||
|
|
|
|||
|
|
@ -1315,7 +1315,12 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
|||
if (i > 0 && i < static_cast<int>(chatTabUnread_.size()) && chatTabUnread_[i] > 0) {
|
||||
tabLabel += " (" + std::to_string(chatTabUnread_[i]) + ")";
|
||||
}
|
||||
// Use ImGuiTabItemFlags_NoPushId so label changes don't break tab identity
|
||||
// Flash tab text color when unread messages exist
|
||||
bool hasUnread = (i > 0 && i < static_cast<int>(chatTabUnread_.size()) && chatTabUnread_[i] > 0);
|
||||
if (hasUnread) {
|
||||
float pulse = 0.6f + 0.4f * std::sin(static_cast<float>(ImGui::GetTime()) * 4.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f * pulse, 0.2f * pulse, 1.0f));
|
||||
}
|
||||
if (ImGui::BeginTabItem(tabLabel.c_str())) {
|
||||
if (activeChatTab_ != i) {
|
||||
activeChatTab_ = i;
|
||||
|
|
@ -1325,6 +1330,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (hasUnread) ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
|
@ -2627,7 +2633,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
|||
"/g", "/guild", "/guildinfo",
|
||||
"/gmticket", "/grouploot", "/i", "/instance",
|
||||
"/invite", "/j", "/join", "/kick",
|
||||
"/l", "/leave", "/local", "/me",
|
||||
"/l", "/leave", "/local", "/macrohelp", "/me",
|
||||
"/p", "/party", "/petaggressive", "/petattack", "/petdefensive",
|
||||
"/petdismiss", "/petfollow", "/pethalt", "/petpassive", "/petstay",
|
||||
"/r", "/raid",
|
||||
|
|
@ -5668,12 +5674,17 @@ static std::string evaluateMacroConditionals(const std::string& rawArg,
|
|||
size_t e = c.find_last_not_of(" \t"); if (e != std::string::npos) c.resize(e + 1);
|
||||
if (c.empty()) return true;
|
||||
|
||||
// @target specifiers: @player, @focus, @mouseover, @target
|
||||
// @target specifiers: @player, @focus, @pet, @mouseover, @target
|
||||
if (!c.empty() && c[0] == '@') {
|
||||
std::string spec = c.substr(1);
|
||||
if (spec == "player") tgt = gameHandler.getPlayerGuid();
|
||||
if (spec == "player") tgt = gameHandler.getPlayerGuid();
|
||||
else if (spec == "focus") tgt = gameHandler.getFocusGuid();
|
||||
else if (spec == "target") tgt = gameHandler.getTargetGuid();
|
||||
else if (spec == "pet") {
|
||||
uint64_t pg = gameHandler.getPetGuid();
|
||||
if (pg != 0) tgt = pg;
|
||||
else return false; // no pet — skip this alternative
|
||||
}
|
||||
else if (spec == "mouseover") {
|
||||
uint64_t mo = gameHandler.getMouseoverGuid();
|
||||
if (mo != 0) tgt = mo;
|
||||
|
|
@ -5684,9 +5695,14 @@ static std::string evaluateMacroConditionals(const std::string& rawArg,
|
|||
// target=X specifiers
|
||||
if (c.rfind("target=", 0) == 0) {
|
||||
std::string spec = c.substr(7);
|
||||
if (spec == "player") tgt = gameHandler.getPlayerGuid();
|
||||
if (spec == "player") tgt = gameHandler.getPlayerGuid();
|
||||
else if (spec == "focus") tgt = gameHandler.getFocusGuid();
|
||||
else if (spec == "target") tgt = gameHandler.getTargetGuid();
|
||||
else if (spec == "pet") {
|
||||
uint64_t pg = gameHandler.getPetGuid();
|
||||
if (pg != 0) tgt = pg;
|
||||
else return false; // no pet — skip this alternative
|
||||
}
|
||||
else if (spec == "mouseover") {
|
||||
uint64_t mo = gameHandler.getMouseoverGuid();
|
||||
if (mo != 0) tgt = mo;
|
||||
|
|
@ -5742,6 +5758,61 @@ static std::string evaluateMacroConditionals(const std::string& rawArg,
|
|||
if (c == "harm" || c == "nohelp") { return unitHostile(effTarget()); }
|
||||
if (c == "help" || c == "noharm") { return !unitHostile(effTarget()); }
|
||||
|
||||
// mounted / nomounted
|
||||
if (c == "mounted") return gameHandler.isMounted();
|
||||
if (c == "nomounted") return !gameHandler.isMounted();
|
||||
|
||||
// swimming / noswimming
|
||||
if (c == "swimming") return gameHandler.isSwimming();
|
||||
if (c == "noswimming") return !gameHandler.isSwimming();
|
||||
|
||||
// flying / noflying (CAN_FLY + FLYING flags active)
|
||||
if (c == "flying") return gameHandler.isPlayerFlying();
|
||||
if (c == "noflying") return !gameHandler.isPlayerFlying();
|
||||
|
||||
// channeling / nochanneling
|
||||
if (c == "channeling") return gameHandler.isCasting() && gameHandler.isChanneling();
|
||||
if (c == "nochanneling") return !(gameHandler.isCasting() && gameHandler.isChanneling());
|
||||
|
||||
// stealthed / nostealthed (unit flag 0x02000000 = UNIT_FLAG_SNEAKING)
|
||||
auto isStealthedFn = [&]() -> bool {
|
||||
auto pe = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
||||
if (!pe) return false;
|
||||
auto pu = std::dynamic_pointer_cast<game::Unit>(pe);
|
||||
return pu && (pu->getUnitFlags() & 0x02000000u) != 0;
|
||||
};
|
||||
if (c == "stealthed") return isStealthedFn();
|
||||
if (c == "nostealthed") return !isStealthedFn();
|
||||
|
||||
// pet / nopet — player has an active pet (hunters, warlocks, DKs)
|
||||
if (c == "pet") return gameHandler.hasPet();
|
||||
if (c == "nopet") return !gameHandler.hasPet();
|
||||
|
||||
// indoors / outdoors — WMO interior detection (affects mount type selection)
|
||||
if (c == "indoors" || c == "nooutdoors") {
|
||||
auto* r = core::Application::getInstance().getRenderer();
|
||||
return r && r->isPlayerIndoors();
|
||||
}
|
||||
if (c == "outdoors" || c == "noindoors") {
|
||||
auto* r = core::Application::getInstance().getRenderer();
|
||||
return !r || !r->isPlayerIndoors();
|
||||
}
|
||||
|
||||
// group / nogroup — player is in a party or raid
|
||||
if (c == "group" || c == "party") return gameHandler.isInGroup();
|
||||
if (c == "nogroup") return !gameHandler.isInGroup();
|
||||
|
||||
// raid / noraid — player is in a raid group (groupType == 1)
|
||||
if (c == "raid") return gameHandler.isInGroup() && gameHandler.getPartyData().groupType == 1;
|
||||
if (c == "noraid") return !gameHandler.isInGroup() || gameHandler.getPartyData().groupType != 1;
|
||||
|
||||
// spec:N — active talent spec (1-based: spec:1 = primary, spec:2 = secondary)
|
||||
if (c.rfind("spec:", 0) == 0) {
|
||||
uint8_t wantSpec = 0;
|
||||
try { wantSpec = static_cast<uint8_t>(std::stoul(c.substr(5))); } catch (...) {}
|
||||
return wantSpec > 0 && gameHandler.getActiveTalentSpec() == (wantSpec - 1);
|
||||
}
|
||||
|
||||
// noform / nostance — player is NOT in a shapeshift/stance
|
||||
if (c == "noform" || c == "nostance") {
|
||||
for (const auto& a : gameHandler.getPlayerAuras())
|
||||
|
|
@ -6091,6 +6162,33 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
return;
|
||||
}
|
||||
|
||||
// /macrohelp command — list available macro conditionals
|
||||
if (cmdLower == "macrohelp") {
|
||||
static const char* kMacroHelp[] = {
|
||||
"--- Macro Conditionals ---",
|
||||
"Usage: /cast [cond1,cond2] Spell1; [cond3] Spell2; Default",
|
||||
"State: [combat] [mounted] [swimming] [flying] [stealthed]",
|
||||
" [channeling] [pet] [group] [raid] [indoors] [outdoors]",
|
||||
"Spec: [spec:1] [spec:2] (active talent spec, 1-based)",
|
||||
" (prefix no- to negate any condition)",
|
||||
"Target: [harm] [help] [exists] [noexists] [dead] [nodead]",
|
||||
" [target=focus] [target=pet] [target=player]",
|
||||
"Form: [noform] [nostance] [form:0]",
|
||||
"Keys: [mod:shift] [mod:ctrl] [mod:alt]",
|
||||
"Aura: [buff:Name] [nobuff:Name] [debuff:Name] [nodebuff:Name]",
|
||||
"Other: #showtooltip, /stopmacro [cond], /castsequence",
|
||||
};
|
||||
for (const char* line : kMacroHelp) {
|
||||
game::MessageChatData m;
|
||||
m.type = game::ChatType::SYSTEM;
|
||||
m.language = game::ChatLanguage::UNIVERSAL;
|
||||
m.message = line;
|
||||
gameHandler.addLocalChatMessage(m);
|
||||
}
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// /help command — list available slash commands
|
||||
if (cmdLower == "help" || cmdLower == "?") {
|
||||
static const char* kHelpLines[] = {
|
||||
|
|
@ -6109,7 +6207,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
"Movement: /sit /stand /kneel /dismount",
|
||||
"Misc: /played /time /zone /loc /afk [msg] /dnd [msg] /inspect",
|
||||
" /helm /cloak /trade /join <channel> /leave <channel>",
|
||||
" /score /unstuck /logout /ticket /screenshot /help",
|
||||
" /score /unstuck /logout /ticket /screenshot /macrohelp /help",
|
||||
};
|
||||
for (const char* line : kHelpLines) {
|
||||
game::MessageChatData helpMsg;
|
||||
|
|
@ -16020,6 +16118,61 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ItemExtendedCost.dbc loader
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::loadExtendedCostDBC() {
|
||||
if (extendedCostDbLoaded_) return;
|
||||
extendedCostDbLoaded_ = true;
|
||||
auto* am = core::Application::getInstance().getAssetManager();
|
||||
if (!am || !am->isInitialized()) return;
|
||||
auto dbc = am->loadDBC("ItemExtendedCost.dbc");
|
||||
if (!dbc || !dbc->isLoaded()) return;
|
||||
// WotLK ItemExtendedCost.dbc: field 0=ID, 1=honorPoints, 2=arenaPoints,
|
||||
// 3=arenaSlotRestrictions, 4-8=itemId[5], 9-13=itemCount[5], 14=reqRating, 15=purchaseGroup
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
if (id == 0) continue;
|
||||
ExtendedCostEntry e;
|
||||
e.honorPoints = dbc->getUInt32(i, 1);
|
||||
e.arenaPoints = dbc->getUInt32(i, 2);
|
||||
for (int j = 0; j < 5; ++j) {
|
||||
e.itemId[j] = dbc->getUInt32(i, 4 + j);
|
||||
e.itemCount[j] = dbc->getUInt32(i, 9 + j);
|
||||
}
|
||||
extendedCostCache_[id] = e;
|
||||
}
|
||||
LOG_INFO("ItemExtendedCost.dbc: loaded ", extendedCostCache_.size(), " entries");
|
||||
}
|
||||
|
||||
std::string GameScreen::formatExtendedCost(uint32_t extendedCostId, game::GameHandler& gameHandler) {
|
||||
loadExtendedCostDBC();
|
||||
auto it = extendedCostCache_.find(extendedCostId);
|
||||
if (it == extendedCostCache_.end()) return "[Tokens]";
|
||||
const auto& e = it->second;
|
||||
std::string result;
|
||||
if (e.honorPoints > 0) {
|
||||
result += std::to_string(e.honorPoints) + " Honor";
|
||||
}
|
||||
if (e.arenaPoints > 0) {
|
||||
if (!result.empty()) result += ", ";
|
||||
result += std::to_string(e.arenaPoints) + " Arena";
|
||||
}
|
||||
for (int j = 0; j < 5; ++j) {
|
||||
if (e.itemId[j] == 0 || e.itemCount[j] == 0) continue;
|
||||
if (!result.empty()) result += ", ";
|
||||
gameHandler.ensureItemInfo(e.itemId[j]); // query if not cached
|
||||
const auto* itemInfo = gameHandler.getItemInfo(e.itemId[j]);
|
||||
if (itemInfo && itemInfo->valid && !itemInfo->name.empty()) {
|
||||
result += std::to_string(e.itemCount[j]) + "x " + itemInfo->name;
|
||||
} else {
|
||||
result += std::to_string(e.itemCount[j]) + "x Item#" + std::to_string(e.itemId[j]);
|
||||
}
|
||||
}
|
||||
return result.empty() ? "[Tokens]" : result;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Vendor Window (Phase 5)
|
||||
// ============================================================
|
||||
|
|
@ -16272,8 +16425,9 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|||
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
if (item.buyPrice == 0 && item.extendedCost != 0) {
|
||||
// Token-only item (no gold cost)
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "[Tokens]");
|
||||
// Token-only item — show detailed cost from ItemExtendedCost.dbc
|
||||
std::string costStr = formatExtendedCost(item.extendedCost, gameHandler);
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "%s", costStr.c_str());
|
||||
} else {
|
||||
uint32_t g = item.buyPrice / 10000;
|
||||
uint32_t s = (item.buyPrice / 100) % 100;
|
||||
|
|
@ -16284,6 +16438,13 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|||
} else {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%ug %us %uc", g, s, c);
|
||||
}
|
||||
// Show additional token cost if both gold and tokens are required
|
||||
if (item.extendedCost != 0) {
|
||||
std::string costStr = formatExtendedCost(item.extendedCost, gameHandler);
|
||||
if (costStr != "[Tokens]") {
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 0.8f), "+ %s", costStr.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
|
|
@ -21690,7 +21851,8 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
|
|||
gameHandler.auctionSearch(auctionSearchName_,
|
||||
static_cast<uint8_t>(auctionLevelMin_),
|
||||
static_cast<uint8_t>(auctionLevelMax_),
|
||||
q, getSearchClassId(), getSearchSubClassId(), 0, 0, offset);
|
||||
q, getSearchClassId(), getSearchSubClassId(), 0,
|
||||
auctionUsableOnly_ ? 1 : 0, offset);
|
||||
};
|
||||
|
||||
// Row 1: Name + Level range
|
||||
|
|
@ -21736,6 +21898,8 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Usable", &auctionUsableOnly_);
|
||||
ImGui::SameLine();
|
||||
float delay = gameHandler.getAuctionSearchDelay();
|
||||
if (delay > 0.0f) {
|
||||
|
|
@ -23441,6 +23605,8 @@ void GameScreen::renderBattlegroundScore(game::GameHandler& gameHandler) {
|
|||
{ 566, "Eye of the Storm", 2757, 2758, 0, 1600, "resources" },
|
||||
// Strand of the Ancients (WotLK)
|
||||
{ 607, "Strand of the Ancients", 3476, 3477, 0, 4, "" },
|
||||
// Isle of Conquest (WotLK): reinforcements (300 default)
|
||||
{ 628, "Isle of Conquest", 4221, 4222, 0, 300, "reinforcements" },
|
||||
};
|
||||
|
||||
const BgScoreDef* def = nullptr;
|
||||
|
|
|
|||
|
|
@ -1249,6 +1249,22 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
|||
ImGui::Text("%s", fmtTime(levelSec).c_str()); ImGui::NextColumn();
|
||||
ImGui::Columns(1);
|
||||
}
|
||||
|
||||
// PvP Currency (TBC/WotLK only)
|
||||
uint32_t honor = gameHandler.getHonorPoints();
|
||||
uint32_t arena = gameHandler.getArenaPoints();
|
||||
if (honor > 0 || arena > 0) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("PvP Currency");
|
||||
ImGui::Columns(2, "##pvpcurrency", false);
|
||||
ImGui::SetColumnWidth(0, 130);
|
||||
ImGui::Text("Honor Points:"); ImGui::NextColumn();
|
||||
ImGui::TextColored(ImVec4(0.9f, 0.75f, 0.2f, 1.0f), "%u", honor); ImGui::NextColumn();
|
||||
ImGui::Text("Arena Points:"); ImGui::NextColumn();
|
||||
ImGui::TextColored(ImVec4(0.9f, 0.75f, 0.2f, 1.0f), "%u", arena); ImGui::NextColumn();
|
||||
ImGui::Columns(1);
|
||||
}
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
|
|
@ -1422,32 +1438,54 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
|||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
// Equipment Sets tab (WotLK only)
|
||||
const auto& eqSets = gameHandler.getEquipmentSets();
|
||||
if (!eqSets.empty()) {
|
||||
if (ImGui::BeginTabItem("Outfits")) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Saved Equipment Sets");
|
||||
ImGui::Separator();
|
||||
// Equipment Sets tab (WotLK only — requires server support)
|
||||
if (gameHandler.supportsEquipmentSets() && ImGui::BeginTabItem("Outfits")) {
|
||||
ImGui::Spacing();
|
||||
|
||||
// Save current gear as new set
|
||||
static char newSetName[64] = {};
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
ImGui::InputTextWithHint("##newsetname", "New set name...", newSetName, sizeof(newSetName));
|
||||
ImGui::SameLine();
|
||||
bool canSave = (newSetName[0] != '\0');
|
||||
if (!canSave) ImGui::BeginDisabled();
|
||||
if (ImGui::SmallButton("Save Current Gear")) {
|
||||
gameHandler.saveEquipmentSet(newSetName);
|
||||
newSetName[0] = '\0';
|
||||
}
|
||||
if (!canSave) ImGui::EndDisabled();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
const auto& eqSets = gameHandler.getEquipmentSets();
|
||||
if (eqSets.empty()) {
|
||||
ImGui::TextDisabled("No saved equipment sets.");
|
||||
} else {
|
||||
ImGui::BeginChild("##EqSetsList", ImVec2(0, 0), false);
|
||||
for (const auto& es : eqSets) {
|
||||
ImGui::PushID(static_cast<int>(es.setId));
|
||||
// Icon placeholder or name
|
||||
const char* displayName = es.name.empty() ? "(Unnamed)" : es.name.c_str();
|
||||
ImGui::Text("%s", displayName);
|
||||
if (!es.iconName.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(%s)", es.iconName.c_str());
|
||||
}
|
||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60.0f);
|
||||
float btnAreaW = 150.0f;
|
||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - btnAreaW + ImGui::GetCursorPosX());
|
||||
if (ImGui::SmallButton("Equip")) {
|
||||
gameHandler.useEquipmentSet(es.setId);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Update")) {
|
||||
gameHandler.saveEquipmentSet(es.name, es.iconName, es.setGuid, es.setId);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Delete")) {
|
||||
gameHandler.deleteEquipmentSet(es.setGuid);
|
||||
ImGui::PopID();
|
||||
break; // Iterator invalidated
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
ImGui::EndTabBar();
|
||||
|
|
@ -2589,6 +2627,20 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName);
|
||||
}
|
||||
}
|
||||
|
||||
// Show red warning if player lacks proficiency for this weapon/armor type
|
||||
if (gameHandler_) {
|
||||
const auto* qi = gameHandler_->getItemInfo(item.itemId);
|
||||
if (qi && qi->valid) {
|
||||
bool canUse = true;
|
||||
if (qi->itemClass == 2) // Weapon
|
||||
canUse = gameHandler_->canUseWeaponSubclass(qi->subClass);
|
||||
else if (qi->itemClass == 4 && qi->subClass > 0) // Armor (skip subclass 0 = misc)
|
||||
canUse = gameHandler_->canUseArmorSubclass(qi->subClass);
|
||||
if (!canUse)
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "You can't use this type of item.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto isWeaponInventoryType = [](uint32_t invType) {
|
||||
|
|
@ -3246,6 +3298,17 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info,
|
|||
else
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName);
|
||||
}
|
||||
|
||||
// Proficiency check for vendor/loot tooltips (ItemQueryResponseData has itemClass/subClass)
|
||||
if (gameHandler_) {
|
||||
bool canUse = true;
|
||||
if (info.itemClass == 2) // Weapon
|
||||
canUse = gameHandler_->canUseWeaponSubclass(info.subClass);
|
||||
else if (info.itemClass == 4 && info.subClass > 0) // Armor (skip subclass 0 = misc)
|
||||
canUse = gameHandler_->canUseArmorSubclass(info.subClass);
|
||||
if (!canUse)
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "You can't use this type of item.");
|
||||
}
|
||||
}
|
||||
|
||||
// Weapon stats
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue