Fix online item GUID resolution, async terrain loading, and inventory enrichment

Enrich online inventory from local DB when server data is incomplete, add
resolveOnlineItemGuid fallback for sell/equip/use, use async enqueueTile for
initial terrain load, improve walk/run animation fallbacks, clear target on
loot close, and broaden equipability detection to include armor/subclass.
This commit is contained in:
Kelsi 2026-02-06 18:52:28 -08:00
parent db4a40a4e6
commit e38c0213e4
6 changed files with 119 additions and 8 deletions

View file

@ -474,6 +474,7 @@ private:
void rebuildOnlineInventory();
void detectInventorySlotBases(const std::map<uint16_t, uint32_t>& fields);
bool applyInventoryFields(const std::map<uint16_t, uint32_t>& fields);
uint64_t resolveOnlineItemGuid(uint32_t itemId) const;
// ---- Phase 2 handlers ----
void handleAttackStart(network::Packet& packet);

View file

@ -143,6 +143,11 @@ public:
*/
bool loadTile(int x, int y);
/**
* Enqueue a tile for async loading (returns false if previously failed).
*/
bool enqueueTile(int x, int y);
/**
* Unload a tile
* @param x Tile X coordinate

View file

@ -3120,6 +3120,19 @@ void GameHandler::handleItemQueryResponse(network::Packet& packet) {
}
}
uint64_t GameHandler::resolveOnlineItemGuid(uint32_t itemId) const {
if (itemId == 0) return 0;
uint64_t found = 0;
for (const auto& [guid, info] : onlineItems_) {
if (info.entry != itemId) continue;
if (found != 0) {
return 0; // Ambiguous
}
found = guid;
}
return found;
}
void GameHandler::detectInventorySlotBases(const std::map<uint16_t, uint32_t>& fields) {
if (invSlotBase_ >= 0 && packSlotBase_ >= 0) return;
if (onlineItems_.empty() || fields.empty()) return;
@ -3215,6 +3228,31 @@ void GameHandler::rebuildOnlineInventory() {
def.name = "Item " + std::to_string(def.itemId);
queryItemInfo(def.itemId, guid);
}
if (def.itemId != 0) {
auto& db = getSinglePlayerLootDb();
auto itTpl = db.itemTemplates.find(def.itemId);
if (itTpl != db.itemTemplates.end()) {
if (def.name.empty() || def.name.rfind("Item ", 0) == 0) def.name = itTpl->second.name;
if (def.quality == ItemQuality::COMMON && itTpl->second.quality != 0) {
def.quality = static_cast<ItemQuality>(itTpl->second.quality);
}
if (def.inventoryType == 0 && itTpl->second.inventoryType != 0) {
def.inventoryType = itTpl->second.inventoryType;
}
if (def.maxStack <= 1 && itTpl->second.maxStack > 1) {
def.maxStack = static_cast<uint32_t>(itTpl->second.maxStack);
}
if (def.displayInfoId == 0 && itTpl->second.displayId != 0) {
def.displayInfoId = itTpl->second.displayId;
}
if (def.armor == 0 && itTpl->second.armor > 0) def.armor = itTpl->second.armor;
if (def.stamina == 0 && itTpl->second.stamina > 0) def.stamina = itTpl->second.stamina;
if (def.strength == 0 && itTpl->second.strength > 0) def.strength = itTpl->second.strength;
if (def.agility == 0 && itTpl->second.agility > 0) def.agility = itTpl->second.agility;
if (def.intellect == 0 && itTpl->second.intellect > 0) def.intellect = itTpl->second.intellect;
if (def.spirit == 0 && itTpl->second.spirit > 0) def.spirit = itTpl->second.spirit;
}
}
inventory.setEquipSlot(static_cast<EquipSlot>(i), def);
}
@ -3250,6 +3288,31 @@ void GameHandler::rebuildOnlineInventory() {
def.name = "Item " + std::to_string(def.itemId);
queryItemInfo(def.itemId, guid);
}
if (def.itemId != 0) {
auto& db = getSinglePlayerLootDb();
auto itTpl = db.itemTemplates.find(def.itemId);
if (itTpl != db.itemTemplates.end()) {
if (def.name.empty() || def.name.rfind("Item ", 0) == 0) def.name = itTpl->second.name;
if (def.quality == ItemQuality::COMMON && itTpl->second.quality != 0) {
def.quality = static_cast<ItemQuality>(itTpl->second.quality);
}
if (def.inventoryType == 0 && itTpl->second.inventoryType != 0) {
def.inventoryType = itTpl->second.inventoryType;
}
if (def.maxStack <= 1 && itTpl->second.maxStack > 1) {
def.maxStack = static_cast<uint32_t>(itTpl->second.maxStack);
}
if (def.displayInfoId == 0 && itTpl->second.displayId != 0) {
def.displayInfoId = itTpl->second.displayId;
}
if (def.armor == 0 && itTpl->second.armor > 0) def.armor = itTpl->second.armor;
if (def.stamina == 0 && itTpl->second.stamina > 0) def.stamina = itTpl->second.stamina;
if (def.strength == 0 && itTpl->second.strength > 0) def.strength = itTpl->second.strength;
if (def.agility == 0 && itTpl->second.agility > 0) def.agility = itTpl->second.agility;
if (def.intellect == 0 && itTpl->second.intellect > 0) def.intellect = itTpl->second.intellect;
if (def.spirit == 0 && itTpl->second.spirit > 0) def.spirit = itTpl->second.spirit;
}
}
inventory.setBackpackSlot(i, def);
}
@ -3846,6 +3909,9 @@ void GameHandler::lootItem(uint8_t slotIndex) {
void GameHandler::closeLoot() {
if (!lootWindowOpen) return;
lootWindowOpen = false;
if (currentLoot.lootGuid != 0 && targetGuid == currentLoot.lootGuid) {
clearTarget();
}
if (singlePlayerMode_ && currentLoot.lootGuid != 0) {
auto st = localLootState_.find(currentLoot.lootGuid);
if (st != localLootState_.end()) {
@ -3987,8 +4053,13 @@ void GameHandler::sellItemBySlot(int backpackIndex) {
notifyInventoryChanged();
} else {
uint64_t itemGuid = backpackSlotGuids_[backpackIndex];
if (itemGuid == 0) {
itemGuid = resolveOnlineItemGuid(slot.item.itemId);
}
if (itemGuid != 0 && currentVendorItems.vendorGuid != 0) {
sellItem(currentVendorItems.vendorGuid, itemGuid, 1);
} else if (itemGuid == 0) {
LOG_WARNING("Sell failed: missing item GUID for slot ", backpackIndex);
}
}
}
@ -4004,9 +4075,14 @@ void GameHandler::autoEquipItemBySlot(int backpackIndex) {
}
uint64_t itemGuid = backpackSlotGuids_[backpackIndex];
if (itemGuid == 0) {
itemGuid = resolveOnlineItemGuid(slot.item.itemId);
}
if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) {
auto packet = AutoEquipItemPacket::build(itemGuid);
socket->send(packet);
} else if (itemGuid == 0) {
LOG_WARNING("Auto-equip failed: missing item GUID for slot ", backpackIndex);
}
}
@ -4021,9 +4097,14 @@ void GameHandler::useItemBySlot(int backpackIndex) {
}
uint64_t itemGuid = backpackSlotGuids_[backpackIndex];
if (itemGuid == 0) {
itemGuid = resolveOnlineItemGuid(slot.item.itemId);
}
if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) {
auto packet = UseItemPacket::build(0xFF, static_cast<uint8_t>(backpackIndex), itemGuid);
socket->send(packet);
} else if (itemGuid == 0) {
LOG_WARNING("Use item failed: missing item GUID for slot ", backpackIndex);
}
}

View file

@ -661,7 +661,7 @@ void Renderer::updateCharacterAnimation() {
} else if (anyStrafeRight) {
animId = pickFirstAvailable({ANIM_STRAFE_WALK_RIGHT, ANIM_STRAFE_RUN_RIGHT}, ANIM_WALK);
} else {
animId = ANIM_WALK;
animId = pickFirstAvailable({ANIM_WALK, ANIM_RUN}, ANIM_STAND);
}
loop = true;
break;
@ -673,7 +673,7 @@ void Renderer::updateCharacterAnimation() {
} else if (anyStrafeRight) {
animId = pickFirstAvailable({ANIM_STRAFE_RUN_RIGHT}, ANIM_RUN);
} else {
animId = ANIM_RUN;
animId = pickFirstAvailable({ANIM_RUN, ANIM_WALK}, ANIM_STAND);
}
loop = true;
break;
@ -1622,11 +1622,11 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
}
}
LOG_INFO("Loading initial tile [", tileX, ",", tileY, "] via terrain manager");
LOG_INFO("Enqueuing initial tile [", tileX, ",", tileY, "] via terrain manager");
// Load the initial tile through TerrainManager (properly tracked for streaming)
if (!terrainManager->loadTile(tileX, tileY)) {
LOG_ERROR("Failed to load initial tile [", tileX, ",", tileY, "]");
// Enqueue the initial tile for async loading (avoids long sync stalls)
if (!terrainManager->enqueueTile(tileX, tileY)) {
LOG_ERROR("Failed to enqueue initial tile [", tileX, ",", tileY, "]");
return false;
}

View file

@ -194,6 +194,27 @@ bool TerrainManager::loadTile(int x, int y) {
return true;
}
bool TerrainManager::enqueueTile(int x, int y) {
TileCoord coord = {x, y};
if (loadedTiles.find(coord) != loadedTiles.end()) {
return true;
}
if (pendingTiles.find(coord) != pendingTiles.end()) {
return true;
}
if (failedTiles.find(coord) != failedTiles.end()) {
return false;
}
{
std::lock_guard<std::mutex> lock(queueMutex);
loadQueue.push(coord);
pendingTiles[coord] = true;
}
queueCV.notify_all();
return true;
}
std::unique_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
TileCoord coord = {x, y};

View file

@ -1097,15 +1097,18 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
inventoryDirty = true;
}
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
bool looksEquipable = (item.inventoryType > 0) ||
(item.armor > 0) ||
(!item.subclassName.empty());
if (gameHandler_ && !gameHandler_->isSinglePlayerMode()) {
if (item.inventoryType > 0) {
if (looksEquipable) {
// Auto-equip (online)
gameHandler_->autoEquipItemBySlot(backpackIndex);
} else {
// Use consumable (online)
gameHandler_->useItemBySlot(backpackIndex);
}
} else if (item.inventoryType > 0) {
} else if (looksEquipable) {
// Auto-equip (single-player)
uint8_t equippingType = item.inventoryType;
game::EquipSlot targetSlot = getEquipSlotForType(equippingType, inventory);