Fix shutdown hangs, bank bag icons/drag-drop, loading screen progress, and login spawn

- Fix shutdown hang: skip vmaDestroyAllocator (walked thousands of allocations),
  replace unsafe pthread_timedjoin_np with plain join + early-exit checks in workers
- Bank window: full icon rendering, click-and-hold pickup (0.10s), drag-drop for
  all bank slots including bank bag equip slots, same-slot drop detection
- Loading screen: process one tile per frame for live progress updates
- Camera reset: trust server position in online mode to avoid spawning under WMOs
- Fix PLAYER_BYTES/PLAYER_BYTES_2 field indices, preserve purchasedBankBagSlots
  across inventory rebuilds, fix bank slot purchase result codes
This commit is contained in:
Kelsi 2026-02-26 13:38:29 -08:00
parent 804b947203
commit a559d5944b
14 changed files with 489 additions and 146 deletions

View file

@ -18,8 +18,8 @@
"UNIT_FIELD_RESISTANCES": 99, "UNIT_FIELD_RESISTANCES": 99,
"UNIT_END": 148, "UNIT_END": 148,
"PLAYER_FLAGS": 150, "PLAYER_FLAGS": 150,
"PLAYER_BYTES": 151, "PLAYER_BYTES": 153,
"PLAYER_BYTES_2": 152, "PLAYER_BYTES_2": 154,
"PLAYER_XP": 634, "PLAYER_XP": 634,
"PLAYER_NEXT_LEVEL_XP": 635, "PLAYER_NEXT_LEVEL_XP": 635,
"PLAYER_FIELD_COINAGE": 1170, "PLAYER_FIELD_COINAGE": 1170,

View file

@ -89,8 +89,11 @@ public:
const ItemSlot& getBankBagSlot(int bagIndex, int slotIndex) const; const ItemSlot& getBankBagSlot(int bagIndex, int slotIndex) const;
bool setBankBagSlot(int bagIndex, int slotIndex, const ItemDef& item); bool setBankBagSlot(int bagIndex, int slotIndex, const ItemDef& item);
bool clearBankBagSlot(int bagIndex, int slotIndex);
int getBankBagSize(int bagIndex) const; int getBankBagSize(int bagIndex) const;
void setBankBagSize(int bagIndex, int size); 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_; } uint8_t getPurchasedBankBagSlots() const { return purchasedBankBagSlots_; }
void setPurchasedBankBagSlots(uint8_t count) { purchasedBankBagSlots_ = count; } void setPurchasedBankBagSlots(uint8_t count) { purchasedBankBagSlots_ = count; }
@ -111,6 +114,7 @@ private:
struct BagData { struct BagData {
int size = 0; int size = 0;
ItemSlot bagItem; // The bag item itself (for icon/name/tooltip)
std::array<ItemSlot, MAX_BAG_SIZE> slots{}; std::array<ItemSlot, MAX_BAG_SIZE> slots{};
}; };
std::array<BagData, NUM_BAG_SLOTS> bags{}; std::array<BagData, NUM_BAG_SLOTS> bags{};

View file

@ -212,6 +212,7 @@ public:
* Unload all tiles * Unload all tiles
*/ */
void unloadAll(); void unloadAll();
void stopWorkers(); // Stop worker threads without restarting (for shutdown)
void softReset(); // Clear tile data without stopping worker threads (non-blocking) 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) */ /** Process all ready tiles immediately (use during loading screens) */
void processAllReadyTiles(); void processAllReadyTiles();
/** Process one ready tile (for loading screens with per-tile progress updates) */
void processOneReadyTile();
private: private:
/** /**
* Get tile coordinates from GL world position * Get tile coordinates from GL world position

View file

@ -118,11 +118,14 @@ private:
// Drag-and-drop held item state // Drag-and-drop held item state
bool holdingItem = false; bool holdingItem = false;
game::ItemDef heldItem; 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; HeldSource heldSource = HeldSource::NONE;
int heldBackpackIndex = -1; int heldBackpackIndex = -1;
int heldBagIndex = -1; int heldBagIndex = -1;
int heldBagSlotIndex = -1; int heldBagSlotIndex = -1;
int heldBankIndex = -1;
int heldBankBagIndex = -1;
int heldBankBagSlotIndex = -1;
game::EquipSlot heldEquipSlot = game::EquipSlot::NUM_SLOTS; game::EquipSlot heldEquipSlot = game::EquipSlot::NUM_SLOTS;
// Slot rendering with interaction support // Slot rendering with interaction support
@ -136,7 +139,7 @@ private:
int pickupBagIndex_ = -1; int pickupBagIndex_ = -1;
int pickupBagSlotIndex_ = -1; int pickupBagSlotIndex_ = -1;
game::EquipSlot pickupEquipSlot_ = game::EquipSlot::NUM_SLOTS; 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 renderSeparateBags(game::Inventory& inventory, uint64_t moneyCopper);
void renderAggregateBags(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); bool dropHeldItemToEquipSlot(game::Inventory& inv, game::EquipSlot slot);
/// Drop the currently held item into a bank slot via CMSG_SWAP_ITEM. /// Drop the currently held item into a bank slot via CMSG_SWAP_ITEM.
void dropIntoBankSlot(game::GameHandler& gh, uint8_t dstBag, uint8_t dstSlot); 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 } // namespace ui

View file

@ -421,34 +421,43 @@ void Application::run() {
} }
void Application::shutdown() { void Application::shutdown() {
LOG_INFO("Shutting down application"); LOG_WARNING("Shutting down application...");
// Save floor cache before renderer is destroyed // Save floor cache before renderer is destroyed
if (renderer && renderer->getWMORenderer()) { if (renderer && renderer->getWMORenderer()) {
size_t cacheSize = renderer->getWMORenderer()->getFloorCacheSize(); size_t cacheSize = renderer->getWMORenderer()->getFloorCacheSize();
if (cacheSize > 0) { if (cacheSize > 0) {
LOG_INFO("Saving WMO floor cache (", cacheSize, " entries)..."); LOG_WARNING("Saving WMO floor cache (", cacheSize, " entries)...");
renderer->getWMORenderer()->saveFloorCache(); renderer->getWMORenderer()->saveFloorCache();
LOG_WARNING("Floor cache saved.");
} }
} }
// Explicitly shut down the renderer before destroying it — this ensures // Explicitly shut down the renderer before destroying it — this ensures
// all sub-renderers free their VMA allocations in the correct order, // all sub-renderers free their VMA allocations in the correct order,
// before VkContext::shutdown() calls vmaDestroyAllocator(). // before VkContext::shutdown() calls vmaDestroyAllocator().
LOG_WARNING("Shutting down renderer...");
if (renderer) { if (renderer) {
renderer->shutdown(); renderer->shutdown();
} }
LOG_WARNING("Renderer shutdown complete, resetting...");
renderer.reset(); renderer.reset();
LOG_WARNING("Resetting world...");
world.reset(); world.reset();
LOG_WARNING("Resetting gameHandler...");
gameHandler.reset(); gameHandler.reset();
LOG_WARNING("Resetting authHandler...");
authHandler.reset(); authHandler.reset();
LOG_WARNING("Resetting assetManager...");
assetManager.reset(); assetManager.reset();
LOG_WARNING("Resetting uiManager...");
uiManager.reset(); uiManager.reset();
LOG_WARNING("Resetting window...");
window.reset(); window.reset();
running = false; running = false;
LOG_INFO("Application shutdown complete"); LOG_WARNING("Application shutdown complete");
} }
void Application::setState(AppState newState) { 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 startTime = std::chrono::high_resolution_clock::now();
auto lastProgressTime = startTime; auto lastProgressTime = startTime;
const float maxWaitSeconds = 20.0f; const float maxWaitSeconds = 60.0f;
const float stallSeconds = 5.0f; const float stallSeconds = 10.0f;
int initialRemaining = terrainMgr->getRemainingTileCount(); int initialRemaining = terrainMgr->getRemainingTileCount();
if (initialRemaining < 1) initialRemaining = 1; if (initialRemaining < 1) initialRemaining = 1;
int lastRemaining = initialRemaining; 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->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<float>(loaded) / static_cast<float>(total);
float progress = 0.35f + tileProgress * 0.50f;
auto now = std::chrono::high_resolution_clock::now();
float elapsedSec = std::chrono::duration<float>(now - startTime).count();
char buf[192];
if (loaded > 0 && remaining > 0) {
float tilesPerSec = static_cast<float>(loaded) / std::max(elapsedSec, 0.1f);
float etaSec = static_cast<float>(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) { if (loadingScreenOk) {
int remaining = terrainMgr->getRemainingTileCount();
int loaded = terrainMgr->getLoadedTileCount();
float tileProgress = static_cast<float>(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.setStatus(buf);
loadingScreen.setProgress(progress); loadingScreen.setProgress(progress);
loadingScreen.render(); loadingScreen.render();
window->swapBuffers(); window->swapBuffers();
}
if (remaining != lastRemaining) { if (remaining != lastRemaining) {
lastRemaining = remaining; lastRemaining = remaining;
lastProgressTime = std::chrono::high_resolution_clock::now(); lastProgressTime = now;
}
} }
auto elapsed = std::chrono::high_resolution_clock::now() - startTime; 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; 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"); 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 // Load/precompute collision cache
if (renderer->getWMORenderer()) { if (renderer->getWMORenderer()) {
showProgress("Building collision cache...", 0.88f); showProgress("Building collision cache...", 0.88f);
if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); }
renderer->getWMORenderer()->loadFloorCache(); renderer->getWMORenderer()->loadFloorCache();
if (renderer->getWMORenderer()->getFloorCacheSize() == 0) { if (renderer->getWMORenderer()->getFloorCacheSize() == 0) {
showProgress("Computing walkable surfaces...", 0.90f);
if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); }
renderer->getWMORenderer()->precomputeFloorCache(); renderer->getWMORenderer()->precomputeFloorCache();
} }
} }

View file

@ -94,19 +94,22 @@ bool Window::initialize() {
} }
void Window::shutdown() { void Window::shutdown() {
LOG_WARNING("Window::shutdown - vkContext...");
if (vkContext) { if (vkContext) {
vkContext->shutdown(); vkContext->shutdown();
vkContext.reset(); vkContext.reset();
} }
LOG_WARNING("Window::shutdown - SDL_DestroyWindow...");
if (window) { if (window) {
SDL_DestroyWindow(window); SDL_DestroyWindow(window);
window = nullptr; window = nullptr;
} }
LOG_WARNING("Window::shutdown - SDL_Quit...");
SDL_Vulkan_UnloadLibrary(); SDL_Vulkan_UnloadLibrary();
SDL_Quit(); SDL_Quit();
LOG_INFO("Window shutdown complete"); LOG_WARNING("Window shutdown complete");
} }
void Window::pollEvents() { void Window::pollEvents() {

View file

@ -5233,6 +5233,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
} }
else if (ufPBytes2 != 0xFFFF && key == ufPBytes2) { else if (ufPBytes2 != 0xFFFF && key == ufPBytes2) {
uint8_t bankBagSlots = static_cast<uint8_t>((val >> 16) & 0xFF); uint8_t bankBagSlots = static_cast<uint8_t>((val >> 16) & 0xFF);
LOG_WARNING("PLAYER_BYTES_2 (CREATE): raw=0x", std::hex, val, std::dec,
" bankBagSlots=", static_cast<int>(bankBagSlots));
inventory.setPurchasedBankBagSlots(bankBagSlots); inventory.setPurchasedBankBagSlots(bankBagSlots);
} }
// Do not synthesize quest-log entries from raw update-field slots. // 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) { else if (ufPBytes2v != 0xFFFF && key == ufPBytes2v) {
uint8_t bankBagSlots = static_cast<uint8_t>((val >> 16) & 0xFF); uint8_t bankBagSlots = static_cast<uint8_t>((val >> 16) & 0xFF);
LOG_WARNING("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec,
" bankBagSlots=", static_cast<int>(bankBagSlots));
inventory.setPurchasedBankBagSlots(bankBagSlots); inventory.setPurchasedBankBagSlots(bankBagSlots);
} }
else if (key == ufPlayerFlags) { else if (key == ufPlayerFlags) {
@ -7707,7 +7711,9 @@ void GameHandler::extractContainerFields(uint64_t containerGuid, const std::map<
void GameHandler::rebuildOnlineInventory() { void GameHandler::rebuildOnlineInventory() {
uint8_t savedBankBagSlots = inventory.getPurchasedBankBagSlots();
inventory = Inventory(); inventory = Inventory();
inventory.setPurchasedBankBagSlots(savedBankBagSlots);
// Equipment slots // Equipment slots
for (int i = 0; i < 23; i++) { for (int i = 0; i < 23; i++) {
@ -7910,14 +7916,31 @@ void GameHandler::rebuildOnlineInventory() {
if (contIt != containerContents_.end()) { if (contIt != containerContents_.end()) {
numSlots = static_cast<int>(contIt->second.numSlots); numSlots = static_cast<int>(contIt->second.numSlots);
} }
if (numSlots <= 0) {
auto bagItemIt = onlineItems_.find(bagGuid); // Populate the bag item itself (for icon/name in the bank bag equip slot)
if (bagItemIt != onlineItems_.end()) { auto bagItemIt = onlineItems_.find(bagGuid);
if (bagItemIt != onlineItems_.end()) {
if (numSlots <= 0) {
auto bagInfoIt = itemInfoCache_.find(bagItemIt->second.entry); auto bagInfoIt = itemInfoCache_.find(bagItemIt->second.entry);
if (bagInfoIt != itemInfoCache_.end()) { if (bagInfoIt != itemInfoCache_.end()) {
numSlots = bagInfoIt->second.containerSlots; 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<ItemQuality>(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; if (numSlots <= 0) continue;
@ -13673,7 +13696,12 @@ void GameHandler::closeBank() {
} }
void GameHandler::buyBankSlot() { 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<int>(inventory.getPurchasedBankBagSlots()));
auto pkt = BuyBankSlotPacket::build(bankerGuid_); auto pkt = BuyBankSlotPacket::build(bankerGuid_);
socket->send(pkt); socket->send(pkt);
} }
@ -13698,17 +13726,33 @@ void GameHandler::handleShowBank(network::Packet& packet) {
// Bank items are already tracked via update fields (bank slot GUIDs) // Bank items are already tracked via update fields (bank slot GUIDs)
// Trigger rebuild to populate bank slots in inventory // Trigger rebuild to populate bank slots in inventory
rebuildOnlineInventory(); 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<int>(inventory.getPurchasedBankBagSlots()),
" filledBags=", filledBags,
" effectiveBankBagSlots=", effectiveBankBagSlots_);
} }
void GameHandler::handleBuyBankSlotResult(network::Packet& packet) { void GameHandler::handleBuyBankSlotResult(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 4) return; if (packet.getSize() - packet.getReadPos() < 4) return;
uint32_t result = packet.readUInt32(); 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."); addSystemChatMessage("Bank slot purchased.");
inventory.setPurchasedBankBagSlots(inventory.getPurchasedBankBagSlots() + 1); 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 { } else {
addSystemChatMessage("Cannot purchase bank slot."); addSystemChatMessage("Cannot purchase bank slot (error " + std::to_string(result) + ").");
} }
} }

View file

@ -105,6 +105,13 @@ bool Inventory::setBankBagSlot(int bagIndex, int slotIndex, const ItemDef& item)
return true; 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 { int Inventory::getBankBagSize(int bagIndex) const {
if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return 0; if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return 0;
return bankBags_[bagIndex].size; return bankBags_[bagIndex].size;
@ -115,6 +122,17 @@ void Inventory::setBankBagSize(int bagIndex, int size) {
bankBags_[bagIndex].size = std::min(size, MAX_BAG_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) { void Inventory::swapBagContents(int bagA, int bagB) {
if (bagA < 0 || bagA >= NUM_BAG_SLOTS || bagB < 0 || bagB >= NUM_BAG_SLOTS) return; if (bagA < 0 || bagA >= NUM_BAG_SLOTS || bagB < 0 || bagB >= NUM_BAG_SLOTS) return;
if (bagA == bagB) return; if (bagA == bagB) return;

View file

@ -1756,15 +1756,47 @@ void CameraController::reset() {
return h; 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. // 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<float>::max(); float bestScore = std::numeric_limits<float>::max();
glm::vec3 bestPos = spawnPos; glm::vec3 bestPos = spawnPos;
bool foundBest = false; bool foundBest = false;
constexpr float radiiOffline[] = {0.0f, 6.0f, 12.0f, 18.0f, 24.0f, 32.0f}; 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 = radiiOffline;
const float* radii = onlineMode ? radiiOnline : radiiOffline; const int radiiCount = 6;
const int radiiCount = onlineMode ? 2 : 6;
constexpr int ANGLES = 16; constexpr int ANGLES = 16;
constexpr float PI = 3.14159265f; constexpr float PI = 3.14159265f;
for (int ri = 0; ri < radiiCount; ri++) { for (int ri = 0; ri < radiiCount; ri++) {

View file

@ -726,31 +726,38 @@ bool Renderer::initialize(core::Window* win) {
} }
void Renderer::shutdown() { void Renderer::shutdown() {
LOG_WARNING("Renderer::shutdown - terrainManager stopWorkers...");
if (terrainManager) { if (terrainManager) {
terrainManager->unloadAll(); terrainManager->stopWorkers();
LOG_WARNING("Renderer::shutdown - terrainManager reset...");
terrainManager.reset(); terrainManager.reset();
} }
LOG_WARNING("Renderer::shutdown - terrainRenderer...");
if (terrainRenderer) { if (terrainRenderer) {
terrainRenderer->shutdown(); terrainRenderer->shutdown();
terrainRenderer.reset(); terrainRenderer.reset();
} }
LOG_WARNING("Renderer::shutdown - waterRenderer...");
if (waterRenderer) { if (waterRenderer) {
waterRenderer->shutdown(); waterRenderer->shutdown();
waterRenderer.reset(); waterRenderer.reset();
} }
LOG_WARNING("Renderer::shutdown - minimap...");
if (minimap) { if (minimap) {
minimap->shutdown(); minimap->shutdown();
minimap.reset(); minimap.reset();
} }
LOG_WARNING("Renderer::shutdown - worldMap...");
if (worldMap) { if (worldMap) {
worldMap->shutdown(); worldMap->shutdown();
worldMap.reset(); worldMap.reset();
} }
LOG_WARNING("Renderer::shutdown - skySystem...");
if (skySystem) { if (skySystem) {
skySystem->shutdown(); skySystem->shutdown();
skySystem.reset(); skySystem.reset();
@ -772,34 +779,41 @@ void Renderer::shutdown() {
swimEffects.reset(); swimEffects.reset();
} }
LOG_WARNING("Renderer::shutdown - characterRenderer...");
if (characterRenderer) { if (characterRenderer) {
characterRenderer->shutdown(); characterRenderer->shutdown();
characterRenderer.reset(); characterRenderer.reset();
} }
LOG_WARNING("Renderer::shutdown - wmoRenderer...");
if (wmoRenderer) { if (wmoRenderer) {
wmoRenderer->shutdown(); wmoRenderer->shutdown();
wmoRenderer.reset(); wmoRenderer.reset();
} }
LOG_WARNING("Renderer::shutdown - m2Renderer...");
if (m2Renderer) { if (m2Renderer) {
m2Renderer->shutdown(); m2Renderer->shutdown();
m2Renderer.reset(); m2Renderer.reset();
} }
LOG_WARNING("Renderer::shutdown - musicManager...");
if (musicManager) { if (musicManager) {
musicManager->shutdown(); musicManager->shutdown();
musicManager.reset(); musicManager.reset();
} }
LOG_WARNING("Renderer::shutdown - footstepManager...");
if (footstepManager) { if (footstepManager) {
footstepManager->shutdown(); footstepManager->shutdown();
footstepManager.reset(); footstepManager.reset();
} }
LOG_WARNING("Renderer::shutdown - activitySoundManager...");
if (activitySoundManager) { if (activitySoundManager) {
activitySoundManager->shutdown(); activitySoundManager->shutdown();
activitySoundManager.reset(); activitySoundManager.reset();
} }
LOG_WARNING("Renderer::shutdown - AudioEngine...");
// Shutdown AudioEngine singleton // Shutdown AudioEngine singleton
audio::AudioEngine::instance().shutdown(); audio::AudioEngine::instance().shutdown();

View file

@ -129,17 +129,7 @@ TerrainManager::TerrainManager() {
} }
TerrainManager::~TerrainManager() { TerrainManager::~TerrainManager() {
// Stop worker thread before cleanup (containers clean up via destructors) stopWorkers();
if (workerRunning.load()) {
workerRunning.store(false);
queueCV.notify_all();
for (auto& t : workerThreads) {
if (t.joinable()) {
t.join();
}
}
workerThreads.clear();
}
} }
bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* renderer) { bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* renderer) {
@ -276,6 +266,9 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
LOG_DEBUG("Preparing tile [", x, ",", y, "] (CPU work)"); 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 // Load ADT file
std::string adtPath = getADTPath(coord); std::string adtPath = getADTPath(coord);
auto adtData = assetManager->readFile(adtPath); auto adtData = assetManager->readFile(adtPath);
@ -294,6 +287,8 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
return nullptr; return nullptr;
} }
if (!workerRunning.load()) return nullptr;
// WotLK split ADTs can store placements in *_obj0.adt. // WotLK split ADTs can store placements in *_obj0.adt.
// Merge object chunks so doodads/WMOs (including ground clutter) are available. // Merge object chunks so doodads/WMOs (including ground clutter) are available.
std::string objPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" + std::string objPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
@ -362,6 +357,8 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
return nullptr; return nullptr;
} }
if (!workerRunning.load()) return nullptr;
auto pending = std::make_shared<PendingTile>(); auto pending = std::make_shared<PendingTile>();
pending->coord = coord; pending->coord = coord;
pending->terrain = std::move(*terrainPtr); pending->terrain = std::move(*terrainPtr);
@ -412,6 +409,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
// Pre-load M2 doodads (CPU: read files, parse models) // Pre-load M2 doodads (CPU: read files, parse models)
int skippedNameId = 0, skippedFileNotFound = 0, skippedInvalid = 0, skippedSkinNotFound = 0; int skippedNameId = 0, skippedFileNotFound = 0, skippedInvalid = 0, skippedSkinNotFound = 0;
for (const auto& placement : pending->terrain.doodadPlacements) { for (const auto& placement : pending->terrain.doodadPlacements) {
if (!workerRunning.load()) return nullptr;
if (placement.nameId >= pending->terrain.doodadNames.size()) { if (placement.nameId >= pending->terrain.doodadNames.size()) {
skippedNameId++; skippedNameId++;
continue; continue;
@ -460,9 +458,12 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
ensureGroundEffectTablesLoaded(); ensureGroundEffectTablesLoaded();
generateGroundClutterPlacements(pending, preparedModelIds); generateGroundClutterPlacements(pending, preparedModelIds);
if (!workerRunning.load()) return nullptr;
// Pre-load WMOs (CPU: read files, parse models and groups) // Pre-load WMOs (CPU: read files, parse models and groups)
if (!pending->terrain.wmoPlacements.empty()) { if (!pending->terrain.wmoPlacements.empty()) {
for (const auto& placement : pending->terrain.wmoPlacements) { for (const auto& placement : pending->terrain.wmoPlacements) {
if (!workerRunning.load()) return nullptr;
if (placement.nameId >= pending->terrain.wmoNames.size()) continue; if (placement.nameId >= pending->terrain.wmoNames.size()) continue;
const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId]; const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId];
@ -513,6 +514,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
); );
// Pre-load WMO doodads (M2 models inside WMO) // Pre-load WMO doodads (M2 models inside WMO)
if (!workerRunning.load()) return nullptr;
if (!wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) { if (!wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) {
glm::mat4 wmoMatrix(1.0f); glm::mat4 wmoMatrix(1.0f);
wmoMatrix = glm::translate(wmoMatrix, pos); wmoMatrix = glm::translate(wmoMatrix, pos);
@ -636,6 +638,8 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
} }
} }
if (!workerRunning.load()) return nullptr;
// Pre-load terrain texture BLP data on background thread so finalizeTile // Pre-load terrain texture BLP data on background thread so finalizeTile
// doesn't block the main thread with file I/O. // doesn't block the main thread with file I/O.
for (const auto& texPath : pending->terrain.textures) { 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<std::mutex> 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<PendingTile> TerrainManager::getCachedTile(const TileCoord& coord) { std::shared_ptr<PendingTile> TerrainManager::getCachedTile(const TileCoord& coord) {
std::lock_guard<std::mutex> lock(tileCacheMutex_); std::lock_guard<std::mutex> lock(tileCacheMutex_);
auto it = tileCache_.find(coord); auto it = tileCache_.find(coord);
@ -1237,6 +1263,29 @@ void TerrainManager::unloadTile(int x, int y) {
loadedTiles.erase(it); 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() { void TerrainManager::unloadAll() {
// Signal worker threads to stop and wait briefly for them to finish. // Signal worker threads to stop and wait briefly for them to finish.
// Workers may be mid-prepareTile (reading MPQ / parsing ADT) which can // Workers may be mid-prepareTile (reading MPQ / parsing ADT) which can
@ -1245,29 +1294,8 @@ void TerrainManager::unloadAll() {
workerRunning.store(false); workerRunning.store(false);
queueCV.notify_all(); queueCV.notify_all();
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
for (auto& t : workerThreads) { for (auto& t : workerThreads) {
if (!t.joinable()) continue; if (t.joinable()) t.join();
// 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();
}
} }
workerThreads.clear(); workerThreads.clear();
} }

View file

@ -50,10 +50,12 @@ bool VkContext::initialize(SDL_Window* window) {
} }
void VkContext::shutdown() { void VkContext::shutdown() {
LOG_WARNING("VkContext::shutdown - vkDeviceWaitIdle...");
if (device) { if (device) {
vkDeviceWaitIdle(device); vkDeviceWaitIdle(device);
} }
LOG_WARNING("VkContext::shutdown - destroyImGuiResources...");
destroyImGuiResources(); destroyImGuiResources();
// Destroy sync objects // Destroy sync objects
@ -68,9 +70,16 @@ void VkContext::shutdown() {
if (immFence) { vkDestroyFence(device, immFence, nullptr); immFence = VK_NULL_HANDLE; } if (immFence) { vkDestroyFence(device, immFence, nullptr); immFence = VK_NULL_HANDLE; }
if (immCommandPool) { vkDestroyCommandPool(device, immCommandPool, nullptr); immCommandPool = VK_NULL_HANDLE; } if (immCommandPool) { vkDestroyCommandPool(device, immCommandPool, nullptr); immCommandPool = VK_NULL_HANDLE; }
LOG_WARNING("VkContext::shutdown - destroySwapchain...");
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 (device) { vkDestroyDevice(device, nullptr); device = VK_NULL_HANDLE; }
if (surface) { vkDestroySurfaceKHR(instance, surface, nullptr); surface = 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; } 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) { bool VkContext::createInstance(SDL_Window* window) {

View file

@ -7764,57 +7764,132 @@ void GameScreen::renderBankWindow(game::GameHandler& gameHandler) {
} }
auto& inv = gameHandler.getInventory(); 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) // Main bank slots (24 for Classic, 28 for TBC/WotLK)
int bankSlotCount = gameHandler.getEffectiveBankSlots(); int bankSlotCount = gameHandler.getEffectiveBankSlots();
int bankBagCount = gameHandler.getEffectiveBankBagSlots(); int bankBagCount = gameHandler.getEffectiveBankBagSlots();
ImGui::Text("Bank Slots"); ImGui::Text("Bank Slots");
ImGui::Separator(); ImGui::Separator();
bool isHolding = inventoryScreen.isHoldingItem();
for (int i = 0; i < bankSlotCount; i++) { for (int i = 0; i < bankSlotCount; i++) {
if (i % 7 != 0) ImGui::SameLine(); if (i % 7 != 0) ImGui::SameLine();
const auto& slot = inv.getBankSlot(i);
ImGui::PushID(i + 1000); ImGui::PushID(i + 1000);
if (slot.empty()) { renderBankItemSlot(inv.getBankSlot(i), 0, i, -1, -1, 0xFF, static_cast<uint8_t>(39 + i));
// 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<uint8_t>(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<uint8_t>(39 + i));
} else if (!isHolding && ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
// Withdraw on click
gameHandler.withdrawItem(0xFF, static_cast<uint8_t>(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();
}
}
ImGui::PopID(); ImGui::PopID();
} }
// Bank bag slots // Bank bag equip slots — show bag icon with pickup/drop, or "Buy Slot"
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
ImGui::Text("Bank Bags"); ImGui::Text("Bank Bags");
@ -7824,12 +7899,12 @@ void GameScreen::renderBankWindow(game::GameHandler& gameHandler) {
ImGui::PushID(i + 2000); ImGui::PushID(i + 2000);
int bagSize = inv.getBankBagSize(i); int bagSize = inv.getBankBagSize(i);
if (i < static_cast<int>(purchased) || bagSize > 0) { if (i < purchased || bagSize > 0) {
if (ImGui::Button(bagSize > 0 ? std::to_string(bagSize).c_str() : "Empty", ImVec2(50, 30))) { const auto& bagSlot = inv.getBankBagItem(i);
// Could open bag contents // Render as an item slot: icon with pickup/drop (pickType=2 for bag equip)
} renderBankItemSlot(bagSlot, 2, i, -1, -1, 0xFF, static_cast<uint8_t>(67 + i));
} else { } else {
if (ImGui::Button("Buy", ImVec2(50, 30))) { if (ImGui::Button("Buy Slot", ImVec2(50, 30))) {
gameHandler.buyBankSlot(); gameHandler.buyBankSlot();
} }
} }
@ -7845,37 +7920,9 @@ void GameScreen::renderBankWindow(game::GameHandler& gameHandler) {
ImGui::Text("Bank Bag %d (%d slots)", bagIdx + 1, bagSize); ImGui::Text("Bank Bag %d (%d slots)", bagIdx + 1, bagSize);
for (int s = 0; s < bagSize; s++) { for (int s = 0; s < bagSize; s++) {
if (s % 7 != 0) ImGui::SameLine(); if (s % 7 != 0) ImGui::SameLine();
const auto& slot = inv.getBankBagSlot(bagIdx, s);
ImGui::PushID(3000 + bagIdx * 100 + s); ImGui::PushID(3000 + bagIdx * 100 + s);
if (slot.empty()) { renderBankItemSlot(inv.getBankBagSlot(bagIdx, s), 1, -1, bagIdx, s,
if (isHolding) { static_cast<uint8_t>(67 + bagIdx), static_cast<uint8_t>(s));
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<uint8_t>(67 + bagIdx), static_cast<uint8_t>(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<uint8_t>(67 + bagIdx), static_cast<uint8_t>(s));
} else if (!isHolding && ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
gameHandler.withdrawItem(static_cast<uint8_t>(67 + bagIdx), static_cast<uint8_t>(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();
}
}
ImGui::PopID(); ImGui::PopID();
} }
} }

View file

@ -304,6 +304,57 @@ void InventoryScreen::pickupFromEquipment(game::Inventory& inv, game::EquipSlot
inventoryDirty = true; 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) { void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
if (!holdingItem) return; if (!holdingItem) return;
if (gameHandler_) { if (gameHandler_) {
@ -319,6 +370,13 @@ void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
srcSlot = static_cast<uint8_t>(heldBagSlotIndex); srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
} else if (heldSource == HeldSource::EQUIPMENT) { } else if (heldSource == HeldSource::EQUIPMENT) {
srcSlot = static_cast<uint8_t>(heldEquipSlot); srcSlot = static_cast<uint8_t>(heldEquipSlot);
} else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) {
srcSlot = static_cast<uint8_t>(39 + heldBankIndex);
} else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) {
srcBag = static_cast<uint8_t>(67 + heldBankBagIndex);
srcSlot = static_cast<uint8_t>(heldBankBagSlotIndex);
} else if (heldSource == HeldSource::BANK_BAG_EQUIP && heldBankBagIndex >= 0) {
srcSlot = static_cast<uint8_t>(67 + heldBankBagIndex);
} else { } else {
cancelPickup(inv); cancelPickup(inv);
return; return;
@ -357,6 +415,13 @@ void InventoryScreen::placeInBag(game::Inventory& inv, int bagIndex, int slotInd
srcSlot = static_cast<uint8_t>(heldBagSlotIndex); srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
} else if (heldSource == HeldSource::EQUIPMENT) { } else if (heldSource == HeldSource::EQUIPMENT) {
srcSlot = static_cast<uint8_t>(heldEquipSlot); srcSlot = static_cast<uint8_t>(heldEquipSlot);
} else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) {
srcSlot = static_cast<uint8_t>(39 + heldBankIndex);
} else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) {
srcBag = static_cast<uint8_t>(67 + heldBankBagIndex);
srcSlot = static_cast<uint8_t>(heldBankBagSlotIndex);
} else if (heldSource == HeldSource::BANK_BAG_EQUIP && heldBankBagIndex >= 0) {
srcSlot = static_cast<uint8_t>(67 + heldBankBagIndex);
} else { } else {
cancelPickup(inv); cancelPickup(inv);
return; return;
@ -417,6 +482,11 @@ void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slo
srcSlot = static_cast<uint8_t>(heldBagSlotIndex); srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
} else if (heldSource == HeldSource::EQUIPMENT && heldEquipSlot != game::EquipSlot::NUM_SLOTS) { } else if (heldSource == HeldSource::EQUIPMENT && heldEquipSlot != game::EquipSlot::NUM_SLOTS) {
srcSlot = static_cast<uint8_t>(heldEquipSlot); srcSlot = static_cast<uint8_t>(heldEquipSlot);
} else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) {
srcSlot = static_cast<uint8_t>(39 + heldBankIndex);
} else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) {
srcBag = static_cast<uint8_t>(67 + heldBankBagIndex);
srcSlot = static_cast<uint8_t>(heldBankBagSlotIndex);
} else { } else {
cancelPickup(inv); cancelPickup(inv);
return; return;
@ -486,6 +556,24 @@ void InventoryScreen::cancelPickup(game::Inventory& inv) {
} else { } else {
inv.addItem(heldItem); 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 { } else {
inv.addItem(heldItem); inv.addItem(heldItem);
} }
@ -554,9 +642,22 @@ void InventoryScreen::dropIntoBankSlot(game::GameHandler& /*gh*/, uint8_t dstBag
srcSlot = static_cast<uint8_t>(heldBagSlotIndex); srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
} else if (heldSource == HeldSource::EQUIPMENT) { } else if (heldSource == HeldSource::EQUIPMENT) {
srcSlot = static_cast<uint8_t>(heldEquipSlot); srcSlot = static_cast<uint8_t>(heldEquipSlot);
} else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) {
srcSlot = static_cast<uint8_t>(39 + heldBankIndex);
} else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) {
srcBag = static_cast<uint8_t>(67 + heldBankBagIndex);
srcSlot = static_cast<uint8_t>(heldBankBagSlotIndex);
} else if (heldSource == HeldSource::BANK_BAG_EQUIP && heldBankBagIndex >= 0) {
srcSlot = static_cast<uint8_t>(67 + heldBankBagIndex);
} else { } else {
return; 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); gameHandler_->swapContainerItems(srcBag, srcSlot, dstBag, dstSlot);
holdingItem = false; holdingItem = false;
inventoryDirty = true; inventoryDirty = true;