mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Refine gameobject chest interaction flow and remove temporary debug logging
- Keep WotLK chest flow stock-like by relying on GAMEOBJ_USE/REPORT_USE instead of forcing eager CMSG_LOOT. - Preserve fallback eager-loot behavior for Classic/Turtle paths with bounded retries. - Improve GO targeting usability by allowing gameobject pick/retarget in world click logic. - Remove temporary loot/chest diagnostic LOG_INFO traces added during chest-open debugging.
This commit is contained in:
parent
a11c9ae22b
commit
2da2e75253
3 changed files with 116 additions and 54 deletions
|
|
@ -1465,8 +1465,14 @@ private:
|
||||||
uint64_t guid = 0;
|
uint64_t guid = 0;
|
||||||
float timer = 0.0f;
|
float timer = 0.0f;
|
||||||
uint8_t remainingRetries = 0;
|
uint8_t remainingRetries = 0;
|
||||||
|
bool sendLoot = false;
|
||||||
};
|
};
|
||||||
std::vector<PendingLootRetry> pendingGameObjectLootRetries_;
|
std::vector<PendingLootRetry> pendingGameObjectLootRetries_;
|
||||||
|
struct PendingLootOpen {
|
||||||
|
uint64_t guid = 0;
|
||||||
|
float timer = 0.0f;
|
||||||
|
};
|
||||||
|
std::vector<PendingLootOpen> pendingGameObjectLootOpens_;
|
||||||
uint64_t pendingLootMoneyGuid_ = 0;
|
uint64_t pendingLootMoneyGuid_ = 0;
|
||||||
uint32_t pendingLootMoneyAmount_ = 0;
|
uint32_t pendingLootMoneyAmount_ = 0;
|
||||||
float pendingLootMoneyNotifyTimer_ = 0.0f;
|
float pendingLootMoneyNotifyTimer_ = 0.0f;
|
||||||
|
|
|
||||||
|
|
@ -616,10 +616,14 @@ void GameHandler::update(float deltaTime) {
|
||||||
it->timer -= deltaTime;
|
it->timer -= deltaTime;
|
||||||
if (it->timer <= 0.0f) {
|
if (it->timer <= 0.0f) {
|
||||||
if (it->remainingRetries > 0 && state == WorldState::IN_WORLD && socket) {
|
if (it->remainingRetries > 0 && state == WorldState::IN_WORLD && socket) {
|
||||||
|
// Keep server-side position/facing fresh before retrying GO use.
|
||||||
|
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
|
||||||
auto usePacket = GameObjectUsePacket::build(it->guid);
|
auto usePacket = GameObjectUsePacket::build(it->guid);
|
||||||
socket->send(usePacket);
|
socket->send(usePacket);
|
||||||
|
if (it->sendLoot) {
|
||||||
auto lootPacket = LootPacket::build(it->guid);
|
auto lootPacket = LootPacket::build(it->guid);
|
||||||
socket->send(lootPacket);
|
socket->send(lootPacket);
|
||||||
|
}
|
||||||
--it->remainingRetries;
|
--it->remainingRetries;
|
||||||
it->timer = 0.20f;
|
it->timer = 0.20f;
|
||||||
}
|
}
|
||||||
|
|
@ -631,6 +635,18 @@ void GameHandler::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto it = pendingGameObjectLootOpens_.begin(); it != pendingGameObjectLootOpens_.end();) {
|
||||||
|
it->timer -= deltaTime;
|
||||||
|
if (it->timer <= 0.0f) {
|
||||||
|
if (state == WorldState::IN_WORLD && socket) {
|
||||||
|
lootTarget(it->guid);
|
||||||
|
}
|
||||||
|
it = pendingGameObjectLootOpens_.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (pendingLootMoneyNotifyTimer_ > 0.0f) {
|
if (pendingLootMoneyNotifyTimer_ > 0.0f) {
|
||||||
pendingLootMoneyNotifyTimer_ -= deltaTime;
|
pendingLootMoneyNotifyTimer_ -= deltaTime;
|
||||||
if (pendingLootMoneyNotifyTimer_ <= 0.0f) {
|
if (pendingLootMoneyNotifyTimer_ <= 0.0f) {
|
||||||
|
|
@ -712,7 +728,7 @@ void GameHandler::update(float deltaTime) {
|
||||||
|
|
||||||
// Update cast timer (Phase 3)
|
// Update cast timer (Phase 3)
|
||||||
if (pendingGameObjectInteractGuid_ != 0 &&
|
if (pendingGameObjectInteractGuid_ != 0 &&
|
||||||
(autoAttacking || !hostileAttackers_.empty())) {
|
(autoAttacking || autoAttackRequested_)) {
|
||||||
pendingGameObjectInteractGuid_ = 0;
|
pendingGameObjectInteractGuid_ = 0;
|
||||||
casting = false;
|
casting = false;
|
||||||
currentCastSpellId = 0;
|
currentCastSpellId = 0;
|
||||||
|
|
@ -9671,15 +9687,13 @@ void GameHandler::interactWithNpc(uint64_t guid) {
|
||||||
void GameHandler::interactWithGameObject(uint64_t guid) {
|
void GameHandler::interactWithGameObject(uint64_t guid) {
|
||||||
if (guid == 0) return;
|
if (guid == 0) return;
|
||||||
if (state != WorldState::IN_WORLD || !socket) return;
|
if (state != WorldState::IN_WORLD || !socket) return;
|
||||||
if (casting && currentCastSpellId != 0) return; // don't overlap spell cast bar
|
// Do not overlap an actual spell cast.
|
||||||
if (autoAttacking) {
|
if (casting && currentCastSpellId != 0) return;
|
||||||
|
// Always clear melee intent before GO interactions.
|
||||||
stopAutoAttack();
|
stopAutoAttack();
|
||||||
}
|
// Interact immediately; server drives any real cast/channel feedback.
|
||||||
pendingGameObjectInteractGuid_ = guid;
|
pendingGameObjectInteractGuid_ = 0;
|
||||||
casting = true;
|
performGameObjectInteractionNow(guid);
|
||||||
currentCastSpellId = 0;
|
|
||||||
castTimeTotal = 1.5f;
|
|
||||||
castTimeRemaining = castTimeTotal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
||||||
|
|
@ -9691,19 +9705,45 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
||||||
static uint64_t lastInteractGuid = 0;
|
static uint64_t lastInteractGuid = 0;
|
||||||
static std::chrono::steady_clock::time_point lastInteractTime{};
|
static std::chrono::steady_clock::time_point lastInteractTime{};
|
||||||
auto now = std::chrono::steady_clock::now();
|
auto now = std::chrono::steady_clock::now();
|
||||||
int64_t minRepeatMs = turtleMode ? 250 : 1000;
|
// Keep duplicate suppression, but allow quick retry clicks.
|
||||||
|
int64_t minRepeatMs = turtleMode ? 150 : 150;
|
||||||
if (guid == lastInteractGuid &&
|
if (guid == lastInteractGuid &&
|
||||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - lastInteractTime).count() < minRepeatMs) {
|
std::chrono::duration_cast<std::chrono::milliseconds>(now - lastInteractTime).count() < minRepeatMs) {
|
||||||
return; // Ignore repeated clicks within 1 second
|
return;
|
||||||
}
|
}
|
||||||
lastInteractGuid = guid;
|
lastInteractGuid = guid;
|
||||||
lastInteractTime = now;
|
lastInteractTime = now;
|
||||||
|
|
||||||
// Ensure chest interaction isn't blocked by our own auto-attack state.
|
// Ensure GO interaction isn't blocked by stale or active melee state.
|
||||||
if (autoAttacking) {
|
|
||||||
stopAutoAttack();
|
stopAutoAttack();
|
||||||
}
|
|
||||||
auto entity = entityManager.getEntity(guid);
|
auto entity = entityManager.getEntity(guid);
|
||||||
|
uint32_t goEntry = 0;
|
||||||
|
uint32_t goType = 0;
|
||||||
|
std::string goName;
|
||||||
|
|
||||||
|
if (entity) {
|
||||||
|
if (entity->getType() == ObjectType::GAMEOBJECT) {
|
||||||
|
auto go = std::static_pointer_cast<GameObject>(entity);
|
||||||
|
goEntry = go->getEntry();
|
||||||
|
goName = go->getName();
|
||||||
|
if (auto* info = getCachedGameObjectInfo(goEntry)) goType = info->type;
|
||||||
|
}
|
||||||
|
// Face object and send heartbeat before use so strict servers don't require
|
||||||
|
// a nudge movement to accept interaction.
|
||||||
|
float dx = entity->getX() - movementInfo.x;
|
||||||
|
float dy = entity->getY() - movementInfo.y;
|
||||||
|
float dz = entity->getZ() - movementInfo.z;
|
||||||
|
float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
if (dist3d > 6.0f) {
|
||||||
|
addSystemChatMessage("Too far away.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
||||||
|
movementInfo.orientation = std::atan2(-dy, dx);
|
||||||
|
sendMovement(Opcode::MSG_MOVE_SET_FACING);
|
||||||
|
}
|
||||||
|
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
|
||||||
|
}
|
||||||
|
|
||||||
auto packet = GameObjectUsePacket::build(guid);
|
auto packet = GameObjectUsePacket::build(guid);
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
|
|
@ -9712,7 +9752,10 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
||||||
// In Vanilla/Classic there is no SMSG_SHOW_MAILBOX — the server just sends
|
// In Vanilla/Classic there is no SMSG_SHOW_MAILBOX — the server just sends
|
||||||
// animation/sound and expects the client to request the mail list.
|
// animation/sound and expects the client to request the mail list.
|
||||||
bool isMailbox = false;
|
bool isMailbox = false;
|
||||||
bool shouldSendLoot = (entity == nullptr);
|
bool chestLike = false;
|
||||||
|
// Stock-like behavior: GO use opens GO loot context. Keep eager CMSG_LOOT only
|
||||||
|
// as Classic/Turtle fallback behavior.
|
||||||
|
bool shouldSendLoot = isActiveExpansion("classic") || isActiveExpansion("turtle");
|
||||||
if (entity && entity->getType() == ObjectType::GAMEOBJECT) {
|
if (entity && entity->getType() == ObjectType::GAMEOBJECT) {
|
||||||
auto go = std::static_pointer_cast<GameObject>(entity);
|
auto go = std::static_pointer_cast<GameObject>(entity);
|
||||||
auto* info = getCachedGameObjectInfo(go->getEntry());
|
auto* info = getCachedGameObjectInfo(go->getEntry());
|
||||||
|
|
@ -9726,29 +9769,36 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
||||||
selectedMailIndex_ = -1;
|
selectedMailIndex_ = -1;
|
||||||
showMailCompose_ = false;
|
showMailCompose_ = false;
|
||||||
refreshMailList();
|
refreshMailList();
|
||||||
} else {
|
} else if (info && info->type == 3) {
|
||||||
// Keep non-Turtle behavior constrained to known lootable GO types.
|
chestLike = true;
|
||||||
if (!turtleMode) {
|
} else if (turtleMode) {
|
||||||
if (info && (info->type == 3 || info->type == 25)) {
|
// Turtle compatibility: keep eager loot open behavior.
|
||||||
shouldSendLoot = true;
|
|
||||||
} else if (info) {
|
|
||||||
shouldSendLoot = false;
|
|
||||||
} else {
|
|
||||||
shouldSendLoot = true;
|
shouldSendLoot = true;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Turtle compatibility: aggressively pair use+loot for chest-like objects.
|
|
||||||
shouldSendLoot = true;
|
|
||||||
}
|
}
|
||||||
|
if (!chestLike && !goName.empty()) {
|
||||||
|
std::string lower = goName;
|
||||||
|
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
chestLike = (lower.find("chest") != std::string::npos);
|
||||||
|
}
|
||||||
|
// For WotLK chest-like gameobjects, report use but let server open loot.
|
||||||
|
if (!isMailbox && chestLike) {
|
||||||
|
if (isActiveExpansion("wotlk")) {
|
||||||
|
network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE));
|
||||||
|
reportUse.writeUInt64(guid);
|
||||||
|
socket->send(reportUse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldSendLoot) {
|
if (shouldSendLoot) {
|
||||||
LOG_INFO("GameObject interaction: sent CMSG_GAMEOBJ_USE + CMSG_LOOT for guid=0x", std::hex, guid, std::dec,
|
|
||||||
" mailbox=", (isMailbox ? 1 : 0), " turtle=", (turtleMode ? 1 : 0));
|
|
||||||
lootTarget(guid);
|
lootTarget(guid);
|
||||||
if (turtleMode) {
|
|
||||||
pendingGameObjectLootRetries_.push_back(PendingLootRetry{guid, 0.20f, 2});
|
|
||||||
}
|
}
|
||||||
|
// Retry use briefly to survive packet loss/order races. Keep loot retries only
|
||||||
|
// when we intentionally use eager loot-open mode.
|
||||||
|
const bool retryLoot = shouldSendLoot && (turtleMode || isActiveExpansion("classic"));
|
||||||
|
const bool retryUse = turtleMode || isActiveExpansion("classic");
|
||||||
|
if (retryUse || retryLoot) {
|
||||||
|
pendingGameObjectLootRetries_.push_back(PendingLootRetry{guid, 0.15f, 2, retryLoot});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9993,14 +10043,11 @@ void GameHandler::acceptQuest() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// WotLK/TBC expect an additional trailing flag on CMSG_QUESTGIVER_ACCEPT_QUEST.
|
// Keep quest accept payload minimal and expansion-safe: guid + questId.
|
||||||
// Classic/Turtle use the short form (guid + questId only).
|
// Some server cores reject trailing bytes and throw ByteBufferException.
|
||||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
|
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
|
||||||
packet.writeUInt64(npcGuid);
|
packet.writeUInt64(npcGuid);
|
||||||
packet.writeUInt32(questId);
|
packet.writeUInt32(questId);
|
||||||
if (!isActiveExpansion("classic") && !isActiveExpansion("turtle")) {
|
|
||||||
packet.writeUInt8(1); // from-gossip / auto-accept continuation flag
|
|
||||||
}
|
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
pendingQuestAcceptTimeouts_[questId] = 5.0f;
|
pendingQuestAcceptTimeouts_[questId] = 5.0f;
|
||||||
pendingQuestAcceptNpcGuids_[questId] = npcGuid;
|
pendingQuestAcceptNpcGuids_[questId] = npcGuid;
|
||||||
|
|
|
||||||
|
|
@ -1317,10 +1317,6 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
bool hoverInteractableGo = false;
|
bool hoverInteractableGo = false;
|
||||||
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
||||||
if (entity->getType() != game::ObjectType::GAMEOBJECT) continue;
|
if (entity->getType() != game::ObjectType::GAMEOBJECT) continue;
|
||||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
|
||||||
auto* goInfo = gameHandler.getCachedGameObjectInfo(go->getEntry());
|
|
||||||
uint32_t goType = goInfo ? goInfo->type : 0;
|
|
||||||
if (goType == 5) continue; // decoration/non-interactable generic
|
|
||||||
|
|
||||||
glm::vec3 hitCenter;
|
glm::vec3 hitCenter;
|
||||||
float hitRadius = 0.0f;
|
float hitRadius = 0.0f;
|
||||||
|
|
@ -1377,7 +1373,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
||||||
auto t = entity->getType();
|
auto t = entity->getType();
|
||||||
if (t != game::ObjectType::UNIT &&
|
if (t != game::ObjectType::UNIT &&
|
||||||
t != game::ObjectType::PLAYER) continue;
|
t != game::ObjectType::PLAYER &&
|
||||||
|
t != game::ObjectType::GAMEOBJECT) continue;
|
||||||
if (guid == myGuid) continue; // Don't target self
|
if (guid == myGuid) continue; // Don't target self
|
||||||
|
|
||||||
glm::vec3 hitCenter;
|
glm::vec3 hitCenter;
|
||||||
|
|
@ -1394,6 +1391,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
hitRadius = 0.5f;
|
hitRadius = 0.5f;
|
||||||
heightOffset = 0.3f;
|
heightOffset = 0.3f;
|
||||||
}
|
}
|
||||||
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
||||||
|
hitRadius = 2.5f;
|
||||||
|
heightOffset = 1.2f;
|
||||||
}
|
}
|
||||||
hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
||||||
hitCenter.z += heightOffset;
|
hitCenter.z += heightOffset;
|
||||||
|
|
@ -1423,6 +1423,17 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
// Right-click: select NPC (if needed) then interact / loot / auto-attack
|
// Right-click: select NPC (if needed) then interact / loot / auto-attack
|
||||||
// Suppress when left button is held (both-button run)
|
// Suppress when left button is held (both-button run)
|
||||||
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT) && !input.isMouseButtonPressed(SDL_BUTTON_LEFT)) {
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT) && !input.isMouseButtonPressed(SDL_BUTTON_LEFT)) {
|
||||||
|
// If a gameobject is already targeted, prioritize interacting with that target
|
||||||
|
// instead of re-picking under cursor (which can hit nearby decorative GOs).
|
||||||
|
if (gameHandler.hasTarget()) {
|
||||||
|
auto target = gameHandler.getTarget();
|
||||||
|
if (target && target->getType() == game::ObjectType::GAMEOBJECT) {
|
||||||
|
gameHandler.setTarget(target->getGuid());
|
||||||
|
gameHandler.interactWithGameObject(target->getGuid());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If no target or right-clicking in world, try to pick one under cursor
|
// If no target or right-clicking in world, try to pick one under cursor
|
||||||
{
|
{
|
||||||
auto* renderer = core::Application::getInstance().getRenderer();
|
auto* renderer = core::Application::getInstance().getRenderer();
|
||||||
|
|
@ -1456,12 +1467,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
heightOffset = 0.3f;
|
heightOffset = 0.3f;
|
||||||
}
|
}
|
||||||
} else if (t == game::ObjectType::GAMEOBJECT) {
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
||||||
// Check GO type — skip non-interactable decorations
|
// Do not hard-filter by GO type here. Some realms/content
|
||||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
// classify usable objects (including some chests) with types
|
||||||
auto* goInfo = gameHandler.getCachedGameObjectInfo(go->getEntry());
|
// that look decorative in cache data.
|
||||||
uint32_t goType = goInfo ? goInfo->type : 0;
|
|
||||||
// Type 5 = GENERIC (decorations), skip
|
|
||||||
if (goType == 5) continue;
|
|
||||||
hitRadius = 2.5f;
|
hitRadius = 2.5f;
|
||||||
heightOffset = 1.2f;
|
heightOffset = 1.2f;
|
||||||
}
|
}
|
||||||
|
|
@ -1482,6 +1490,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
if (closestGuid != 0) {
|
if (closestGuid != 0) {
|
||||||
if (closestType == game::ObjectType::GAMEOBJECT) {
|
if (closestType == game::ObjectType::GAMEOBJECT) {
|
||||||
|
gameHandler.setTarget(closestGuid);
|
||||||
gameHandler.interactWithGameObject(closestGuid);
|
gameHandler.interactWithGameObject(closestGuid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue