diff --git a/Data/expansions/wotlk/update_fields.json b/Data/expansions/wotlk/update_fields.json index 1ea25b2b..f308cf0d 100644 --- a/Data/expansions/wotlk/update_fields.json +++ b/Data/expansions/wotlk/update_fields.json @@ -18,8 +18,8 @@ "UNIT_FIELD_RESISTANCES": 99, "UNIT_END": 148, "PLAYER_FLAGS": 150, - "PLAYER_BYTES": 151, - "PLAYER_BYTES_2": 152, + "PLAYER_BYTES": 153, + "PLAYER_BYTES_2": 154, "PLAYER_XP": 634, "PLAYER_NEXT_LEVEL_XP": 635, "PLAYER_FIELD_COINAGE": 1170, diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index 4ec0b418..b25d5234 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -89,8 +89,11 @@ public: const ItemSlot& getBankBagSlot(int bagIndex, int slotIndex) const; bool setBankBagSlot(int bagIndex, int slotIndex, const ItemDef& item); + bool clearBankBagSlot(int bagIndex, int slotIndex); int getBankBagSize(int bagIndex) const; void setBankBagSize(int bagIndex, int size); + const ItemSlot& getBankBagItem(int bagIndex) const; + void setBankBagItem(int bagIndex, const ItemDef& item); uint8_t getPurchasedBankBagSlots() const { return purchasedBankBagSlots_; } void setPurchasedBankBagSlots(uint8_t count) { purchasedBankBagSlots_ = count; } @@ -111,6 +114,7 @@ private: struct BagData { int size = 0; + ItemSlot bagItem; // The bag item itself (for icon/name/tooltip) std::array slots{}; }; std::array bags{}; diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index ee06eb30..0090edc4 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -212,6 +212,7 @@ public: * Unload all tiles */ void unloadAll(); + void stopWorkers(); // Stop worker threads without restarting (for shutdown) void softReset(); // Clear tile data without stopping worker threads (non-blocking) /** @@ -262,6 +263,9 @@ public: /** Process all ready tiles immediately (use during loading screens) */ void processAllReadyTiles(); + /** Process one ready tile (for loading screens with per-tile progress updates) */ + void processOneReadyTile(); + private: /** * Get tile coordinates from GL world position diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index 2d2ca9c9..bc580bde 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -118,11 +118,14 @@ private: // Drag-and-drop held item state bool holdingItem = false; game::ItemDef heldItem; - enum class HeldSource { NONE, BACKPACK, BAG, EQUIPMENT }; + enum class HeldSource { NONE, BACKPACK, BAG, EQUIPMENT, BANK, BANK_BAG, BANK_BAG_EQUIP }; HeldSource heldSource = HeldSource::NONE; int heldBackpackIndex = -1; int heldBagIndex = -1; int heldBagSlotIndex = -1; + int heldBankIndex = -1; + int heldBankBagIndex = -1; + int heldBankBagSlotIndex = -1; game::EquipSlot heldEquipSlot = game::EquipSlot::NUM_SLOTS; // Slot rendering with interaction support @@ -136,7 +139,7 @@ private: int pickupBagIndex_ = -1; int pickupBagSlotIndex_ = -1; game::EquipSlot pickupEquipSlot_ = game::EquipSlot::NUM_SLOTS; - static constexpr float kPickupHoldThreshold = 0.12f; // seconds + static constexpr float kPickupHoldThreshold = 0.10f; // seconds void renderSeparateBags(game::Inventory& inventory, uint64_t moneyCopper); void renderAggregateBags(game::Inventory& inventory, uint64_t moneyCopper); @@ -186,6 +189,12 @@ public: bool dropHeldItemToEquipSlot(game::Inventory& inv, game::EquipSlot slot); /// Drop the currently held item into a bank slot via CMSG_SWAP_ITEM. void dropIntoBankSlot(game::GameHandler& gh, uint8_t dstBag, uint8_t dstSlot); + /// Pick up an item from main bank slot (click-and-hold from bank window). + void pickupFromBank(game::Inventory& inv, int bankIndex); + /// Pick up an item from a bank bag slot (click-and-hold from bank window). + void pickupFromBankBag(game::Inventory& inv, int bagIndex, int slotIndex); + /// Pick up a bag from a bank bag equip slot (click-and-hold from bank window). + void pickupFromBankBagEquip(game::Inventory& inv, int bagIndex); }; } // namespace ui diff --git a/src/core/application.cpp b/src/core/application.cpp index 6ecaaf03..7e75ede3 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -421,34 +421,43 @@ void Application::run() { } void Application::shutdown() { - LOG_INFO("Shutting down application"); + LOG_WARNING("Shutting down application..."); // Save floor cache before renderer is destroyed if (renderer && renderer->getWMORenderer()) { size_t cacheSize = renderer->getWMORenderer()->getFloorCacheSize(); if (cacheSize > 0) { - LOG_INFO("Saving WMO floor cache (", cacheSize, " entries)..."); + LOG_WARNING("Saving WMO floor cache (", cacheSize, " entries)..."); renderer->getWMORenderer()->saveFloorCache(); + LOG_WARNING("Floor cache saved."); } } // Explicitly shut down the renderer before destroying it — this ensures // all sub-renderers free their VMA allocations in the correct order, // before VkContext::shutdown() calls vmaDestroyAllocator(). + LOG_WARNING("Shutting down renderer..."); if (renderer) { renderer->shutdown(); } + LOG_WARNING("Renderer shutdown complete, resetting..."); renderer.reset(); + LOG_WARNING("Resetting world..."); world.reset(); + LOG_WARNING("Resetting gameHandler..."); gameHandler.reset(); + LOG_WARNING("Resetting authHandler..."); authHandler.reset(); + LOG_WARNING("Resetting assetManager..."); assetManager.reset(); + LOG_WARNING("Resetting uiManager..."); uiManager.reset(); + LOG_WARNING("Resetting window..."); window.reset(); running = false; - LOG_INFO("Application shutdown complete"); + LOG_WARNING("Application shutdown complete"); } void Application::setState(AppState newState) { @@ -3335,8 +3344,8 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float auto startTime = std::chrono::high_resolution_clock::now(); auto lastProgressTime = startTime; - const float maxWaitSeconds = 20.0f; - const float stallSeconds = 5.0f; + const float maxWaitSeconds = 60.0f; + const float stallSeconds = 10.0f; int initialRemaining = terrainMgr->getRemainingTileCount(); if (initialRemaining < 1) initialRemaining = 1; int lastRemaining = initialRemaining; @@ -3362,28 +3371,43 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float } } - // Trigger new streaming and process ALL ready tiles (not just 2) + // Trigger new streaming — enqueue tiles for background workers terrainMgr->update(*camera, 0.016f); - terrainMgr->processAllReadyTiles(); + + // Process ONE tile per iteration so loading screen updates after each + terrainMgr->processOneReadyTile(); + + int remaining = terrainMgr->getRemainingTileCount(); + int loaded = terrainMgr->getLoadedTileCount(); + int total = loaded + remaining; + if (total < 1) total = 1; + float tileProgress = static_cast(loaded) / static_cast(total); + float progress = 0.35f + tileProgress * 0.50f; + + auto now = std::chrono::high_resolution_clock::now(); + float elapsedSec = std::chrono::duration(now - startTime).count(); + + char buf[192]; + if (loaded > 0 && remaining > 0) { + float tilesPerSec = static_cast(loaded) / std::max(elapsedSec, 0.1f); + float etaSec = static_cast(remaining) / std::max(tilesPerSec, 0.1f); + snprintf(buf, sizeof(buf), "Loading terrain... %d / %d tiles (%.0f tiles/s, ~%.0fs remaining)", + loaded, total, tilesPerSec, etaSec); + } else { + snprintf(buf, sizeof(buf), "Loading terrain... %d / %d tiles", + loaded, total); + } if (loadingScreenOk) { - int remaining = terrainMgr->getRemainingTileCount(); - int loaded = terrainMgr->getLoadedTileCount(); - float tileProgress = static_cast(initialRemaining - remaining) / initialRemaining; - if (tileProgress < 0.0f) tileProgress = 0.0f; - float progress = 0.35f + tileProgress * 0.50f; - char buf[128]; - snprintf(buf, sizeof(buf), "Loading terrain... %d tiles loaded, %d remaining", - loaded, remaining); loadingScreen.setStatus(buf); loadingScreen.setProgress(progress); loadingScreen.render(); window->swapBuffers(); + } - if (remaining != lastRemaining) { - lastRemaining = remaining; - lastProgressTime = std::chrono::high_resolution_clock::now(); - } + if (remaining != lastRemaining) { + lastRemaining = remaining; + lastProgressTime = now; } auto elapsed = std::chrono::high_resolution_clock::now() - startTime; @@ -3398,7 +3422,10 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float break; } - SDL_Delay(16); + // Don't sleep if there are more tiles to finalize — keep processing + if (remaining > 0 && terrainMgr->getReadyQueueCount() == 0) { + SDL_Delay(16); + } } LOG_INFO("Online terrain streaming complete: ", terrainMgr->getLoadedTileCount(), " tiles loaded"); @@ -3406,8 +3433,11 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // Load/precompute collision cache if (renderer->getWMORenderer()) { showProgress("Building collision cache...", 0.88f); + if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); } renderer->getWMORenderer()->loadFloorCache(); if (renderer->getWMORenderer()->getFloorCacheSize() == 0) { + showProgress("Computing walkable surfaces...", 0.90f); + if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); } renderer->getWMORenderer()->precomputeFloorCache(); } } diff --git a/src/core/window.cpp b/src/core/window.cpp index f533689b..eed83c97 100644 --- a/src/core/window.cpp +++ b/src/core/window.cpp @@ -94,19 +94,22 @@ bool Window::initialize() { } void Window::shutdown() { + LOG_WARNING("Window::shutdown - vkContext..."); if (vkContext) { vkContext->shutdown(); vkContext.reset(); } + LOG_WARNING("Window::shutdown - SDL_DestroyWindow..."); if (window) { SDL_DestroyWindow(window); window = nullptr; } + LOG_WARNING("Window::shutdown - SDL_Quit..."); SDL_Vulkan_UnloadLibrary(); SDL_Quit(); - LOG_INFO("Window shutdown complete"); + LOG_WARNING("Window shutdown complete"); } void Window::pollEvents() { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 555e31e2..ae8250d7 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -5233,6 +5233,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } else if (ufPBytes2 != 0xFFFF && key == ufPBytes2) { uint8_t bankBagSlots = static_cast((val >> 16) & 0xFF); + LOG_WARNING("PLAYER_BYTES_2 (CREATE): raw=0x", std::hex, val, std::dec, + " bankBagSlots=", static_cast(bankBagSlots)); inventory.setPurchasedBankBagSlots(bankBagSlots); } // Do not synthesize quest-log entries from raw update-field slots. @@ -5535,6 +5537,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } else if (ufPBytes2v != 0xFFFF && key == ufPBytes2v) { uint8_t bankBagSlots = static_cast((val >> 16) & 0xFF); + LOG_WARNING("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec, + " bankBagSlots=", static_cast(bankBagSlots)); inventory.setPurchasedBankBagSlots(bankBagSlots); } else if (key == ufPlayerFlags) { @@ -7707,7 +7711,9 @@ void GameHandler::extractContainerFields(uint64_t containerGuid, const std::map< void GameHandler::rebuildOnlineInventory() { + uint8_t savedBankBagSlots = inventory.getPurchasedBankBagSlots(); inventory = Inventory(); + inventory.setPurchasedBankBagSlots(savedBankBagSlots); // Equipment slots for (int i = 0; i < 23; i++) { @@ -7910,14 +7916,31 @@ void GameHandler::rebuildOnlineInventory() { if (contIt != containerContents_.end()) { numSlots = static_cast(contIt->second.numSlots); } - if (numSlots <= 0) { - auto bagItemIt = onlineItems_.find(bagGuid); - if (bagItemIt != onlineItems_.end()) { + + // Populate the bag item itself (for icon/name in the bank bag equip slot) + auto bagItemIt = onlineItems_.find(bagGuid); + if (bagItemIt != onlineItems_.end()) { + if (numSlots <= 0) { auto bagInfoIt = itemInfoCache_.find(bagItemIt->second.entry); if (bagInfoIt != itemInfoCache_.end()) { numSlots = bagInfoIt->second.containerSlots; } } + ItemDef bagDef; + bagDef.itemId = bagItemIt->second.entry; + bagDef.stackCount = 1; + bagDef.inventoryType = 18; // bag + auto bagInfoIt = itemInfoCache_.find(bagItemIt->second.entry); + if (bagInfoIt != itemInfoCache_.end()) { + bagDef.name = bagInfoIt->second.name; + bagDef.quality = static_cast(bagInfoIt->second.quality); + bagDef.displayInfoId = bagInfoIt->second.displayInfoId; + bagDef.bagSlots = bagInfoIt->second.containerSlots; + } else { + bagDef.name = "Bag"; + queryItemInfo(bagDef.itemId, bagGuid); + } + inventory.setBankBagItem(bagIdx, bagDef); } if (numSlots <= 0) continue; @@ -13673,7 +13696,12 @@ void GameHandler::closeBank() { } void GameHandler::buyBankSlot() { - if (!isConnected() || !bankOpen_) return; + if (!isConnected() || !bankOpen_) { + LOG_WARNING("buyBankSlot: not connected or bank not open"); + return; + } + LOG_WARNING("buyBankSlot: sending CMSG_BUY_BANK_SLOT banker=0x", std::hex, bankerGuid_, std::dec, + " purchased=", static_cast(inventory.getPurchasedBankBagSlots())); auto pkt = BuyBankSlotPacket::build(bankerGuid_); socket->send(pkt); } @@ -13698,17 +13726,33 @@ void GameHandler::handleShowBank(network::Packet& packet) { // Bank items are already tracked via update fields (bank slot GUIDs) // Trigger rebuild to populate bank slots in inventory rebuildOnlineInventory(); - LOG_INFO("SMSG_SHOW_BANK: banker=0x", std::hex, bankerGuid_, std::dec); + // Count bank bags that actually have items/containers + int filledBags = 0; + for (int i = 0; i < effectiveBankBagSlots_; i++) { + if (inventory.getBankBagSize(i) > 0) filledBags++; + } + LOG_WARNING("SMSG_SHOW_BANK: banker=0x", std::hex, bankerGuid_, std::dec, + " purchased=", static_cast(inventory.getPurchasedBankBagSlots()), + " filledBags=", filledBags, + " effectiveBankBagSlots=", effectiveBankBagSlots_); } void GameHandler::handleBuyBankSlotResult(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() < 4) return; uint32_t result = packet.readUInt32(); - if (result == 0) { + LOG_WARNING("SMSG_BUY_BANK_SLOT_RESULT: result=", result); + // AzerothCore/TrinityCore: 0=TOO_MANY, 1=INSUFFICIENT_FUNDS, 2=NOT_BANKER, 3=OK + if (result == 3) { addSystemChatMessage("Bank slot purchased."); inventory.setPurchasedBankBagSlots(inventory.getPurchasedBankBagSlots() + 1); + } else if (result == 1) { + addSystemChatMessage("Not enough gold to purchase bank slot."); + } else if (result == 0) { + addSystemChatMessage("No more bank slots available."); + } else if (result == 2) { + addSystemChatMessage("You must be at a banker to purchase bank slots."); } else { - addSystemChatMessage("Cannot purchase bank slot."); + addSystemChatMessage("Cannot purchase bank slot (error " + std::to_string(result) + ")."); } } diff --git a/src/game/inventory.cpp b/src/game/inventory.cpp index 57806ebf..1750253a 100644 --- a/src/game/inventory.cpp +++ b/src/game/inventory.cpp @@ -105,6 +105,13 @@ bool Inventory::setBankBagSlot(int bagIndex, int slotIndex, const ItemDef& item) return true; } +bool Inventory::clearBankBagSlot(int bagIndex, int slotIndex) { + if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return false; + if (slotIndex < 0 || slotIndex >= bankBags_[bagIndex].size) return false; + bankBags_[bagIndex].slots[slotIndex].item = ItemDef{}; + return true; +} + int Inventory::getBankBagSize(int bagIndex) const { if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return 0; return bankBags_[bagIndex].size; @@ -115,6 +122,17 @@ void Inventory::setBankBagSize(int bagIndex, int size) { bankBags_[bagIndex].size = std::min(size, MAX_BAG_SIZE); } +const ItemSlot& Inventory::getBankBagItem(int bagIndex) const { + static const ItemSlot EMPTY_SLOT; + if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return EMPTY_SLOT; + return bankBags_[bagIndex].bagItem; +} + +void Inventory::setBankBagItem(int bagIndex, const ItemDef& item) { + if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return; + bankBags_[bagIndex].bagItem.item = item; +} + void Inventory::swapBagContents(int bagA, int bagB) { if (bagA < 0 || bagA >= NUM_BAG_SLOTS || bagB < 0 || bagB >= NUM_BAG_SLOTS) return; if (bagA == bagB) return; diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index eed0d684..6f02589a 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -1756,15 +1756,47 @@ void CameraController::reset() { return h; }; + // In online mode, try to snap to a nearby floor but fall back to the server + // position when no WMO floor is found (e.g. WMO not loaded yet in cities). + // This prevents spawning under WMO cities like Stormwind. + if (onlineMode) { + auto h = evalFloorAt(spawnPos.x, spawnPos.y, spawnPos.z); + if (h && std::abs(*h - spawnPos.z) < 16.0f) { + spawnPos.z = *h + 0.05f; + } + // else: keep server Z as-is + lastGroundZ = spawnPos.z - 0.05f; + + camera->setRotation(yaw, pitch); + glm::vec3 forward3D = camera->getForward(); + + if (thirdPerson && followTarget) { + *followTarget = spawnPos; + currentDistance = userTargetDistance; + collisionDistance = currentDistance; + float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; + glm::vec3 pivot = spawnPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT + mountedOffset); + glm::vec3 camDir = -forward3D; + glm::vec3 camPos = pivot + camDir * currentDistance; + smoothedCamPos = camPos; + camera->setPosition(camPos); + } else { + spawnPos.z += eyeHeight; + smoothedCamPos = spawnPos; + camera->setPosition(spawnPos); + } + + LOG_INFO("Camera reset to server position (online mode)"); + return; + } + // Search nearby for a stable, non-steep spawn floor to avoid waterfall/ledge spawns. - // In online mode, use a tight search radius since the server dictates position. float bestScore = std::numeric_limits::max(); glm::vec3 bestPos = spawnPos; bool foundBest = false; constexpr float radiiOffline[] = {0.0f, 6.0f, 12.0f, 18.0f, 24.0f, 32.0f}; - constexpr float radiiOnline[] = {0.0f, 2.0f}; - const float* radii = onlineMode ? radiiOnline : radiiOffline; - const int radiiCount = onlineMode ? 2 : 6; + const float* radii = radiiOffline; + const int radiiCount = 6; constexpr int ANGLES = 16; constexpr float PI = 3.14159265f; for (int ri = 0; ri < radiiCount; ri++) { diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index d6d3f4b1..cf4500b7 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -726,31 +726,38 @@ bool Renderer::initialize(core::Window* win) { } void Renderer::shutdown() { + LOG_WARNING("Renderer::shutdown - terrainManager stopWorkers..."); if (terrainManager) { - terrainManager->unloadAll(); + terrainManager->stopWorkers(); + LOG_WARNING("Renderer::shutdown - terrainManager reset..."); terrainManager.reset(); } + LOG_WARNING("Renderer::shutdown - terrainRenderer..."); if (terrainRenderer) { terrainRenderer->shutdown(); terrainRenderer.reset(); } + LOG_WARNING("Renderer::shutdown - waterRenderer..."); if (waterRenderer) { waterRenderer->shutdown(); waterRenderer.reset(); } + LOG_WARNING("Renderer::shutdown - minimap..."); if (minimap) { minimap->shutdown(); minimap.reset(); } + LOG_WARNING("Renderer::shutdown - worldMap..."); if (worldMap) { worldMap->shutdown(); worldMap.reset(); } + LOG_WARNING("Renderer::shutdown - skySystem..."); if (skySystem) { skySystem->shutdown(); skySystem.reset(); @@ -772,34 +779,41 @@ void Renderer::shutdown() { swimEffects.reset(); } + LOG_WARNING("Renderer::shutdown - characterRenderer..."); if (characterRenderer) { characterRenderer->shutdown(); characterRenderer.reset(); } + LOG_WARNING("Renderer::shutdown - wmoRenderer..."); if (wmoRenderer) { wmoRenderer->shutdown(); wmoRenderer.reset(); } + LOG_WARNING("Renderer::shutdown - m2Renderer..."); if (m2Renderer) { m2Renderer->shutdown(); m2Renderer.reset(); } + LOG_WARNING("Renderer::shutdown - musicManager..."); if (musicManager) { musicManager->shutdown(); musicManager.reset(); } + LOG_WARNING("Renderer::shutdown - footstepManager..."); if (footstepManager) { footstepManager->shutdown(); footstepManager.reset(); } + LOG_WARNING("Renderer::shutdown - activitySoundManager..."); if (activitySoundManager) { activitySoundManager->shutdown(); activitySoundManager.reset(); } + LOG_WARNING("Renderer::shutdown - AudioEngine..."); // Shutdown AudioEngine singleton audio::AudioEngine::instance().shutdown(); diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index ee118c1f..a47806d3 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -129,17 +129,7 @@ TerrainManager::TerrainManager() { } TerrainManager::~TerrainManager() { - // Stop worker thread before cleanup (containers clean up via destructors) - if (workerRunning.load()) { - workerRunning.store(false); - queueCV.notify_all(); - for (auto& t : workerThreads) { - if (t.joinable()) { - t.join(); - } - } - workerThreads.clear(); - } + stopWorkers(); } bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* renderer) { @@ -276,6 +266,9 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { LOG_DEBUG("Preparing tile [", x, ",", y, "] (CPU work)"); + // Early-exit check — worker should bail fast during shutdown + if (!workerRunning.load()) return nullptr; + // Load ADT file std::string adtPath = getADTPath(coord); auto adtData = assetManager->readFile(adtPath); @@ -294,6 +287,8 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { return nullptr; } + if (!workerRunning.load()) return nullptr; + // WotLK split ADTs can store placements in *_obj0.adt. // Merge object chunks so doodads/WMOs (including ground clutter) are available. std::string objPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" + @@ -362,6 +357,8 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { return nullptr; } + if (!workerRunning.load()) return nullptr; + auto pending = std::make_shared(); pending->coord = coord; pending->terrain = std::move(*terrainPtr); @@ -412,6 +409,7 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { // Pre-load M2 doodads (CPU: read files, parse models) int skippedNameId = 0, skippedFileNotFound = 0, skippedInvalid = 0, skippedSkinNotFound = 0; for (const auto& placement : pending->terrain.doodadPlacements) { + if (!workerRunning.load()) return nullptr; if (placement.nameId >= pending->terrain.doodadNames.size()) { skippedNameId++; continue; @@ -460,9 +458,12 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { ensureGroundEffectTablesLoaded(); generateGroundClutterPlacements(pending, preparedModelIds); + if (!workerRunning.load()) return nullptr; + // Pre-load WMOs (CPU: read files, parse models and groups) if (!pending->terrain.wmoPlacements.empty()) { for (const auto& placement : pending->terrain.wmoPlacements) { + if (!workerRunning.load()) return nullptr; if (placement.nameId >= pending->terrain.wmoNames.size()) continue; const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId]; @@ -513,6 +514,7 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { ); // Pre-load WMO doodads (M2 models inside WMO) + if (!workerRunning.load()) return nullptr; if (!wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) { glm::mat4 wmoMatrix(1.0f); wmoMatrix = glm::translate(wmoMatrix, pos); @@ -636,6 +638,8 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { } } + if (!workerRunning.load()) return nullptr; + // Pre-load terrain texture BLP data on background thread so finalizeTile // doesn't block the main thread with file I/O. for (const auto& texPath : pending->terrain.textures) { @@ -1068,6 +1072,28 @@ void TerrainManager::processAllReadyTiles() { } } +void TerrainManager::processOneReadyTile() { + // Move ready tiles into finalizing deque + { + std::lock_guard lock(queueMutex); + while (!readyQueue.empty()) { + auto pending = readyQueue.front(); + readyQueue.pop(); + if (pending) { + FinalizingTile ft; + ft.pending = std::move(pending); + finalizingTiles_.push_back(std::move(ft)); + } + } + } + // Finalize ONE tile completely, then return so caller can update the screen + if (!finalizingTiles_.empty()) { + auto& ft = finalizingTiles_.front(); + while (!advanceFinalization(ft)) {} + finalizingTiles_.pop_front(); + } +} + std::shared_ptr TerrainManager::getCachedTile(const TileCoord& coord) { std::lock_guard lock(tileCacheMutex_); auto it = tileCache_.find(coord); @@ -1237,6 +1263,29 @@ void TerrainManager::unloadTile(int x, int y) { loadedTiles.erase(it); } +void TerrainManager::stopWorkers() { + if (!workerRunning.load()) { + LOG_WARNING("stopWorkers: already stopped"); + return; + } + LOG_WARNING("stopWorkers: signaling ", workerThreads.size(), " workers to stop..."); + workerRunning.store(false); + queueCV.notify_all(); + + // Workers check workerRunning at each I/O point in prepareTile() and bail + // out quickly. Use plain join() which is safe with std::thread — no + // pthread_timedjoin_np (which silently joins the pthread but leaves the + // std::thread object thinking it's still joinable → std::terminate on dtor). + for (size_t i = 0; i < workerThreads.size(); i++) { + if (workerThreads[i].joinable()) { + LOG_WARNING("stopWorkers: joining worker ", i, "..."); + workerThreads[i].join(); + } + } + workerThreads.clear(); + LOG_WARNING("stopWorkers: done"); +} + void TerrainManager::unloadAll() { // Signal worker threads to stop and wait briefly for them to finish. // Workers may be mid-prepareTile (reading MPQ / parsing ADT) which can @@ -1245,29 +1294,8 @@ void TerrainManager::unloadAll() { workerRunning.store(false); queueCV.notify_all(); - auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500); for (auto& t : workerThreads) { - if (!t.joinable()) continue; - // Try a timed wait via polling — std::thread has no timed join. - bool joined = false; - while (std::chrono::steady_clock::now() < deadline) { - // Check if thread finished by trying a native timed join - #ifdef __linux__ - struct timespec ts; - clock_gettime(CLOCK_REALTIME, &ts); - ts.tv_nsec += 50000000; // 50ms - if (ts.tv_nsec >= 1000000000) { ts.tv_sec++; ts.tv_nsec -= 1000000000; } - if (pthread_timedjoin_np(t.native_handle(), nullptr, &ts) == 0) { - joined = true; - break; - } - #else - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - #endif - } - if (!joined && t.joinable()) { - t.detach(); - } + if (t.joinable()) t.join(); } workerThreads.clear(); } diff --git a/src/rendering/vk_context.cpp b/src/rendering/vk_context.cpp index 8dcdef1b..8a7fd505 100644 --- a/src/rendering/vk_context.cpp +++ b/src/rendering/vk_context.cpp @@ -50,10 +50,12 @@ bool VkContext::initialize(SDL_Window* window) { } void VkContext::shutdown() { + LOG_WARNING("VkContext::shutdown - vkDeviceWaitIdle..."); if (device) { vkDeviceWaitIdle(device); } + LOG_WARNING("VkContext::shutdown - destroyImGuiResources..."); destroyImGuiResources(); // Destroy sync objects @@ -68,9 +70,16 @@ void VkContext::shutdown() { if (immFence) { vkDestroyFence(device, immFence, nullptr); immFence = VK_NULL_HANDLE; } if (immCommandPool) { vkDestroyCommandPool(device, immCommandPool, nullptr); immCommandPool = VK_NULL_HANDLE; } + LOG_WARNING("VkContext::shutdown - destroySwapchain..."); destroySwapchain(); - if (allocator) { vmaDestroyAllocator(allocator); allocator = VK_NULL_HANDLE; } + // Skip vmaDestroyAllocator — it walks every allocation to free it, which + // takes many seconds with thousands of loaded textures/models. The driver + // reclaims all device memory when we destroy the device, and the OS reclaims + // everything on process exit. Skipping this makes shutdown instant. + allocator = VK_NULL_HANDLE; + + LOG_WARNING("VkContext::shutdown - vkDestroyDevice..."); if (device) { vkDestroyDevice(device, nullptr); device = VK_NULL_HANDLE; } if (surface) { vkDestroySurfaceKHR(instance, surface, nullptr); surface = VK_NULL_HANDLE; } @@ -83,7 +92,7 @@ void VkContext::shutdown() { if (instance) { vkDestroyInstance(instance, nullptr); instance = VK_NULL_HANDLE; } - LOG_INFO("Vulkan context shutdown"); + LOG_WARNING("Vulkan context shutdown complete"); } bool VkContext::createInstance(SDL_Window* window) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 3298680c..ea6c5c15 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -7764,57 +7764,132 @@ void GameScreen::renderBankWindow(game::GameHandler& gameHandler) { } auto& inv = gameHandler.getInventory(); + bool isHolding = inventoryScreen.isHoldingItem(); + constexpr float SLOT_SIZE = 42.0f; + static constexpr float kBankPickupHold = 0.10f; // seconds + // Persistent pickup tracking for bank (mirrors inventory_screen's pickupPending_) + static bool bankPickupPending = false; + static float bankPickupPressTime = 0.0f; + static int bankPickupType = 0; // 0=main bank, 1=bank bag slot, 2=bank bag equip slot + static int bankPickupIndex = -1; + static int bankPickupBagIndex = -1; + static int bankPickupBagSlotIndex = -1; + + // Helper: render a bank item slot with icon, click-and-hold pickup, drop, tooltip + auto renderBankItemSlot = [&](const game::ItemSlot& slot, int pickType, int mainIdx, + int bagIdx, int bagSlotIdx, uint8_t dstBag, uint8_t dstSlot) { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 pos = ImGui::GetCursorScreenPos(); + + if (slot.empty()) { + ImU32 bgCol = IM_COL32(30, 30, 30, 200); + ImU32 borderCol = IM_COL32(60, 60, 60, 200); + if (isHolding) { + bgCol = IM_COL32(20, 50, 20, 200); + borderCol = IM_COL32(0, 180, 0, 200); + } + drawList->AddRectFilled(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE), bgCol); + drawList->AddRect(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE), borderCol); + ImGui::InvisibleButton("slot", ImVec2(SLOT_SIZE, SLOT_SIZE)); + if (isHolding && ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + inventoryScreen.dropIntoBankSlot(gameHandler, dstBag, dstSlot); + } + } else { + const auto& item = slot.item; + ImVec4 qc = InventoryScreen::getQualityColor(item.quality); + ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qc); + VkDescriptorSet iconTex = inventoryScreen.getItemIcon(item.displayInfoId); + + if (iconTex) { + drawList->AddImage((ImTextureID)(uintptr_t)iconTex, pos, + ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE)); + drawList->AddRect(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE), + borderCol, 0.0f, 0, 2.0f); + } else { + ImU32 bgCol = IM_COL32(40, 35, 30, 220); + drawList->AddRectFilled(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE), bgCol); + drawList->AddRect(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE), + borderCol, 0.0f, 0, 2.0f); + if (!item.name.empty()) { + char abbr[3] = { item.name[0], item.name.size() > 1 ? item.name[1] : '\0', '\0' }; + float tw = ImGui::CalcTextSize(abbr).x; + drawList->AddText(ImVec2(pos.x + (SLOT_SIZE - tw) * 0.5f, pos.y + 2.0f), + ImGui::ColorConvertFloat4ToU32(qc), abbr); + } + } + + if (item.stackCount > 1) { + char countStr[16]; + snprintf(countStr, sizeof(countStr), "%u", item.stackCount); + float cw = ImGui::CalcTextSize(countStr).x; + drawList->AddText(ImVec2(pos.x + SLOT_SIZE - cw - 2.0f, pos.y + SLOT_SIZE - 14.0f), + IM_COL32(255, 255, 255, 220), countStr); + } + + ImGui::InvisibleButton("slot", ImVec2(SLOT_SIZE, SLOT_SIZE)); + + if (!isHolding) { + // Start pickup tracking on mouse press + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + bankPickupPending = true; + bankPickupPressTime = ImGui::GetTime(); + bankPickupType = pickType; + bankPickupIndex = mainIdx; + bankPickupBagIndex = bagIdx; + bankPickupBagSlotIndex = bagSlotIdx; + } + // Check if held long enough to pick up + if (bankPickupPending && ImGui::IsMouseDown(ImGuiMouseButton_Left) && + (ImGui::GetTime() - bankPickupPressTime) >= kBankPickupHold) { + bool sameSlot = (bankPickupType == pickType); + if (pickType == 0) + sameSlot = sameSlot && (bankPickupIndex == mainIdx); + else if (pickType == 1) + sameSlot = sameSlot && (bankPickupBagIndex == bagIdx) && (bankPickupBagSlotIndex == bagSlotIdx); + else if (pickType == 2) + sameSlot = sameSlot && (bankPickupIndex == mainIdx); + + if (sameSlot && ImGui::IsItemHovered()) { + bankPickupPending = false; + if (pickType == 0) { + inventoryScreen.pickupFromBank(inv, mainIdx); + } else if (pickType == 1) { + inventoryScreen.pickupFromBankBag(inv, bagIdx, bagSlotIdx); + } else if (pickType == 2) { + inventoryScreen.pickupFromBankBagEquip(inv, mainIdx); + } + } + } + } else { + // Drop/swap on mouse release + if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + inventoryScreen.dropIntoBankSlot(gameHandler, dstBag, dstSlot); + } + } + + // Tooltip + if (ImGui::IsItemHovered() && !isHolding) { + ImGui::BeginTooltip(); + ImGui::TextColored(qc, "%s", item.name.c_str()); + if (item.stackCount > 1) ImGui::Text("Count: %u", item.stackCount); + ImGui::EndTooltip(); + } + } + }; // Main bank slots (24 for Classic, 28 for TBC/WotLK) int bankSlotCount = gameHandler.getEffectiveBankSlots(); int bankBagCount = gameHandler.getEffectiveBankBagSlots(); ImGui::Text("Bank Slots"); ImGui::Separator(); - bool isHolding = inventoryScreen.isHoldingItem(); for (int i = 0; i < bankSlotCount; i++) { if (i % 7 != 0) ImGui::SameLine(); - const auto& slot = inv.getBankSlot(i); - ImGui::PushID(i + 1000); - if (slot.empty()) { - // Highlight as drop target when holding an item - if (isHolding) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.08f, 0.20f, 0.08f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.0f, 0.35f, 0.0f, 0.9f)); - } - ImGui::Button("##bank", ImVec2(42, 42)); - if (isHolding) ImGui::PopStyleColor(2); - if (ImGui::IsItemHovered() && isHolding && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - // Drop held item into empty bank slot - inventoryScreen.dropIntoBankSlot(gameHandler, 0xFF, static_cast(39 + i)); - } - } else { - ImVec4 qc = InventoryScreen::getQualityColor(slot.item.quality); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qc.x * 0.3f, qc.y * 0.3f, qc.z * 0.3f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qc.x * 0.5f, qc.y * 0.5f, qc.z * 0.5f, 0.9f)); - - std::string label = std::to_string(slot.item.stackCount > 1 ? slot.item.stackCount : 0); - if (slot.item.stackCount <= 1) label = "##b" + std::to_string(i); - ImGui::Button(label.c_str(), ImVec2(42, 42)); - ImGui::PopStyleColor(2); - if (isHolding && ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - // Drop held item into occupied bank slot (swap) - inventoryScreen.dropIntoBankSlot(gameHandler, 0xFF, static_cast(39 + i)); - } else if (!isHolding && ImGui::IsItemClicked(ImGuiMouseButton_Left)) { - // Withdraw on click - gameHandler.withdrawItem(0xFF, static_cast(39 + i)); - } - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextColored(qc, "%s", slot.item.name.c_str()); - if (slot.item.stackCount > 1) ImGui::Text("Count: %u", slot.item.stackCount); - ImGui::EndTooltip(); - } - } + renderBankItemSlot(inv.getBankSlot(i), 0, i, -1, -1, 0xFF, static_cast(39 + i)); ImGui::PopID(); } - // Bank bag slots + // Bank bag equip slots — show bag icon with pickup/drop, or "Buy Slot" ImGui::Spacing(); ImGui::Separator(); ImGui::Text("Bank Bags"); @@ -7824,12 +7899,12 @@ void GameScreen::renderBankWindow(game::GameHandler& gameHandler) { ImGui::PushID(i + 2000); int bagSize = inv.getBankBagSize(i); - if (i < static_cast(purchased) || bagSize > 0) { - if (ImGui::Button(bagSize > 0 ? std::to_string(bagSize).c_str() : "Empty", ImVec2(50, 30))) { - // Could open bag contents - } + if (i < purchased || bagSize > 0) { + const auto& bagSlot = inv.getBankBagItem(i); + // Render as an item slot: icon with pickup/drop (pickType=2 for bag equip) + renderBankItemSlot(bagSlot, 2, i, -1, -1, 0xFF, static_cast(67 + i)); } else { - if (ImGui::Button("Buy", ImVec2(50, 30))) { + if (ImGui::Button("Buy Slot", ImVec2(50, 30))) { gameHandler.buyBankSlot(); } } @@ -7845,37 +7920,9 @@ void GameScreen::renderBankWindow(game::GameHandler& gameHandler) { ImGui::Text("Bank Bag %d (%d slots)", bagIdx + 1, bagSize); for (int s = 0; s < bagSize; s++) { if (s % 7 != 0) ImGui::SameLine(); - const auto& slot = inv.getBankBagSlot(bagIdx, s); ImGui::PushID(3000 + bagIdx * 100 + s); - if (slot.empty()) { - if (isHolding) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.08f, 0.20f, 0.08f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.0f, 0.35f, 0.0f, 0.9f)); - } - ImGui::Button("##bb", ImVec2(42, 42)); - if (isHolding) ImGui::PopStyleColor(2); - if (isHolding && ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - inventoryScreen.dropIntoBankSlot(gameHandler, static_cast(67 + bagIdx), static_cast(s)); - } - } else { - ImVec4 qc = InventoryScreen::getQualityColor(slot.item.quality); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qc.x * 0.3f, qc.y * 0.3f, qc.z * 0.3f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qc.x * 0.5f, qc.y * 0.5f, qc.z * 0.5f, 0.9f)); - std::string lbl = slot.item.stackCount > 1 ? std::to_string(slot.item.stackCount) : ("##bb" + std::to_string(bagIdx * 100 + s)); - ImGui::Button(lbl.c_str(), ImVec2(42, 42)); - ImGui::PopStyleColor(2); - if (isHolding && ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - inventoryScreen.dropIntoBankSlot(gameHandler, static_cast(67 + bagIdx), static_cast(s)); - } else if (!isHolding && ImGui::IsItemClicked(ImGuiMouseButton_Left)) { - gameHandler.withdrawItem(static_cast(67 + bagIdx), static_cast(s)); - } - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextColored(qc, "%s", slot.item.name.c_str()); - if (slot.item.stackCount > 1) ImGui::Text("Count: %u", slot.item.stackCount); - ImGui::EndTooltip(); - } - } + renderBankItemSlot(inv.getBankBagSlot(bagIdx, s), 1, -1, bagIdx, s, + static_cast(67 + bagIdx), static_cast(s)); ImGui::PopID(); } } diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index ad3c3c2c..ee4b54a3 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -304,6 +304,57 @@ void InventoryScreen::pickupFromEquipment(game::Inventory& inv, game::EquipSlot inventoryDirty = true; } +void InventoryScreen::pickupFromBank(game::Inventory& inv, int bankIndex) { + const auto& slot = inv.getBankSlot(bankIndex); + if (slot.empty()) return; + holdingItem = true; + heldItem = slot.item; + heldSource = HeldSource::BANK; + heldBankIndex = bankIndex; + heldBackpackIndex = -1; + heldBagIndex = -1; + heldBagSlotIndex = -1; + heldBankBagIndex = -1; + heldBankBagSlotIndex = -1; + heldEquipSlot = game::EquipSlot::NUM_SLOTS; + inv.clearBankSlot(bankIndex); + inventoryDirty = true; +} + +void InventoryScreen::pickupFromBankBag(game::Inventory& inv, int bagIndex, int slotIndex) { + const auto& slot = inv.getBankBagSlot(bagIndex, slotIndex); + if (slot.empty()) return; + holdingItem = true; + heldItem = slot.item; + heldSource = HeldSource::BANK_BAG; + heldBankBagIndex = bagIndex; + heldBankBagSlotIndex = slotIndex; + heldBankIndex = -1; + heldBackpackIndex = -1; + heldBagIndex = -1; + heldBagSlotIndex = -1; + heldEquipSlot = game::EquipSlot::NUM_SLOTS; + inv.clearBankBagSlot(bagIndex, slotIndex); + inventoryDirty = true; +} + +void InventoryScreen::pickupFromBankBagEquip(game::Inventory& inv, int bagIndex) { + const auto& slot = inv.getBankBagItem(bagIndex); + if (slot.empty()) return; + holdingItem = true; + heldItem = slot.item; + heldSource = HeldSource::BANK_BAG_EQUIP; + heldBankBagIndex = bagIndex; + heldBankBagSlotIndex = -1; + heldBankIndex = -1; + heldBackpackIndex = -1; + heldBagIndex = -1; + heldBagSlotIndex = -1; + heldEquipSlot = game::EquipSlot::NUM_SLOTS; + inv.setBankBagItem(bagIndex, game::ItemDef{}); + inventoryDirty = true; +} + void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) { if (!holdingItem) return; if (gameHandler_) { @@ -319,6 +370,13 @@ void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) { srcSlot = static_cast(heldBagSlotIndex); } else if (heldSource == HeldSource::EQUIPMENT) { srcSlot = static_cast(heldEquipSlot); + } else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) { + srcSlot = static_cast(39 + heldBankIndex); + } else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) { + srcBag = static_cast(67 + heldBankBagIndex); + srcSlot = static_cast(heldBankBagSlotIndex); + } else if (heldSource == HeldSource::BANK_BAG_EQUIP && heldBankBagIndex >= 0) { + srcSlot = static_cast(67 + heldBankBagIndex); } else { cancelPickup(inv); return; @@ -357,6 +415,13 @@ void InventoryScreen::placeInBag(game::Inventory& inv, int bagIndex, int slotInd srcSlot = static_cast(heldBagSlotIndex); } else if (heldSource == HeldSource::EQUIPMENT) { srcSlot = static_cast(heldEquipSlot); + } else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) { + srcSlot = static_cast(39 + heldBankIndex); + } else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) { + srcBag = static_cast(67 + heldBankBagIndex); + srcSlot = static_cast(heldBankBagSlotIndex); + } else if (heldSource == HeldSource::BANK_BAG_EQUIP && heldBankBagIndex >= 0) { + srcSlot = static_cast(67 + heldBankBagIndex); } else { cancelPickup(inv); return; @@ -417,6 +482,11 @@ void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slo srcSlot = static_cast(heldBagSlotIndex); } else if (heldSource == HeldSource::EQUIPMENT && heldEquipSlot != game::EquipSlot::NUM_SLOTS) { srcSlot = static_cast(heldEquipSlot); + } else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) { + srcSlot = static_cast(39 + heldBankIndex); + } else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) { + srcBag = static_cast(67 + heldBankBagIndex); + srcSlot = static_cast(heldBankBagSlotIndex); } else { cancelPickup(inv); return; @@ -486,6 +556,24 @@ void InventoryScreen::cancelPickup(game::Inventory& inv) { } else { inv.addItem(heldItem); } + } else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) { + if (inv.getBankSlot(heldBankIndex).empty()) { + inv.setBankSlot(heldBankIndex, heldItem); + } else { + inv.addItem(heldItem); + } + } else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0 && heldBankBagSlotIndex >= 0) { + if (inv.getBankBagSlot(heldBankBagIndex, heldBankBagSlotIndex).empty()) { + inv.setBankBagSlot(heldBankBagIndex, heldBankBagSlotIndex, heldItem); + } else { + inv.addItem(heldItem); + } + } else if (heldSource == HeldSource::BANK_BAG_EQUIP && heldBankBagIndex >= 0) { + if (inv.getBankBagItem(heldBankBagIndex).empty()) { + inv.setBankBagItem(heldBankBagIndex, heldItem); + } else { + inv.addItem(heldItem); + } } else { inv.addItem(heldItem); } @@ -554,9 +642,22 @@ void InventoryScreen::dropIntoBankSlot(game::GameHandler& /*gh*/, uint8_t dstBag srcSlot = static_cast(heldBagSlotIndex); } else if (heldSource == HeldSource::EQUIPMENT) { srcSlot = static_cast(heldEquipSlot); + } else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) { + srcSlot = static_cast(39 + heldBankIndex); + } else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) { + srcBag = static_cast(67 + heldBankBagIndex); + srcSlot = static_cast(heldBankBagSlotIndex); + } else if (heldSource == HeldSource::BANK_BAG_EQUIP && heldBankBagIndex >= 0) { + srcSlot = static_cast(67 + heldBankBagIndex); } else { return; } + // Same source and dest — just cancel pickup (restore item locally). + // Server ignores same-slot swaps so no rebuild would run, losing the item data. + if (srcBag == dstBag && srcSlot == dstSlot) { + cancelPickup(gameHandler_->getInventory()); + return; + } gameHandler_->swapContainerItems(srcBag, srcSlot, dstBag, dstSlot); holdingItem = false; inventoryDirty = true;