mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +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;
|
||||
float timer = 0.0f;
|
||||
uint8_t remainingRetries = 0;
|
||||
bool sendLoot = false;
|
||||
};
|
||||
std::vector<PendingLootRetry> pendingGameObjectLootRetries_;
|
||||
struct PendingLootOpen {
|
||||
uint64_t guid = 0;
|
||||
float timer = 0.0f;
|
||||
};
|
||||
std::vector<PendingLootOpen> pendingGameObjectLootOpens_;
|
||||
uint64_t pendingLootMoneyGuid_ = 0;
|
||||
uint32_t pendingLootMoneyAmount_ = 0;
|
||||
float pendingLootMoneyNotifyTimer_ = 0.0f;
|
||||
|
|
|
|||
|
|
@ -616,10 +616,14 @@ void GameHandler::update(float deltaTime) {
|
|||
it->timer -= deltaTime;
|
||||
if (it->timer <= 0.0f) {
|
||||
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);
|
||||
socket->send(usePacket);
|
||||
auto lootPacket = LootPacket::build(it->guid);
|
||||
socket->send(lootPacket);
|
||||
if (it->sendLoot) {
|
||||
auto lootPacket = LootPacket::build(it->guid);
|
||||
socket->send(lootPacket);
|
||||
}
|
||||
--it->remainingRetries;
|
||||
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) {
|
||||
pendingLootMoneyNotifyTimer_ -= deltaTime;
|
||||
if (pendingLootMoneyNotifyTimer_ <= 0.0f) {
|
||||
|
|
@ -712,7 +728,7 @@ void GameHandler::update(float deltaTime) {
|
|||
|
||||
// Update cast timer (Phase 3)
|
||||
if (pendingGameObjectInteractGuid_ != 0 &&
|
||||
(autoAttacking || !hostileAttackers_.empty())) {
|
||||
(autoAttacking || autoAttackRequested_)) {
|
||||
pendingGameObjectInteractGuid_ = 0;
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
|
|
@ -9671,15 +9687,13 @@ void GameHandler::interactWithNpc(uint64_t guid) {
|
|||
void GameHandler::interactWithGameObject(uint64_t guid) {
|
||||
if (guid == 0) return;
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
if (casting && currentCastSpellId != 0) return; // don't overlap spell cast bar
|
||||
if (autoAttacking) {
|
||||
stopAutoAttack();
|
||||
}
|
||||
pendingGameObjectInteractGuid_ = guid;
|
||||
casting = true;
|
||||
currentCastSpellId = 0;
|
||||
castTimeTotal = 1.5f;
|
||||
castTimeRemaining = castTimeTotal;
|
||||
// Do not overlap an actual spell cast.
|
||||
if (casting && currentCastSpellId != 0) return;
|
||||
// Always clear melee intent before GO interactions.
|
||||
stopAutoAttack();
|
||||
// Interact immediately; server drives any real cast/channel feedback.
|
||||
pendingGameObjectInteractGuid_ = 0;
|
||||
performGameObjectInteractionNow(guid);
|
||||
}
|
||||
|
||||
void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
||||
|
|
@ -9691,19 +9705,45 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
|||
static uint64_t lastInteractGuid = 0;
|
||||
static std::chrono::steady_clock::time_point lastInteractTime{};
|
||||
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 &&
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - lastInteractTime).count() < minRepeatMs) {
|
||||
return; // Ignore repeated clicks within 1 second
|
||||
return;
|
||||
}
|
||||
lastInteractGuid = guid;
|
||||
lastInteractTime = now;
|
||||
|
||||
// Ensure chest interaction isn't blocked by our own auto-attack state.
|
||||
if (autoAttacking) {
|
||||
stopAutoAttack();
|
||||
}
|
||||
// Ensure GO interaction isn't blocked by stale or active melee state.
|
||||
stopAutoAttack();
|
||||
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);
|
||||
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
|
||||
// animation/sound and expects the client to request the mail list.
|
||||
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) {
|
||||
auto go = std::static_pointer_cast<GameObject>(entity);
|
||||
auto* info = getCachedGameObjectInfo(go->getEntry());
|
||||
|
|
@ -9726,29 +9769,36 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
|||
selectedMailIndex_ = -1;
|
||||
showMailCompose_ = false;
|
||||
refreshMailList();
|
||||
} else {
|
||||
// Keep non-Turtle behavior constrained to known lootable GO types.
|
||||
if (!turtleMode) {
|
||||
if (info && (info->type == 3 || info->type == 25)) {
|
||||
shouldSendLoot = true;
|
||||
} else if (info) {
|
||||
shouldSendLoot = false;
|
||||
} else {
|
||||
shouldSendLoot = true;
|
||||
}
|
||||
} else {
|
||||
// Turtle compatibility: aggressively pair use+loot for chest-like objects.
|
||||
shouldSendLoot = true;
|
||||
}
|
||||
} else if (info && info->type == 3) {
|
||||
chestLike = true;
|
||||
} else if (turtleMode) {
|
||||
// Turtle compatibility: keep eager loot open behavior.
|
||||
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) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
// WotLK/TBC expect an additional trailing flag on CMSG_QUESTGIVER_ACCEPT_QUEST.
|
||||
// Classic/Turtle use the short form (guid + questId only).
|
||||
// Keep quest accept payload minimal and expansion-safe: guid + questId.
|
||||
// Some server cores reject trailing bytes and throw ByteBufferException.
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(questId);
|
||||
if (!isActiveExpansion("classic") && !isActiveExpansion("turtle")) {
|
||||
packet.writeUInt8(1); // from-gossip / auto-accept continuation flag
|
||||
}
|
||||
socket->send(packet);
|
||||
pendingQuestAcceptTimeouts_[questId] = 5.0f;
|
||||
pendingQuestAcceptNpcGuids_[questId] = npcGuid;
|
||||
|
|
|
|||
|
|
@ -1317,10 +1317,6 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
bool hoverInteractableGo = false;
|
||||
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
||||
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;
|
||||
float hitRadius = 0.0f;
|
||||
|
|
@ -1377,7 +1373,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
||||
auto t = entity->getType();
|
||||
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
|
||||
|
||||
glm::vec3 hitCenter;
|
||||
|
|
@ -1394,6 +1391,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
hitRadius = 0.5f;
|
||||
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.z += heightOffset;
|
||||
|
|
@ -1423,6 +1423,17 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
// Right-click: select NPC (if needed) then interact / loot / auto-attack
|
||||
// Suppress when left button is held (both-button run)
|
||||
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
|
||||
{
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
|
|
@ -1456,12 +1467,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
heightOffset = 0.3f;
|
||||
}
|
||||
} else if (t == game::ObjectType::GAMEOBJECT) {
|
||||
// Check GO type — skip non-interactable decorations
|
||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
||||
auto* goInfo = gameHandler.getCachedGameObjectInfo(go->getEntry());
|
||||
uint32_t goType = goInfo ? goInfo->type : 0;
|
||||
// Type 5 = GENERIC (decorations), skip
|
||||
if (goType == 5) continue;
|
||||
// Do not hard-filter by GO type here. Some realms/content
|
||||
// classify usable objects (including some chests) with types
|
||||
// that look decorative in cache data.
|
||||
hitRadius = 2.5f;
|
||||
heightOffset = 1.2f;
|
||||
}
|
||||
|
|
@ -1482,6 +1490,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
}
|
||||
if (closestGuid != 0) {
|
||||
if (closestType == game::ObjectType::GAMEOBJECT) {
|
||||
gameHandler.setTarget(closestGuid);
|
||||
gameHandler.interactWithGameObject(closestGuid);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue