diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 8ad451f6..478b4349 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -474,6 +474,7 @@ private: void rebuildOnlineInventory(); void detectInventorySlotBases(const std::map& fields); bool applyInventoryFields(const std::map& fields); + uint64_t resolveOnlineItemGuid(uint32_t itemId) const; // ---- Phase 2 handlers ---- void handleAttackStart(network::Packet& packet); diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 954872d5..237c1f8c 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -143,6 +143,11 @@ public: */ bool loadTile(int x, int y); + /** + * Enqueue a tile for async loading (returns false if previously failed). + */ + bool enqueueTile(int x, int y); + /** * Unload a tile * @param x Tile X coordinate diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index d4ed54dc..7c12417c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3120,6 +3120,19 @@ void GameHandler::handleItemQueryResponse(network::Packet& packet) { } } +uint64_t GameHandler::resolveOnlineItemGuid(uint32_t itemId) const { + if (itemId == 0) return 0; + uint64_t found = 0; + for (const auto& [guid, info] : onlineItems_) { + if (info.entry != itemId) continue; + if (found != 0) { + return 0; // Ambiguous + } + found = guid; + } + return found; +} + void GameHandler::detectInventorySlotBases(const std::map& fields) { if (invSlotBase_ >= 0 && packSlotBase_ >= 0) return; if (onlineItems_.empty() || fields.empty()) return; @@ -3215,6 +3228,31 @@ void GameHandler::rebuildOnlineInventory() { def.name = "Item " + std::to_string(def.itemId); queryItemInfo(def.itemId, guid); } + if (def.itemId != 0) { + auto& db = getSinglePlayerLootDb(); + auto itTpl = db.itemTemplates.find(def.itemId); + if (itTpl != db.itemTemplates.end()) { + if (def.name.empty() || def.name.rfind("Item ", 0) == 0) def.name = itTpl->second.name; + if (def.quality == ItemQuality::COMMON && itTpl->second.quality != 0) { + def.quality = static_cast(itTpl->second.quality); + } + if (def.inventoryType == 0 && itTpl->second.inventoryType != 0) { + def.inventoryType = itTpl->second.inventoryType; + } + if (def.maxStack <= 1 && itTpl->second.maxStack > 1) { + def.maxStack = static_cast(itTpl->second.maxStack); + } + if (def.displayInfoId == 0 && itTpl->second.displayId != 0) { + def.displayInfoId = itTpl->second.displayId; + } + if (def.armor == 0 && itTpl->second.armor > 0) def.armor = itTpl->second.armor; + if (def.stamina == 0 && itTpl->second.stamina > 0) def.stamina = itTpl->second.stamina; + if (def.strength == 0 && itTpl->second.strength > 0) def.strength = itTpl->second.strength; + if (def.agility == 0 && itTpl->second.agility > 0) def.agility = itTpl->second.agility; + if (def.intellect == 0 && itTpl->second.intellect > 0) def.intellect = itTpl->second.intellect; + if (def.spirit == 0 && itTpl->second.spirit > 0) def.spirit = itTpl->second.spirit; + } + } inventory.setEquipSlot(static_cast(i), def); } @@ -3250,6 +3288,31 @@ void GameHandler::rebuildOnlineInventory() { def.name = "Item " + std::to_string(def.itemId); queryItemInfo(def.itemId, guid); } + if (def.itemId != 0) { + auto& db = getSinglePlayerLootDb(); + auto itTpl = db.itemTemplates.find(def.itemId); + if (itTpl != db.itemTemplates.end()) { + if (def.name.empty() || def.name.rfind("Item ", 0) == 0) def.name = itTpl->second.name; + if (def.quality == ItemQuality::COMMON && itTpl->second.quality != 0) { + def.quality = static_cast(itTpl->second.quality); + } + if (def.inventoryType == 0 && itTpl->second.inventoryType != 0) { + def.inventoryType = itTpl->second.inventoryType; + } + if (def.maxStack <= 1 && itTpl->second.maxStack > 1) { + def.maxStack = static_cast(itTpl->second.maxStack); + } + if (def.displayInfoId == 0 && itTpl->second.displayId != 0) { + def.displayInfoId = itTpl->second.displayId; + } + if (def.armor == 0 && itTpl->second.armor > 0) def.armor = itTpl->second.armor; + if (def.stamina == 0 && itTpl->second.stamina > 0) def.stamina = itTpl->second.stamina; + if (def.strength == 0 && itTpl->second.strength > 0) def.strength = itTpl->second.strength; + if (def.agility == 0 && itTpl->second.agility > 0) def.agility = itTpl->second.agility; + if (def.intellect == 0 && itTpl->second.intellect > 0) def.intellect = itTpl->second.intellect; + if (def.spirit == 0 && itTpl->second.spirit > 0) def.spirit = itTpl->second.spirit; + } + } inventory.setBackpackSlot(i, def); } @@ -3846,6 +3909,9 @@ void GameHandler::lootItem(uint8_t slotIndex) { void GameHandler::closeLoot() { if (!lootWindowOpen) return; lootWindowOpen = false; + if (currentLoot.lootGuid != 0 && targetGuid == currentLoot.lootGuid) { + clearTarget(); + } if (singlePlayerMode_ && currentLoot.lootGuid != 0) { auto st = localLootState_.find(currentLoot.lootGuid); if (st != localLootState_.end()) { @@ -3987,8 +4053,13 @@ void GameHandler::sellItemBySlot(int backpackIndex) { notifyInventoryChanged(); } else { uint64_t itemGuid = backpackSlotGuids_[backpackIndex]; + if (itemGuid == 0) { + itemGuid = resolveOnlineItemGuid(slot.item.itemId); + } if (itemGuid != 0 && currentVendorItems.vendorGuid != 0) { sellItem(currentVendorItems.vendorGuid, itemGuid, 1); + } else if (itemGuid == 0) { + LOG_WARNING("Sell failed: missing item GUID for slot ", backpackIndex); } } } @@ -4004,9 +4075,14 @@ void GameHandler::autoEquipItemBySlot(int backpackIndex) { } uint64_t itemGuid = backpackSlotGuids_[backpackIndex]; + if (itemGuid == 0) { + itemGuid = resolveOnlineItemGuid(slot.item.itemId); + } if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) { auto packet = AutoEquipItemPacket::build(itemGuid); socket->send(packet); + } else if (itemGuid == 0) { + LOG_WARNING("Auto-equip failed: missing item GUID for slot ", backpackIndex); } } @@ -4021,9 +4097,14 @@ void GameHandler::useItemBySlot(int backpackIndex) { } uint64_t itemGuid = backpackSlotGuids_[backpackIndex]; + if (itemGuid == 0) { + itemGuid = resolveOnlineItemGuid(slot.item.itemId); + } if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) { auto packet = UseItemPacket::build(0xFF, static_cast(backpackIndex), itemGuid); socket->send(packet); + } else if (itemGuid == 0) { + LOG_WARNING("Use item failed: missing item GUID for slot ", backpackIndex); } } diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 3abb4e24..e07a54ac 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -661,7 +661,7 @@ void Renderer::updateCharacterAnimation() { } else if (anyStrafeRight) { animId = pickFirstAvailable({ANIM_STRAFE_WALK_RIGHT, ANIM_STRAFE_RUN_RIGHT}, ANIM_WALK); } else { - animId = ANIM_WALK; + animId = pickFirstAvailable({ANIM_WALK, ANIM_RUN}, ANIM_STAND); } loop = true; break; @@ -673,7 +673,7 @@ void Renderer::updateCharacterAnimation() { } else if (anyStrafeRight) { animId = pickFirstAvailable({ANIM_STRAFE_RUN_RIGHT}, ANIM_RUN); } else { - animId = ANIM_RUN; + animId = pickFirstAvailable({ANIM_RUN, ANIM_WALK}, ANIM_STAND); } loop = true; break; @@ -1622,11 +1622,11 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: } } - LOG_INFO("Loading initial tile [", tileX, ",", tileY, "] via terrain manager"); + LOG_INFO("Enqueuing initial tile [", tileX, ",", tileY, "] via terrain manager"); - // Load the initial tile through TerrainManager (properly tracked for streaming) - if (!terrainManager->loadTile(tileX, tileY)) { - LOG_ERROR("Failed to load initial tile [", tileX, ",", tileY, "]"); + // Enqueue the initial tile for async loading (avoids long sync stalls) + if (!terrainManager->enqueueTile(tileX, tileY)) { + LOG_ERROR("Failed to enqueue initial tile [", tileX, ",", tileY, "]"); return false; } diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index 22ab20f0..94c097b5 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -194,6 +194,27 @@ bool TerrainManager::loadTile(int x, int y) { return true; } +bool TerrainManager::enqueueTile(int x, int y) { + TileCoord coord = {x, y}; + if (loadedTiles.find(coord) != loadedTiles.end()) { + return true; + } + if (pendingTiles.find(coord) != pendingTiles.end()) { + return true; + } + if (failedTiles.find(coord) != failedTiles.end()) { + return false; + } + + { + std::lock_guard lock(queueMutex); + loadQueue.push(coord); + pendingTiles[coord] = true; + } + queueCV.notify_all(); + return true; +} + std::unique_ptr TerrainManager::prepareTile(int x, int y) { TileCoord coord = {x, y}; diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 68ab6db4..2d699244 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -1097,15 +1097,18 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite inventoryDirty = true; } } else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { + bool looksEquipable = (item.inventoryType > 0) || + (item.armor > 0) || + (!item.subclassName.empty()); if (gameHandler_ && !gameHandler_->isSinglePlayerMode()) { - if (item.inventoryType > 0) { + if (looksEquipable) { // Auto-equip (online) gameHandler_->autoEquipItemBySlot(backpackIndex); } else { // Use consumable (online) gameHandler_->useItemBySlot(backpackIndex); } - } else if (item.inventoryType > 0) { + } else if (looksEquipable) { // Auto-equip (single-player) uint8_t equippingType = item.inventoryType; game::EquipSlot targetSlot = getEquipSlotForType(equippingType, inventory);