Fix online interactions, UI, and inventory sync

This commit is contained in:
Kelsi 2026-02-06 18:34:45 -08:00
parent 7436420cd1
commit fdc614902b
14 changed files with 525 additions and 143 deletions

View file

@ -900,6 +900,20 @@ void GameHandler::update(float deltaTime) {
if (unit->getHealth() == 0) {
stopAutoAttack();
} else {
// Out-of-range notice (melee)
constexpr float MELEE_RANGE = 5.0f;
float dx = target->getX() - movementInfo.x;
float dy = target->getY() - movementInfo.y;
float dz = target->getZ() - movementInfo.z;
float dist = std::sqrt(dx*dx + dy*dy + dz*dz);
bool outOfRange = dist > MELEE_RANGE;
if (outOfRange && !autoAttackOutOfRange_) {
addSystemChatMessage("Target is out of range.");
autoAttackOutOfRange_ = true;
} else if (!outOfRange && autoAttackOutOfRange_) {
autoAttackOutOfRange_ = false;
}
// Re-send attack swing every 2 seconds to keep server combat alive
swingTimer_ += deltaTime;
if (swingTimer_ >= 2.0f) {
@ -966,9 +980,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::SMSG_CHAR_DELETE: {
uint8_t result = packet.readUInt8();
bool success = (result == 0x47); // CHAR_DELETE_SUCCESS
lastCharDeleteResult_ = result;
bool success = (result == 0x00 || result == 0x47); // Common success codes
LOG_INFO("SMSG_CHAR_DELETE result: ", (int)result, success ? " (success)" : " (failed)");
if (success) requestCharacterList();
requestCharacterList();
if (charDeleteCallback_) charDeleteCallback_(success);
break;
}
@ -2608,6 +2623,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
// Extract XP / inventory slot fields for player entity
if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) {
lastPlayerFields_ = block.fields;
detectInventorySlotBases(block.fields);
bool slotsChanged = false;
for (const auto& [key, val] : block.fields) {
if (key == 634) { playerXp_ = val; } // PLAYER_XP
@ -2619,28 +2636,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
}
else if (key == 632) { playerMoneyCopper_ = val; } // PLAYER_FIELD_COINAGE
else if (key >= 322 && key <= 367) {
// PLAYER_FIELD_INV_SLOT_HEAD: equipment slots (23 slots × 2 fields)
int slotIndex = (key - 322) / 2;
bool isLow = ((key - 322) % 2 == 0);
if (slotIndex < 23) {
uint64_t& guid = equipSlotGuids_[slotIndex];
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
slotsChanged = true;
}
} else if (key >= 368 && key <= 399) {
// PLAYER_FIELD_PACK_SLOT_1: backpack slots (16 slots × 2 fields)
int slotIndex = (key - 368) / 2;
bool isLow = ((key - 368) % 2 == 0);
if (slotIndex < 16) {
uint64_t& guid = backpackSlotGuids_[slotIndex];
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
slotsChanged = true;
}
}
}
if (applyInventoryFields(block.fields)) slotsChanged = true;
if (slotsChanged) rebuildOnlineInventory();
}
break;
@ -2666,6 +2663,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
if (block.guid == autoAttackTarget) {
stopAutoAttack();
}
hostileAttackers_.erase(block.guid);
// Player death
if (block.guid == playerGuid) {
playerDead_ = true;
@ -2705,6 +2703,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
// Update XP / inventory slot fields for player entity
if (block.guid == playerGuid) {
for (const auto& [key, val] : block.fields) {
lastPlayerFields_[key] = val;
}
detectInventorySlotBases(block.fields);
bool slotsChanged = false;
for (const auto& [key, val] : block.fields) {
if (key == 634) {
@ -2730,26 +2732,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
playerMoneyCopper_ = val;
LOG_INFO("Money updated via VALUES: ", val, " copper");
}
else if (key >= 322 && key <= 367) {
int slotIndex = (key - 322) / 2;
bool isLow = ((key - 322) % 2 == 0);
if (slotIndex < 23) {
uint64_t& guid = equipSlotGuids_[slotIndex];
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
slotsChanged = true;
}
} else if (key >= 368 && key <= 399) {
int slotIndex = (key - 368) / 2;
bool isLow = ((key - 368) % 2 == 0);
if (slotIndex < 16) {
uint64_t& guid = backpackSlotGuids_[slotIndex];
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
slotsChanged = true;
}
}
}
if (applyInventoryFields(block.fields)) slotsChanged = true;
if (slotsChanged) rebuildOnlineInventory();
}
@ -2791,6 +2775,16 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
tabCycleStale = true;
LOG_INFO("Entity count: ", entityManager.getEntityCount());
// Late inventory base detection once items are known
if (playerGuid != 0 && invSlotBase_ < 0 && !lastPlayerFields_.empty() && !onlineItems_.empty()) {
detectInventorySlotBases(lastPlayerFields_);
if (invSlotBase_ >= 0) {
if (applyInventoryFields(lastPlayerFields_)) {
rebuildOnlineInventory();
}
}
}
}
void GameHandler::handleCompressedUpdateObject(network::Packet& packet) {
@ -2856,6 +2850,7 @@ void GameHandler::handleDestroyObject(network::Packet& packet) {
if (data.guid == targetGuid) {
targetGuid = 0;
}
hostileAttackers_.erase(data.guid);
// Remove online item tracking
if (onlineItems_.erase(data.guid)) {
@ -2975,6 +2970,14 @@ void GameHandler::releaseSpirit() {
}
}
void GameHandler::activateSpiritHealer(uint64_t npcGuid) {
if (!playerDead_) return;
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = SpiritHealerActivatePacket::build(npcGuid);
socket->send(packet);
LOG_INFO("Sent CMSG_SPIRIT_HEALER_ACTIVATE to 0x", std::hex, npcGuid, std::dec);
}
void GameHandler::tabTarget(float playerX, float playerY, float playerZ) {
// Rebuild cycle list if stale
if (tabCycleStale) {
@ -3117,6 +3120,65 @@ void GameHandler::handleItemQueryResponse(network::Packet& packet) {
}
}
void GameHandler::detectInventorySlotBases(const std::map<uint16_t, uint32_t>& fields) {
if (invSlotBase_ >= 0 && packSlotBase_ >= 0) return;
if (onlineItems_.empty() || fields.empty()) return;
std::vector<uint16_t> matchingPairs;
matchingPairs.reserve(32);
for (const auto& [idx, low] : fields) {
if ((idx % 2) != 0) continue;
auto itHigh = fields.find(static_cast<uint16_t>(idx + 1));
if (itHigh == fields.end()) continue;
uint64_t guid = (uint64_t(itHigh->second) << 32) | low;
if (guid == 0) continue;
if (onlineItems_.count(guid)) {
matchingPairs.push_back(idx);
}
}
if (matchingPairs.empty()) return;
std::sort(matchingPairs.begin(), matchingPairs.end());
if (invSlotBase_ < 0) {
invSlotBase_ = matchingPairs.front();
packSlotBase_ = invSlotBase_ + (game::Inventory::NUM_EQUIP_SLOTS * 2);
LOG_INFO("Detected inventory field base: equip=", invSlotBase_,
" pack=", packSlotBase_);
}
}
bool GameHandler::applyInventoryFields(const std::map<uint16_t, uint32_t>& fields) {
bool slotsChanged = false;
int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : 322;
int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : 368;
for (const auto& [key, val] : fields) {
if (key >= equipBase && key <= equipBase + (game::Inventory::NUM_EQUIP_SLOTS * 2 - 1)) {
int slotIndex = (key - equipBase) / 2;
bool isLow = ((key - equipBase) % 2 == 0);
if (slotIndex < static_cast<int>(equipSlotGuids_.size())) {
uint64_t& guid = equipSlotGuids_[slotIndex];
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
slotsChanged = true;
}
} else if (key >= packBase && key <= packBase + (game::Inventory::BACKPACK_SLOTS * 2 - 1)) {
int slotIndex = (key - packBase) / 2;
bool isLow = ((key - packBase) % 2 == 0);
if (slotIndex < static_cast<int>(backpackSlotGuids_.size())) {
uint64_t& guid = backpackSlotGuids_[slotIndex];
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
slotsChanged = true;
}
}
}
return slotsChanged;
}
void GameHandler::rebuildOnlineInventory() {
if (singlePlayerMode_) return;
@ -3151,6 +3213,7 @@ void GameHandler::rebuildOnlineInventory() {
def.spirit = infoIt->second.spirit;
} else {
def.name = "Item " + std::to_string(def.itemId);
queryItemInfo(def.itemId, guid);
}
inventory.setEquipSlot(static_cast<EquipSlot>(i), def);
@ -3185,6 +3248,7 @@ void GameHandler::rebuildOnlineInventory() {
def.spirit = infoIt->second.spirit;
} else {
def.name = "Item " + std::to_string(def.itemId);
queryItemInfo(def.itemId, guid);
}
inventory.setBackpackSlot(i, def);
@ -3206,6 +3270,7 @@ void GameHandler::rebuildOnlineInventory() {
void GameHandler::startAutoAttack(uint64_t targetGuid) {
autoAttacking = true;
autoAttackTarget = targetGuid;
autoAttackOutOfRange_ = false;
swingTimer_ = 0.0f;
if (state == WorldState::IN_WORLD && socket) {
auto packet = AttackSwingPacket::build(targetGuid);
@ -3218,6 +3283,7 @@ void GameHandler::stopAutoAttack() {
if (!autoAttacking) return;
autoAttacking = false;
autoAttackTarget = 0;
autoAttackOutOfRange_ = false;
if (state == WorldState::IN_WORLD && socket) {
auto packet = AttackStopPacket::build();
socket->send(packet);
@ -3265,6 +3331,8 @@ void GameHandler::handleAttackStop(network::Packet& packet) {
// We'll re-send CMSG_ATTACKSWING periodically in the update loop.
if (data.attackerGuid == playerGuid) {
LOG_DEBUG("SMSG_ATTACKSTOP received (keeping auto-attack intent)");
} else if (data.victimGuid == playerGuid) {
hostileAttackers_.erase(data.attackerGuid);
}
}
@ -3345,6 +3413,10 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) {
npcSwingCallback_(data.attackerGuid);
}
if (isPlayerTarget && data.attackerGuid != 0) {
hostileAttackers_.insert(data.attackerGuid);
}
if (data.isMiss()) {
addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker);
} else if (data.victimState == 1) {
@ -3921,6 +3993,40 @@ void GameHandler::sellItemBySlot(int backpackIndex) {
}
}
void GameHandler::autoEquipItemBySlot(int backpackIndex) {
if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return;
const auto& slot = inventory.getBackpackSlot(backpackIndex);
if (slot.empty()) return;
if (singlePlayerMode_) {
// Fall back to local equip logic (UI already handles this).
return;
}
uint64_t itemGuid = backpackSlotGuids_[backpackIndex];
if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) {
auto packet = AutoEquipItemPacket::build(itemGuid);
socket->send(packet);
}
}
void GameHandler::useItemBySlot(int backpackIndex) {
if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return;
const auto& slot = inventory.getBackpackSlot(backpackIndex);
if (slot.empty()) return;
if (singlePlayerMode_) {
// Single-player consumable use not implemented yet.
return;
}
uint64_t itemGuid = backpackSlotGuids_[backpackIndex];
if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) {
auto packet = UseItemPacket::build(0xFF, static_cast<uint8_t>(backpackIndex), itemGuid);
socket->send(packet);
}
}
void GameHandler::handleLootResponse(network::Packet& packet) {
if (!LootResponseParser::parse(packet, currentLoot)) return;
lootWindowOpen = true;