mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
Compare commits
2 commits
804b947203
...
d0e8b44866
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0e8b44866 | ||
|
|
a559d5944b |
18 changed files with 1201 additions and 217 deletions
|
|
@ -284,6 +284,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/m2_loader.cpp
|
||||
src/pipeline/wmo_loader.cpp
|
||||
src/pipeline/adt_loader.cpp
|
||||
src/pipeline/wdt_loader.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
|
@ -401,6 +402,7 @@ set(WOWEE_HEADERS
|
|||
include/pipeline/m2_loader.hpp
|
||||
include/pipeline/wmo_loader.hpp
|
||||
include/pipeline/adt_loader.hpp
|
||||
include/pipeline/wdt_loader.hpp
|
||||
include/pipeline/dbc_loader.hpp
|
||||
include/pipeline/terrain_mesh.hpp
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -321,6 +321,10 @@ public:
|
|||
// Random roll
|
||||
void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100);
|
||||
|
||||
// Battleground
|
||||
bool hasPendingBgInvite() const;
|
||||
void acceptBattlefield(uint32_t queueSlot = 0xFFFFFFFF);
|
||||
|
||||
// Logout commands
|
||||
void requestLogout();
|
||||
void cancelLogout();
|
||||
|
|
@ -1189,8 +1193,13 @@ private:
|
|||
void handleForceMoveFlagChange(network::Packet& packet, const char* name, Opcode ackOpcode, uint32_t flag, bool set);
|
||||
void handleMoveKnockBack(network::Packet& packet);
|
||||
|
||||
// ---- Area trigger detection ----
|
||||
void loadAreaTriggerDbc();
|
||||
void checkAreaTriggers();
|
||||
|
||||
// ---- Arena / Battleground handlers ----
|
||||
void handleBattlefieldStatus(network::Packet& packet);
|
||||
void handleInstanceDifficulty(network::Packet& packet);
|
||||
void handleArenaTeamCommandResult(network::Packet& packet);
|
||||
void handleArenaTeamQueryResponse(network::Packet& packet);
|
||||
void handleArenaTeamInvite(network::Packet& packet);
|
||||
|
|
@ -1477,12 +1486,40 @@ private:
|
|||
std::unordered_map<uint32_t, TalentEntry> talentCache_; // talentId -> entry
|
||||
std::unordered_map<uint32_t, TalentTabEntry> talentTabCache_; // tabId -> entry
|
||||
bool talentDbcLoaded_ = false;
|
||||
|
||||
// ---- Area trigger detection ----
|
||||
struct AreaTriggerEntry {
|
||||
uint32_t id = 0;
|
||||
uint32_t mapId = 0;
|
||||
float x = 0, y = 0, z = 0; // canonical WoW coords (converted from DBC)
|
||||
float radius = 0;
|
||||
float boxLength = 0, boxWidth = 0, boxHeight = 0;
|
||||
float boxYaw = 0;
|
||||
};
|
||||
bool areaTriggerDbcLoaded_ = false;
|
||||
std::vector<AreaTriggerEntry> areaTriggers_;
|
||||
std::unordered_set<uint32_t> activeAreaTriggers_; // triggers player is currently inside
|
||||
float areaTriggerCheckTimer_ = 0.0f;
|
||||
|
||||
float castTimeTotal = 0.0f;
|
||||
std::array<ActionBarSlot, 12> actionBar{};
|
||||
std::vector<AuraSlot> playerAuras;
|
||||
std::vector<AuraSlot> targetAuras;
|
||||
uint64_t petGuid_ = 0;
|
||||
|
||||
// ---- Battleground queue state ----
|
||||
struct BgQueueSlot {
|
||||
uint32_t queueSlot = 0;
|
||||
uint32_t bgTypeId = 0;
|
||||
uint8_t arenaType = 0;
|
||||
uint32_t statusId = 0; // 0=none, 1=wait_queue, 2=wait_join, 3=in_progress
|
||||
};
|
||||
std::array<BgQueueSlot, 3> bgQueues_{};
|
||||
|
||||
// Instance difficulty
|
||||
uint32_t instanceDifficulty_ = 0;
|
||||
bool instanceIsHeroic_ = false;
|
||||
|
||||
// ---- Phase 4: Group ----
|
||||
GroupListData partyData;
|
||||
bool pendingGroupInvite = false;
|
||||
|
|
|
|||
|
|
@ -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<ItemSlot, MAX_BAG_SIZE> slots{};
|
||||
};
|
||||
std::array<BagData, NUM_BAG_SLOTS> bags{};
|
||||
|
|
|
|||
26
include/pipeline/wdt_loader.hpp
Normal file
26
include/pipeline/wdt_loader.hpp
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
struct WDTInfo {
|
||||
uint32_t mphdFlags = 0;
|
||||
bool isWMOOnly() const { return mphdFlags & 0x01; } // WDTF_GLOBAL_WMO
|
||||
|
||||
std::string rootWMOPath; // from MWMO chunk (null-terminated string)
|
||||
|
||||
// MODF placement (only valid for WMO-only maps):
|
||||
float position[3] = {}; // ADT placement space coords
|
||||
float rotation[3] = {}; // degrees
|
||||
uint16_t flags = 0;
|
||||
uint16_t doodadSet = 0;
|
||||
};
|
||||
|
||||
WDTInfo parseWDT(const std::vector<uint8_t>& data);
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
#include <imgui.h>
|
||||
#include "pipeline/m2_loader.hpp"
|
||||
#include "pipeline/wmo_loader.hpp"
|
||||
#include "pipeline/wdt_loader.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "ui/ui_manager.hpp"
|
||||
#include "auth/auth_handler.hpp"
|
||||
|
|
@ -421,34 +422,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) {
|
||||
|
|
@ -3176,6 +3186,59 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
|
||||
showProgress("Entering world...", 0.0f);
|
||||
|
||||
// --- Clean up previous map's state on map change ---
|
||||
// (Same cleanup as logout, but preserves player identity and renderer objects.)
|
||||
if (loadedMapId_ != 0xFFFFFFFF) {
|
||||
LOG_INFO("Map change: cleaning up old map ", loadedMapId_, " before loading map ", mapId);
|
||||
|
||||
// Clear entity instances from old map
|
||||
creatureInstances_.clear();
|
||||
creatureModelIds_.clear();
|
||||
creatureRenderPosCache_.clear();
|
||||
creatureWeaponsAttached_.clear();
|
||||
creatureWeaponAttachAttempts_.clear();
|
||||
deadCreatureGuids_.clear();
|
||||
nonRenderableCreatureDisplayIds_.clear();
|
||||
creaturePermanentFailureGuids_.clear();
|
||||
|
||||
pendingCreatureSpawns_.clear();
|
||||
pendingCreatureSpawnGuids_.clear();
|
||||
creatureSpawnRetryCounts_.clear();
|
||||
|
||||
playerInstances_.clear();
|
||||
onlinePlayerAppearance_.clear();
|
||||
pendingOnlinePlayerEquipment_.clear();
|
||||
deferredEquipmentQueue_.clear();
|
||||
pendingPlayerSpawns_.clear();
|
||||
pendingPlayerSpawnGuids_.clear();
|
||||
|
||||
gameObjectInstances_.clear();
|
||||
pendingGameObjectSpawns_.clear();
|
||||
pendingTransportMoves_.clear();
|
||||
pendingTransportDoodadBatches_.clear();
|
||||
|
||||
if (renderer) {
|
||||
// Clear all world geometry from old map
|
||||
if (auto* wmo = renderer->getWMORenderer()) {
|
||||
wmo->clearInstances();
|
||||
}
|
||||
if (auto* m2 = renderer->getM2Renderer()) {
|
||||
m2->clear();
|
||||
}
|
||||
if (auto* terrain = renderer->getTerrainManager()) {
|
||||
terrain->softReset();
|
||||
terrain->setStreamingEnabled(true); // Re-enable in case previous map disabled it
|
||||
}
|
||||
if (auto* questMarkers = renderer->getQuestMarkerRenderer()) {
|
||||
questMarkers->clear();
|
||||
}
|
||||
renderer->clearMount();
|
||||
}
|
||||
|
||||
// Force player character re-spawn on new map
|
||||
playerCharacterSpawned = false;
|
||||
}
|
||||
|
||||
// Resolve map folder name from Map.dbc (authoritative for world/instance maps).
|
||||
// This is required for instances like DeeprunTram (map 369) that are not Azeroth/Kalimdor.
|
||||
if (!mapNameCacheLoaded_ && assetManager) {
|
||||
|
|
@ -3301,114 +3364,341 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
|
||||
showProgress("Loading terrain...", 0.20f);
|
||||
|
||||
// Compute ADT tile from canonical coordinates
|
||||
auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y);
|
||||
std::string adtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
||||
std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt";
|
||||
LOG_INFO("Loading ADT tile [", tileX, ",", tileY, "] from canonical (",
|
||||
spawnCanonical.x, ", ", spawnCanonical.y, ", ", spawnCanonical.z, ")");
|
||||
|
||||
// Load the initial terrain tile
|
||||
bool terrainOk = renderer->loadTestTerrain(assetManager.get(), adtPath);
|
||||
if (!terrainOk) {
|
||||
LOG_WARNING("Could not load terrain for online world - atmospheric rendering only");
|
||||
} else {
|
||||
LOG_INFO("Online world terrain loading initiated");
|
||||
// Check WDT to detect WMO-only maps (dungeons, raids, BGs)
|
||||
bool isWMOOnlyMap = false;
|
||||
pipeline::WDTInfo wdtInfo;
|
||||
{
|
||||
std::string wdtPath = "World\\Maps\\" + mapName + "\\" + mapName + ".wdt";
|
||||
LOG_WARNING("Reading WDT: ", wdtPath);
|
||||
std::vector<uint8_t> wdtData = assetManager->readFile(wdtPath);
|
||||
if (!wdtData.empty()) {
|
||||
wdtInfo = pipeline::parseWDT(wdtData);
|
||||
isWMOOnlyMap = wdtInfo.isWMOOnly() && !wdtInfo.rootWMOPath.empty();
|
||||
LOG_WARNING("WDT result: isWMOOnly=", isWMOOnlyMap, " rootWMO='", wdtInfo.rootWMOPath, "'");
|
||||
} else {
|
||||
LOG_WARNING("No WDT file found at ", wdtPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Character renderer is created inside loadTestTerrain(), so spawn the
|
||||
// player model now that the renderer actually exists.
|
||||
if (!playerCharacterSpawned) {
|
||||
spawnPlayerCharacter();
|
||||
loadEquippedWeapons();
|
||||
}
|
||||
bool terrainOk = false;
|
||||
|
||||
showProgress("Streaming terrain tiles...", 0.35f);
|
||||
if (isWMOOnlyMap) {
|
||||
// ---- WMO-only map (dungeon/raid/BG): load root WMO directly ----
|
||||
LOG_INFO("WMO-only map detected — loading root WMO: ", wdtInfo.rootWMOPath);
|
||||
showProgress("Loading instance geometry...", 0.25f);
|
||||
|
||||
// Wait for surrounding terrain tiles to stream in
|
||||
if (terrainOk && renderer->getTerrainManager() && renderer->getCamera()) {
|
||||
auto* terrainMgr = renderer->getTerrainManager();
|
||||
auto* camera = renderer->getCamera();
|
||||
// Still call loadTestTerrain with a dummy path to initialize all renderers
|
||||
// (terrain, WMO, M2, character). The terrain load will fail gracefully.
|
||||
auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y);
|
||||
std::string dummyAdtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
||||
std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt";
|
||||
renderer->loadTestTerrain(assetManager.get(), dummyAdtPath);
|
||||
|
||||
// Trigger tile streaming for surrounding area
|
||||
terrainMgr->update(*camera, 1.0f);
|
||||
// Disable terrain streaming — no ADT tiles for WMO-only maps
|
||||
if (renderer->getTerrainManager()) {
|
||||
renderer->getTerrainManager()->setStreamingEnabled(false);
|
||||
}
|
||||
|
||||
auto startTime = std::chrono::high_resolution_clock::now();
|
||||
auto lastProgressTime = startTime;
|
||||
const float maxWaitSeconds = 20.0f;
|
||||
const float stallSeconds = 5.0f;
|
||||
int initialRemaining = terrainMgr->getRemainingTileCount();
|
||||
if (initialRemaining < 1) initialRemaining = 1;
|
||||
int lastRemaining = initialRemaining;
|
||||
// Spawn player character now that renderers are initialized
|
||||
if (!playerCharacterSpawned) {
|
||||
spawnPlayerCharacter();
|
||||
loadEquippedWeapons();
|
||||
}
|
||||
|
||||
// Wait until all pending + ready-queue tiles are finalized
|
||||
while (terrainMgr->getRemainingTileCount() > 0) {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_QUIT) {
|
||||
window->setShouldClose(true);
|
||||
loadingScreen.shutdown();
|
||||
return;
|
||||
}
|
||||
if (event.type == SDL_WINDOWEVENT &&
|
||||
event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||
int w = event.window.data1;
|
||||
int h = event.window.data2;
|
||||
window->setSize(w, h);
|
||||
// Vulkan viewport set in command buffer
|
||||
if (renderer->getCamera()) {
|
||||
renderer->getCamera()->setAspectRatio(static_cast<float>(w) / h);
|
||||
// Load the root WMO
|
||||
auto* wmoRenderer = renderer->getWMORenderer();
|
||||
if (wmoRenderer) {
|
||||
std::vector<uint8_t> wmoData = assetManager->readFile(wdtInfo.rootWMOPath);
|
||||
if (!wmoData.empty()) {
|
||||
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
|
||||
|
||||
if (wmoModel.nGroups > 0) {
|
||||
showProgress("Loading instance groups...", 0.35f);
|
||||
std::string basePath = wdtInfo.rootWMOPath;
|
||||
std::string extension;
|
||||
if (basePath.size() > 4) {
|
||||
extension = basePath.substr(basePath.size() - 4);
|
||||
std::string extLower = extension;
|
||||
for (char& c : extLower) c = std::tolower(c);
|
||||
if (extLower == ".wmo") {
|
||||
basePath = basePath.substr(0, basePath.size() - 4);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t loadedGroups = 0;
|
||||
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
|
||||
char groupSuffix[16];
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
|
||||
std::string groupPath = basePath + groupSuffix;
|
||||
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (!groupData.empty()) {
|
||||
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
|
||||
loadedGroups++;
|
||||
}
|
||||
|
||||
// Update loading progress
|
||||
if (wmoModel.nGroups > 1) {
|
||||
float groupProgress = 0.35f + 0.30f * static_cast<float>(gi + 1) / wmoModel.nGroups;
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "Loading instance groups... %u / %u", gi + 1, wmoModel.nGroups);
|
||||
showProgress(buf, groupProgress);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Loaded ", loadedGroups, " / ", wmoModel.nGroups, " WMO groups for instance");
|
||||
}
|
||||
|
||||
// WMO-only maps: MODF position is at world origin (always 0,0,0 in practice).
|
||||
// Unlike ADT MODF which uses placement space, WMO-only maps place the WMO
|
||||
// directly in render coordinates with no offset or yaw bias.
|
||||
glm::vec3 wmoPos(0.0f);
|
||||
glm::vec3 wmoRot(0.0f);
|
||||
if (wdtInfo.position[0] != 0.0f || wdtInfo.position[1] != 0.0f || wdtInfo.position[2] != 0.0f) {
|
||||
// Non-zero placement — convert from ADT space (rare/never happens, but be safe)
|
||||
wmoPos = core::coords::adtToWorld(
|
||||
wdtInfo.position[0], wdtInfo.position[1], wdtInfo.position[2]);
|
||||
wmoRot = glm::vec3(
|
||||
-wdtInfo.rotation[2] * 3.14159f / 180.0f,
|
||||
-wdtInfo.rotation[0] * 3.14159f / 180.0f,
|
||||
(wdtInfo.rotation[1] + 180.0f) * 3.14159f / 180.0f
|
||||
);
|
||||
}
|
||||
|
||||
showProgress("Uploading instance geometry...", 0.70f);
|
||||
uint32_t wmoModelId = 900000 + mapId; // Unique ID range for instance WMOs
|
||||
if (wmoRenderer->loadModel(wmoModel, wmoModelId)) {
|
||||
uint32_t instanceId = wmoRenderer->createInstance(wmoModelId, wmoPos, wmoRot, 1.0f);
|
||||
if (instanceId > 0) {
|
||||
LOG_INFO("Instance WMO loaded: modelId=", wmoModelId,
|
||||
" instanceId=", instanceId,
|
||||
" pos=(", wmoPos.x, ", ", wmoPos.y, ", ", wmoPos.z, ")");
|
||||
|
||||
// Load doodads from the specified doodad set
|
||||
auto* m2Renderer = renderer->getM2Renderer();
|
||||
if (m2Renderer && !wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) {
|
||||
uint32_t setIdx = std::min(static_cast<uint32_t>(wdtInfo.doodadSet),
|
||||
static_cast<uint32_t>(wmoModel.doodadSets.size() - 1));
|
||||
const auto& doodadSet = wmoModel.doodadSets[setIdx];
|
||||
|
||||
showProgress("Loading instance doodads...", 0.75f);
|
||||
glm::mat4 wmoMatrix(1.0f);
|
||||
wmoMatrix = glm::translate(wmoMatrix, wmoPos);
|
||||
wmoMatrix = glm::rotate(wmoMatrix, wmoRot.z, glm::vec3(0, 0, 1));
|
||||
wmoMatrix = glm::rotate(wmoMatrix, wmoRot.y, glm::vec3(0, 1, 0));
|
||||
wmoMatrix = glm::rotate(wmoMatrix, wmoRot.x, glm::vec3(1, 0, 0));
|
||||
|
||||
uint32_t loadedDoodads = 0;
|
||||
for (uint32_t di = 0; di < doodadSet.count; di++) {
|
||||
uint32_t doodadIdx = doodadSet.startIndex + di;
|
||||
if (doodadIdx >= wmoModel.doodads.size()) break;
|
||||
|
||||
const auto& doodad = wmoModel.doodads[doodadIdx];
|
||||
auto nameIt = wmoModel.doodadNames.find(doodad.nameIndex);
|
||||
if (nameIt == wmoModel.doodadNames.end()) continue;
|
||||
|
||||
std::string m2Path = nameIt->second;
|
||||
if (m2Path.empty()) continue;
|
||||
|
||||
if (m2Path.size() > 4) {
|
||||
std::string ext = m2Path.substr(m2Path.size() - 4);
|
||||
for (char& c : ext) c = std::tolower(c);
|
||||
if (ext == ".mdx" || ext == ".mdl") {
|
||||
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> m2Data = assetManager->readFile(m2Path);
|
||||
if (m2Data.empty()) continue;
|
||||
|
||||
pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data);
|
||||
if (m2Model.name.empty()) m2Model.name = m2Path;
|
||||
|
||||
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||
std::vector<uint8_t> skinData = assetManager->readFile(skinPath);
|
||||
if (!skinData.empty() && m2Model.version >= 264) {
|
||||
pipeline::M2Loader::loadSkin(skinData, m2Model);
|
||||
}
|
||||
if (!m2Model.isValid()) continue;
|
||||
|
||||
glm::quat fixedRotation(doodad.rotation.w, doodad.rotation.y,
|
||||
doodad.rotation.x, doodad.rotation.z);
|
||||
glm::mat4 doodadLocal(1.0f);
|
||||
doodadLocal = glm::translate(doodadLocal, doodad.position);
|
||||
doodadLocal *= glm::mat4_cast(fixedRotation);
|
||||
doodadLocal = glm::scale(doodadLocal, glm::vec3(doodad.scale));
|
||||
|
||||
glm::mat4 worldMatrix = wmoMatrix * doodadLocal;
|
||||
glm::vec3 worldPos = glm::vec3(worldMatrix[3]);
|
||||
|
||||
uint32_t doodadModelId = static_cast<uint32_t>(std::hash<std::string>{}(m2Path));
|
||||
m2Renderer->loadModel(m2Model, doodadModelId);
|
||||
m2Renderer->createInstance(doodadModelId, worldPos, glm::vec3(0.0f), doodad.scale);
|
||||
loadedDoodads++;
|
||||
}
|
||||
LOG_INFO("Loaded ", loadedDoodads, " instance WMO doodads");
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("Failed to create instance WMO instance");
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("Failed to load instance WMO model");
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("Failed to read root WMO file: ", wdtInfo.rootWMOPath);
|
||||
}
|
||||
|
||||
// Trigger new streaming and process ALL ready tiles (not just 2)
|
||||
terrainMgr->update(*camera, 0.016f);
|
||||
terrainMgr->processAllReadyTiles();
|
||||
// Build collision cache for the instance WMO
|
||||
showProgress("Building collision cache...", 0.88f);
|
||||
if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); }
|
||||
wmoRenderer->loadFloorCache();
|
||||
if (wmoRenderer->getFloorCacheSize() == 0) {
|
||||
showProgress("Computing walkable surfaces...", 0.90f);
|
||||
if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); }
|
||||
wmoRenderer->precomputeFloorCache();
|
||||
}
|
||||
}
|
||||
|
||||
terrainOk = true; // Mark as OK so post-load setup runs
|
||||
} else {
|
||||
// ---- Normal ADT-based map ----
|
||||
// Compute ADT tile from canonical coordinates
|
||||
auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y);
|
||||
std::string adtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
||||
std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt";
|
||||
LOG_INFO("Loading ADT tile [", tileX, ",", tileY, "] from canonical (",
|
||||
spawnCanonical.x, ", ", spawnCanonical.y, ", ", spawnCanonical.z, ")");
|
||||
|
||||
// Load the initial terrain tile
|
||||
terrainOk = renderer->loadTestTerrain(assetManager.get(), adtPath);
|
||||
if (!terrainOk) {
|
||||
LOG_WARNING("Could not load terrain for online world - atmospheric rendering only");
|
||||
} else {
|
||||
LOG_INFO("Online world terrain loading initiated");
|
||||
}
|
||||
|
||||
// Character renderer is created inside loadTestTerrain(), so spawn the
|
||||
// player model now that the renderer actually exists.
|
||||
if (!playerCharacterSpawned) {
|
||||
spawnPlayerCharacter();
|
||||
loadEquippedWeapons();
|
||||
}
|
||||
|
||||
showProgress("Streaming terrain tiles...", 0.35f);
|
||||
|
||||
// Wait for surrounding terrain tiles to stream in
|
||||
if (terrainOk && renderer->getTerrainManager() && renderer->getCamera()) {
|
||||
auto* terrainMgr = renderer->getTerrainManager();
|
||||
auto* camera = renderer->getCamera();
|
||||
|
||||
// Trigger tile streaming for surrounding area
|
||||
terrainMgr->update(*camera, 1.0f);
|
||||
|
||||
auto startTime = std::chrono::high_resolution_clock::now();
|
||||
auto lastProgressTime = startTime;
|
||||
const float maxWaitSeconds = 60.0f;
|
||||
const float stallSeconds = 10.0f;
|
||||
int initialRemaining = terrainMgr->getRemainingTileCount();
|
||||
if (initialRemaining < 1) initialRemaining = 1;
|
||||
int lastRemaining = initialRemaining;
|
||||
|
||||
// Wait until all pending + ready-queue tiles are finalized
|
||||
while (terrainMgr->getRemainingTileCount() > 0) {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_QUIT) {
|
||||
window->setShouldClose(true);
|
||||
loadingScreen.shutdown();
|
||||
return;
|
||||
}
|
||||
if (event.type == SDL_WINDOWEVENT &&
|
||||
event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||
int w = event.window.data1;
|
||||
int h = event.window.data2;
|
||||
window->setSize(w, h);
|
||||
// Vulkan viewport set in command buffer
|
||||
if (renderer->getCamera()) {
|
||||
renderer->getCamera()->setAspectRatio(static_cast<float>(w) / h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger new streaming — enqueue tiles for background workers
|
||||
terrainMgr->update(*camera, 0.016f);
|
||||
|
||||
// Process ONE tile per iteration so loading screen updates after each
|
||||
terrainMgr->processOneReadyTile();
|
||||
|
||||
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;
|
||||
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;
|
||||
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();
|
||||
|
||||
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) {
|
||||
loadingScreen.setStatus(buf);
|
||||
loadingScreen.setProgress(progress);
|
||||
loadingScreen.render();
|
||||
window->swapBuffers();
|
||||
}
|
||||
|
||||
if (remaining != lastRemaining) {
|
||||
lastRemaining = remaining;
|
||||
lastProgressTime = std::chrono::high_resolution_clock::now();
|
||||
lastProgressTime = now;
|
||||
}
|
||||
|
||||
auto elapsed = std::chrono::high_resolution_clock::now() - startTime;
|
||||
if (std::chrono::duration<float>(elapsed).count() > maxWaitSeconds) {
|
||||
LOG_WARNING("Online terrain streaming timeout after ", maxWaitSeconds, "s");
|
||||
break;
|
||||
}
|
||||
auto stalledFor = std::chrono::high_resolution_clock::now() - lastProgressTime;
|
||||
if (std::chrono::duration<float>(stalledFor).count() > stallSeconds) {
|
||||
LOG_WARNING("Online terrain streaming stalled for ", stallSeconds,
|
||||
"s (remaining=", lastRemaining, "), continuing without full preload");
|
||||
break;
|
||||
}
|
||||
|
||||
// Don't sleep if there are more tiles to finalize — keep processing
|
||||
if (remaining > 0 && terrainMgr->getReadyQueueCount() == 0) {
|
||||
SDL_Delay(16);
|
||||
}
|
||||
}
|
||||
|
||||
auto elapsed = std::chrono::high_resolution_clock::now() - startTime;
|
||||
if (std::chrono::duration<float>(elapsed).count() > maxWaitSeconds) {
|
||||
LOG_WARNING("Online terrain streaming timeout after ", maxWaitSeconds, "s");
|
||||
break;
|
||||
}
|
||||
auto stalledFor = std::chrono::high_resolution_clock::now() - lastProgressTime;
|
||||
if (std::chrono::duration<float>(stalledFor).count() > stallSeconds) {
|
||||
LOG_WARNING("Online terrain streaming stalled for ", stallSeconds,
|
||||
"s (remaining=", lastRemaining, "), continuing without full preload");
|
||||
break;
|
||||
}
|
||||
LOG_INFO("Online terrain streaming complete: ", terrainMgr->getLoadedTileCount(), " tiles loaded");
|
||||
|
||||
SDL_Delay(16);
|
||||
}
|
||||
|
||||
LOG_INFO("Online terrain streaming complete: ", terrainMgr->getLoadedTileCount(), " tiles loaded");
|
||||
|
||||
// Load/precompute collision cache
|
||||
if (renderer->getWMORenderer()) {
|
||||
showProgress("Building collision cache...", 0.88f);
|
||||
renderer->getWMORenderer()->loadFloorCache();
|
||||
if (renderer->getWMORenderer()->getFloorCacheSize() == 0) {
|
||||
renderer->getWMORenderer()->precomputeFloorCache();
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -513,6 +513,9 @@ void GameHandler::resetDbcCaches() {
|
|||
taxiNodes_.clear();
|
||||
taxiPathEdges_.clear();
|
||||
taxiPathNodes_.clear();
|
||||
areaTriggerDbcLoaded_ = false;
|
||||
areaTriggers_.clear();
|
||||
activeAreaTriggers_.clear();
|
||||
talentDbcLoaded_ = false;
|
||||
talentCache_.clear();
|
||||
talentTabCache_.clear();
|
||||
|
|
@ -720,6 +723,13 @@ void GameHandler::update(float deltaTime) {
|
|||
timeSinceLastMoveHeartbeat_ = 0.0f;
|
||||
}
|
||||
|
||||
// Check area triggers (instance portals, tavern rests, etc.)
|
||||
areaTriggerCheckTimer_ += deltaTime;
|
||||
if (areaTriggerCheckTimer_ >= 0.25f) {
|
||||
areaTriggerCheckTimer_ = 0.0f;
|
||||
checkAreaTriggers();
|
||||
}
|
||||
|
||||
// Update cast timer (Phase 3)
|
||||
if (pendingGameObjectInteractGuid_ != 0 &&
|
||||
(autoAttacking || autoAttackRequested_)) {
|
||||
|
|
@ -2683,7 +2693,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_TRANSFER_PENDING: {
|
||||
// SMSG_TRANSFER_PENDING: uint32 mapId, then optional transport data
|
||||
uint32_t pendingMapId = packet.readUInt32();
|
||||
LOG_INFO("SMSG_TRANSFER_PENDING: mapId=", pendingMapId);
|
||||
LOG_WARNING("SMSG_TRANSFER_PENDING: mapId=", pendingMapId);
|
||||
// Optional: if remaining data, there's a transport entry + mapId
|
||||
if (packet.getReadPos() + 8 <= packet.getSize()) {
|
||||
uint32_t transportEntry = packet.readUInt32();
|
||||
|
|
@ -2750,6 +2760,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_BATTLEGROUND_PLAYER_LEFT:
|
||||
LOG_INFO("Battleground player left");
|
||||
break;
|
||||
case Opcode::SMSG_INSTANCE_DIFFICULTY:
|
||||
handleInstanceDifficulty(packet);
|
||||
break;
|
||||
case Opcode::SMSG_ARENA_TEAM_COMMAND_RESULT:
|
||||
handleArenaTeamCommandResult(packet);
|
||||
break;
|
||||
|
|
@ -5233,6 +5246,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
else if (ufPBytes2 != 0xFFFF && key == ufPBytes2) {
|
||||
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);
|
||||
}
|
||||
// Do not synthesize quest-log entries from raw update-field slots.
|
||||
|
|
@ -5535,6 +5550,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
else if (ufPBytes2v != 0xFFFF && key == ufPBytes2v) {
|
||||
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);
|
||||
}
|
||||
else if (key == ufPlayerFlags) {
|
||||
|
|
@ -7707,7 +7724,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 +7929,31 @@ void GameHandler::rebuildOnlineInventory() {
|
|||
if (contIt != containerContents_.end()) {
|
||||
numSlots = static_cast<int>(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<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;
|
||||
|
||||
|
|
@ -8632,6 +8668,14 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) {
|
|||
bgName = std::to_string(arenaType) + "v" + std::to_string(arenaType) + " Arena";
|
||||
}
|
||||
|
||||
// Store queue state
|
||||
if (queueSlot < bgQueues_.size()) {
|
||||
bgQueues_[queueSlot].queueSlot = queueSlot;
|
||||
bgQueues_[queueSlot].bgTypeId = bgTypeId;
|
||||
bgQueues_[queueSlot].arenaType = arenaType;
|
||||
bgQueues_[queueSlot].statusId = statusId;
|
||||
}
|
||||
|
||||
switch (statusId) {
|
||||
case 0: // STATUS_NONE
|
||||
LOG_INFO("Battlefield status: NONE for ", bgName);
|
||||
|
|
@ -8657,6 +8701,183 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
bool GameHandler::hasPendingBgInvite() const {
|
||||
for (const auto& slot : bgQueues_) {
|
||||
if (slot.statusId == 2) return true; // STATUS_WAIT_JOIN
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void GameHandler::acceptBattlefield(uint32_t queueSlot) {
|
||||
if (state != WorldState::IN_WORLD) return;
|
||||
if (!socket) return;
|
||||
|
||||
// Find first WAIT_JOIN slot if no specific slot given
|
||||
const BgQueueSlot* slot = nullptr;
|
||||
if (queueSlot == 0xFFFFFFFF) {
|
||||
for (const auto& s : bgQueues_) {
|
||||
if (s.statusId == 2) { slot = &s; break; }
|
||||
}
|
||||
} else if (queueSlot < bgQueues_.size() && bgQueues_[queueSlot].statusId == 2) {
|
||||
slot = &bgQueues_[queueSlot];
|
||||
}
|
||||
|
||||
if (!slot) {
|
||||
addSystemChatMessage("No battleground invitation pending.");
|
||||
return;
|
||||
}
|
||||
|
||||
// CMSG_BATTLEFIELD_PORT: arenaType(1) + unk(1) + bgTypeId(4) + unk(2) + action(1) = 9 bytes
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_BATTLEFIELD_PORT));
|
||||
pkt.writeUInt8(slot->arenaType);
|
||||
pkt.writeUInt8(0x00);
|
||||
pkt.writeUInt32(slot->bgTypeId);
|
||||
pkt.writeUInt16(0x0000);
|
||||
pkt.writeUInt8(1); // 1 = accept, 0 = decline
|
||||
|
||||
socket->send(pkt);
|
||||
|
||||
addSystemChatMessage("Accepting battleground invitation...");
|
||||
LOG_INFO("Sent CMSG_BATTLEFIELD_PORT: accept bgTypeId=", slot->bgTypeId);
|
||||
}
|
||||
|
||||
void GameHandler::handleInstanceDifficulty(network::Packet& packet) {
|
||||
if (packet.getSize() - packet.getReadPos() < 8) return;
|
||||
instanceDifficulty_ = packet.readUInt32();
|
||||
uint32_t isHeroic = packet.readUInt32();
|
||||
instanceIsHeroic_ = (isHeroic != 0);
|
||||
LOG_INFO("Instance difficulty: ", instanceDifficulty_, " heroic=", instanceIsHeroic_);
|
||||
}
|
||||
|
||||
void GameHandler::loadAreaTriggerDbc() {
|
||||
if (areaTriggerDbcLoaded_) return;
|
||||
areaTriggerDbcLoaded_ = true;
|
||||
|
||||
auto* am = core::Application::getInstance().getAssetManager();
|
||||
if (!am || !am->isInitialized()) return;
|
||||
|
||||
auto dbc = am->loadDBC("AreaTrigger.dbc");
|
||||
if (!dbc || !dbc->isLoaded()) {
|
||||
LOG_WARNING("Failed to load AreaTrigger.dbc");
|
||||
return;
|
||||
}
|
||||
|
||||
areaTriggers_.reserve(dbc->getRecordCount());
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
AreaTriggerEntry at;
|
||||
at.id = dbc->getUInt32(i, 0);
|
||||
at.mapId = dbc->getUInt32(i, 1);
|
||||
// DBC stores positions in server/wire format (X=west, Y=north) — swap to canonical
|
||||
at.x = dbc->getFloat(i, 3); // canonical X (north) = DBC field 3 (Y_wire)
|
||||
at.y = dbc->getFloat(i, 2); // canonical Y (west) = DBC field 2 (X_wire)
|
||||
at.z = dbc->getFloat(i, 4);
|
||||
at.radius = dbc->getFloat(i, 5);
|
||||
at.boxLength = dbc->getFloat(i, 6);
|
||||
at.boxWidth = dbc->getFloat(i, 7);
|
||||
at.boxHeight = dbc->getFloat(i, 8);
|
||||
at.boxYaw = dbc->getFloat(i, 9);
|
||||
areaTriggers_.push_back(at);
|
||||
}
|
||||
|
||||
LOG_WARNING("Loaded ", areaTriggers_.size(), " area triggers from AreaTrigger.dbc");
|
||||
}
|
||||
|
||||
void GameHandler::checkAreaTriggers() {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
if (onTaxiFlight_ || taxiClientActive_) return;
|
||||
|
||||
loadAreaTriggerDbc();
|
||||
if (areaTriggers_.empty()) return;
|
||||
|
||||
const float px = movementInfo.x;
|
||||
const float py = movementInfo.y;
|
||||
const float pz = movementInfo.z;
|
||||
|
||||
// Debug: log player position periodically to verify trigger proximity
|
||||
static int debugCounter = 0;
|
||||
if (++debugCounter >= 4) { // every ~1s at 0.25s interval
|
||||
debugCounter = 0;
|
||||
int mapTriggerCount = 0;
|
||||
float closestDist = 999999.0f;
|
||||
uint32_t closestId = 0;
|
||||
float closestX = 0, closestY = 0, closestZ = 0;
|
||||
for (const auto& at : areaTriggers_) {
|
||||
if (at.mapId != currentMapId_) continue;
|
||||
mapTriggerCount++;
|
||||
float dx = px - at.x, dy = py - at.y, dz = pz - at.z;
|
||||
float dist = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
if (dist < closestDist) { closestDist = dist; closestId = at.id; closestX = at.x; closestY = at.y; closestZ = at.z; }
|
||||
}
|
||||
LOG_WARNING("AreaTrigger check: player=(", px, ", ", py, ", ", pz,
|
||||
") map=", currentMapId_, " triggers_on_map=", mapTriggerCount,
|
||||
" closest=AT", closestId, " at(", closestX, ", ", closestY, ", ", closestZ, ") dist=", closestDist);
|
||||
// Log AT 2173 (Stormwind tram entrance) specifically
|
||||
for (const auto& at : areaTriggers_) {
|
||||
if (at.id == 2173) {
|
||||
float dx = px - at.x, dy = py - at.y, dz = pz - at.z;
|
||||
float dist = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
LOG_WARNING(" AT2173: map=", at.mapId, " pos=(", at.x, ", ", at.y, ", ", at.z,
|
||||
") r=", at.radius, " box=(", at.boxLength, ", ", at.boxWidth, ", ", at.boxHeight, ") dist=", dist);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& at : areaTriggers_) {
|
||||
if (at.mapId != currentMapId_) continue;
|
||||
|
||||
bool inside = false;
|
||||
if (at.radius > 0.0f) {
|
||||
// Sphere trigger — use generous minimum radius since WMO collision
|
||||
// may block the player from reaching triggers inside doorways/hallways
|
||||
float effectiveRadius = std::max(at.radius, 45.0f);
|
||||
float dx = px - at.x;
|
||||
float dy = py - at.y;
|
||||
float dz = pz - at.z;
|
||||
float distSq = dx * dx + dy * dy + dz * dz;
|
||||
inside = (distSq <= effectiveRadius * effectiveRadius);
|
||||
} else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) {
|
||||
// Box trigger (axis-aligned or rotated)
|
||||
float dx = px - at.x;
|
||||
float dy = py - at.y;
|
||||
float dz = pz - at.z;
|
||||
|
||||
// Rotate into box-local space
|
||||
float cosYaw = std::cos(-at.boxYaw);
|
||||
float sinYaw = std::sin(-at.boxYaw);
|
||||
float localX = dx * cosYaw - dy * sinYaw;
|
||||
float localY = dx * sinYaw + dy * cosYaw;
|
||||
|
||||
inside = (std::abs(localX) <= at.boxLength * 0.5f &&
|
||||
std::abs(localY) <= at.boxWidth * 0.5f &&
|
||||
std::abs(dz) <= at.boxHeight * 0.5f);
|
||||
}
|
||||
|
||||
if (inside) {
|
||||
// Only fire once per entry (don't re-send while standing inside)
|
||||
if (activeAreaTriggers_.count(at.id) == 0) {
|
||||
activeAreaTriggers_.insert(at.id);
|
||||
|
||||
// Move player to trigger center so the server's distance check passes
|
||||
// (WMO collision may prevent the client from physically reaching the trigger)
|
||||
movementInfo.x = at.x;
|
||||
movementInfo.y = at.y;
|
||||
movementInfo.z = at.z;
|
||||
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
|
||||
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_AREATRIGGER));
|
||||
pkt.writeUInt32(at.id);
|
||||
socket->send(pkt);
|
||||
LOG_WARNING("Fired CMSG_AREATRIGGER: id=", at.id,
|
||||
" at (", at.x, ", ", at.y, ", ", at.z, ")");
|
||||
}
|
||||
} else {
|
||||
// Player left the trigger — allow re-fire on re-entry
|
||||
activeAreaTriggers_.erase(at.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleArenaTeamCommandResult(network::Packet& packet) {
|
||||
if (packet.getSize() - packet.getReadPos() < 8) return;
|
||||
uint32_t command = packet.readUInt32();
|
||||
|
|
@ -11937,7 +12158,7 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
|
|||
float serverZ = packet.readFloat();
|
||||
float orientation = packet.readFloat();
|
||||
|
||||
LOG_INFO("SMSG_NEW_WORLD: mapId=", mapId,
|
||||
LOG_WARNING("SMSG_NEW_WORLD: mapId=", mapId,
|
||||
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")",
|
||||
" orient=", orientation);
|
||||
|
||||
|
|
@ -12009,6 +12230,8 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
|
|||
worldStates_.clear();
|
||||
worldStateMapId_ = mapId;
|
||||
worldStateZoneId_ = 0;
|
||||
activeAreaTriggers_.clear();
|
||||
areaTriggerCheckTimer_ = -5.0f; // 5-second cooldown after map transfer
|
||||
stopAutoAttack();
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
|
|
@ -13673,7 +13896,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<int>(inventory.getPurchasedBankBagSlots()));
|
||||
auto pkt = BuyBankSlotPacket::build(bankerGuid_);
|
||||
socket->send(pkt);
|
||||
}
|
||||
|
|
@ -13698,17 +13926,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<int>(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) + ").");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
110
src/pipeline/wdt_loader.cpp
Normal file
110
src/pipeline/wdt_loader.cpp
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#include "pipeline/wdt_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
uint32_t readU32(const uint8_t* data, size_t offset) {
|
||||
uint32_t v;
|
||||
std::memcpy(&v, data + offset, 4);
|
||||
return v;
|
||||
}
|
||||
|
||||
uint16_t readU16(const uint8_t* data, size_t offset) {
|
||||
uint16_t v;
|
||||
std::memcpy(&v, data + offset, 2);
|
||||
return v;
|
||||
}
|
||||
|
||||
float readF32(const uint8_t* data, size_t offset) {
|
||||
float v;
|
||||
std::memcpy(&v, data + offset, 4);
|
||||
return v;
|
||||
}
|
||||
|
||||
// Chunk magic constants (little-endian)
|
||||
constexpr uint32_t MVER = 0x5245564D; // "REVM"
|
||||
constexpr uint32_t MPHD = 0x4448504D; // "DHPM"
|
||||
constexpr uint32_t MAIN = 0x4E49414D; // "NIAM"
|
||||
constexpr uint32_t MWMO = 0x4F4D574D; // "OMWM"
|
||||
constexpr uint32_t MODF = 0x46444F4D; // "FDOM"
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
WDTInfo parseWDT(const std::vector<uint8_t>& data) {
|
||||
WDTInfo info;
|
||||
|
||||
if (data.size() < 8) {
|
||||
LOG_WARNING("WDT data too small (", data.size(), " bytes)");
|
||||
return info;
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
|
||||
while (offset + 8 <= data.size()) {
|
||||
uint32_t magic = readU32(data.data(), offset);
|
||||
uint32_t chunkSize = readU32(data.data(), offset + 4);
|
||||
|
||||
if (offset + 8 + chunkSize > data.size()) {
|
||||
LOG_WARNING("WDT chunk extends beyond file at offset ", offset);
|
||||
break;
|
||||
}
|
||||
|
||||
const uint8_t* chunkData = data.data() + offset + 8;
|
||||
|
||||
if (magic == MVER) {
|
||||
if (chunkSize >= 4) {
|
||||
uint32_t version = readU32(chunkData, 0);
|
||||
LOG_DEBUG("WDT version: ", version);
|
||||
}
|
||||
} else if (magic == MPHD) {
|
||||
if (chunkSize >= 4) {
|
||||
info.mphdFlags = readU32(chunkData, 0);
|
||||
LOG_DEBUG("WDT MPHD flags: 0x", std::hex, info.mphdFlags, std::dec);
|
||||
}
|
||||
} else if (magic == MWMO) {
|
||||
// Null-terminated WMO path string(s)
|
||||
if (chunkSize > 0) {
|
||||
const char* str = reinterpret_cast<const char*>(chunkData);
|
||||
size_t len = std::strlen(str);
|
||||
if (len > 0) {
|
||||
info.rootWMOPath = std::string(str, len);
|
||||
LOG_DEBUG("WDT root WMO: ", info.rootWMOPath);
|
||||
}
|
||||
}
|
||||
} else if (magic == MODF) {
|
||||
// MODF entry is 64 bytes (same layout as ADT MODF)
|
||||
if (chunkSize >= 64) {
|
||||
// nameId at offset 0 (unused for WDT — path comes from MWMO)
|
||||
// uniqueId at offset 4
|
||||
info.position[0] = readF32(chunkData, 8);
|
||||
info.position[1] = readF32(chunkData, 12);
|
||||
info.position[2] = readF32(chunkData, 16);
|
||||
info.rotation[0] = readF32(chunkData, 20);
|
||||
info.rotation[1] = readF32(chunkData, 24);
|
||||
info.rotation[2] = readF32(chunkData, 28);
|
||||
// extents at 32-55
|
||||
info.flags = readU16(chunkData, 56);
|
||||
info.doodadSet = readU16(chunkData, 58);
|
||||
LOG_DEBUG("WDT MODF placement: pos=(", info.position[0], ", ",
|
||||
info.position[1], ", ", info.position[2], ") rot=(",
|
||||
info.rotation[0], ", ", info.rotation[1], ", ",
|
||||
info.rotation[2], ") doodadSet=", info.doodadSet);
|
||||
}
|
||||
}
|
||||
|
||||
offset += 8 + chunkSize;
|
||||
}
|
||||
|
||||
LOG_WARNING("WDT parse result: mphdFlags=0x", std::hex, info.mphdFlags, std::dec,
|
||||
" isWMOOnly=", info.isWMOOnly(),
|
||||
" rootWMO='", info.rootWMOPath, "'");
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -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<float>::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++) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<PendingTile> 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<PendingTile> 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<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
if (!workerRunning.load()) return nullptr;
|
||||
|
||||
auto pending = std::make_shared<PendingTile>();
|
||||
pending->coord = coord;
|
||||
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)
|
||||
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<PendingTile> 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<PendingTile> 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<PendingTile> 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<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::lock_guard<std::mutex> 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -2765,6 +2765,12 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
isChannelCommand = true;
|
||||
switchChatType = 9;
|
||||
} else if (cmdLower == "join") {
|
||||
// /join with no args: accept pending BG invite if any
|
||||
if (spacePos == std::string::npos && gameHandler.hasPendingBgInvite()) {
|
||||
gameHandler.acceptBattlefield();
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
// /join ChannelName [password]
|
||||
if (spacePos != std::string::npos) {
|
||||
std::string rest = command.substr(spacePos + 1);
|
||||
|
|
@ -7764,57 +7770,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<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();
|
||||
}
|
||||
}
|
||||
renderBankItemSlot(inv.getBankSlot(i), 0, i, -1, -1, 0xFF, static_cast<uint8_t>(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 +7905,12 @@ void GameScreen::renderBankWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::PushID(i + 2000);
|
||||
|
||||
int bagSize = inv.getBankBagSize(i);
|
||||
if (i < static_cast<int>(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<uint8_t>(67 + i));
|
||||
} else {
|
||||
if (ImGui::Button("Buy", ImVec2(50, 30))) {
|
||||
if (ImGui::Button("Buy Slot", ImVec2(50, 30))) {
|
||||
gameHandler.buyBankSlot();
|
||||
}
|
||||
}
|
||||
|
|
@ -7845,37 +7926,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<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();
|
||||
}
|
||||
}
|
||||
renderBankItemSlot(inv.getBankBagSlot(bagIdx, s), 1, -1, bagIdx, s,
|
||||
static_cast<uint8_t>(67 + bagIdx), static_cast<uint8_t>(s));
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<uint8_t>(heldBagSlotIndex);
|
||||
} else if (heldSource == HeldSource::EQUIPMENT) {
|
||||
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 {
|
||||
cancelPickup(inv);
|
||||
return;
|
||||
|
|
@ -357,6 +415,13 @@ void InventoryScreen::placeInBag(game::Inventory& inv, int bagIndex, int slotInd
|
|||
srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
|
||||
} else if (heldSource == HeldSource::EQUIPMENT) {
|
||||
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 {
|
||||
cancelPickup(inv);
|
||||
return;
|
||||
|
|
@ -417,6 +482,11 @@ void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slo
|
|||
srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
|
||||
} else if (heldSource == HeldSource::EQUIPMENT && heldEquipSlot != game::EquipSlot::NUM_SLOTS) {
|
||||
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 {
|
||||
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<uint8_t>(heldBagSlotIndex);
|
||||
} else if (heldSource == HeldSource::EQUIPMENT) {
|
||||
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 {
|
||||
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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue