Kelsidavis-WoWee/src/game/inventory.cpp
Kelsi c18720f0f0 feat: server-synced bag sort, fix world map continent bounds, update docs
Inventory sort: clicking "Sort Bags" now generates CMSG_SWAP_ITEM packets
to move items server-side (one swap per frame to avoid race conditions).
Client-side sort runs immediately for visual preview; server swaps follow.
New Inventory::computeSortSwaps() computes minimal swap sequence using
selection-sort permutation on quality→itemId→stackCount comparator.

World map: fix continent bounds derivation that used intersection (max/min)
instead of union (min/max) of child zone bounds, causing continent views
to display zoomed-in/clipped.

Update README.md and docs/status.md with current features, release info,
and known gaps (v1.8.2-preview, 664 opcode handlers, NPC voices, bag
independence, CharSections auto-detect, quest GO server limitation).
2026-03-24 09:24:09 -07:00

494 lines
17 KiB
C++

#include "game/inventory.hpp"
#include "core/logger.hpp"
#include <algorithm>
namespace wowee {
namespace game {
static const ItemSlot EMPTY_SLOT{};
Inventory::Inventory() = default;
const ItemSlot& Inventory::getBackpackSlot(int index) const {
if (index < 0 || index >= BACKPACK_SLOTS) return EMPTY_SLOT;
return backpack[index];
}
bool Inventory::setBackpackSlot(int index, const ItemDef& item) {
if (index < 0 || index >= BACKPACK_SLOTS) return false;
backpack[index].item = item;
return true;
}
bool Inventory::clearBackpackSlot(int index) {
if (index < 0 || index >= BACKPACK_SLOTS) return false;
backpack[index].item = ItemDef{};
return true;
}
const ItemSlot& Inventory::getEquipSlot(EquipSlot slot) const {
int idx = static_cast<int>(slot);
if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return EMPTY_SLOT;
return equipment[idx];
}
bool Inventory::setEquipSlot(EquipSlot slot, const ItemDef& item) {
int idx = static_cast<int>(slot);
if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return false;
equipment[idx].item = item;
return true;
}
bool Inventory::clearEquipSlot(EquipSlot slot) {
int idx = static_cast<int>(slot);
if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return false;
equipment[idx].item = ItemDef{};
return true;
}
const ItemSlot& Inventory::getKeyringSlot(int index) const {
if (index < 0 || index >= KEYRING_SLOTS) return EMPTY_SLOT;
return keyring_[index];
}
bool Inventory::setKeyringSlot(int index, const ItemDef& item) {
if (index < 0 || index >= KEYRING_SLOTS) return false;
keyring_[index].item = item;
return true;
}
bool Inventory::clearKeyringSlot(int index) {
if (index < 0 || index >= KEYRING_SLOTS) return false;
keyring_[index].item = ItemDef{};
return true;
}
int Inventory::getBagSize(int bagIndex) const {
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return 0;
return bags[bagIndex].size;
}
void Inventory::setBagSize(int bagIndex, int size) {
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return;
bags[bagIndex].size = std::min(size, MAX_BAG_SIZE);
}
const ItemSlot& Inventory::getBagSlot(int bagIndex, int slotIndex) const {
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return EMPTY_SLOT;
if (slotIndex < 0 || slotIndex >= bags[bagIndex].size) return EMPTY_SLOT;
return bags[bagIndex].slots[slotIndex];
}
bool Inventory::setBagSlot(int bagIndex, int slotIndex, const ItemDef& item) {
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return false;
if (slotIndex < 0 || slotIndex >= bags[bagIndex].size) return false;
bags[bagIndex].slots[slotIndex].item = item;
return true;
}
bool Inventory::clearBagSlot(int bagIndex, int slotIndex) {
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return false;
if (slotIndex < 0 || slotIndex >= bags[bagIndex].size) return false;
bags[bagIndex].slots[slotIndex].item = ItemDef{};
return true;
}
const ItemSlot& Inventory::getBankSlot(int index) const {
if (index < 0 || index >= BANK_SLOTS) return EMPTY_SLOT;
return bankSlots_[index];
}
bool Inventory::setBankSlot(int index, const ItemDef& item) {
if (index < 0 || index >= BANK_SLOTS) return false;
bankSlots_[index].item = item;
return true;
}
bool Inventory::clearBankSlot(int index) {
if (index < 0 || index >= BANK_SLOTS) return false;
bankSlots_[index].item = ItemDef{};
return true;
}
const ItemSlot& Inventory::getBankBagSlot(int bagIndex, int slotIndex) const {
if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return EMPTY_SLOT;
if (slotIndex < 0 || slotIndex >= bankBags_[bagIndex].size) return EMPTY_SLOT;
return bankBags_[bagIndex].slots[slotIndex];
}
bool Inventory::setBankBagSlot(int bagIndex, int slotIndex, const ItemDef& item) {
if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return false;
if (slotIndex < 0 || slotIndex >= bankBags_[bagIndex].size) return false;
bankBags_[bagIndex].slots[slotIndex].item = 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;
}
void Inventory::setBankBagSize(int bagIndex, int size) {
if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return;
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;
std::swap(bags[bagA], bags[bagB]);
}
int Inventory::findFreeBackpackSlot() const {
for (int i = 0; i < BACKPACK_SLOTS; i++) {
if (backpack[i].empty()) return i;
}
return -1;
}
bool Inventory::addItem(const ItemDef& item) {
// Try stacking first
if (item.maxStack > 1) {
for (int i = 0; i < BACKPACK_SLOTS; i++) {
if (!backpack[i].empty() &&
backpack[i].item.itemId == item.itemId &&
backpack[i].item.stackCount < backpack[i].item.maxStack) {
uint32_t space = backpack[i].item.maxStack - backpack[i].item.stackCount;
uint32_t toAdd = std::min(space, item.stackCount);
backpack[i].item.stackCount += toAdd;
if (toAdd >= item.stackCount) return true;
// Remaining needs a new slot — fall through
}
}
}
int slot = findFreeBackpackSlot();
if (slot < 0) return false;
backpack[slot].item = item;
return true;
}
void Inventory::sortBags() {
// Collect all items from backpack and equip bags into a flat list.
std::vector<ItemDef> items;
items.reserve(BACKPACK_SLOTS + NUM_BAG_SLOTS * MAX_BAG_SIZE);
for (int i = 0; i < BACKPACK_SLOTS; ++i) {
if (!backpack[i].empty())
items.push_back(backpack[i].item);
}
for (int b = 0; b < NUM_BAG_SLOTS; ++b) {
for (int s = 0; s < bags[b].size; ++s) {
if (!bags[b].slots[s].empty())
items.push_back(bags[b].slots[s].item);
}
}
// Sort: quality descending → itemId ascending → stackCount descending.
std::stable_sort(items.begin(), items.end(), [](const ItemDef& a, const ItemDef& b) {
if (a.quality != b.quality)
return static_cast<int>(a.quality) > static_cast<int>(b.quality);
if (a.itemId != b.itemId)
return a.itemId < b.itemId;
return a.stackCount > b.stackCount;
});
// Write sorted items back, filling backpack first then equip bags.
int idx = 0;
int n = static_cast<int>(items.size());
for (int i = 0; i < BACKPACK_SLOTS; ++i)
backpack[i].item = (idx < n) ? items[idx++] : ItemDef{};
for (int b = 0; b < NUM_BAG_SLOTS; ++b) {
for (int s = 0; s < bags[b].size; ++s)
bags[b].slots[s].item = (idx < n) ? items[idx++] : ItemDef{};
}
}
std::vector<Inventory::SwapOp> Inventory::computeSortSwaps() const {
// Build a flat list of (bag, slot, item) entries matching the same traversal
// order as sortBags(): backpack first, then equip bags in order.
struct Entry {
uint8_t bag; // WoW bag address: 0xFF=backpack, 19+i=equip bag i
uint8_t slot; // WoW slot address: 23+i for backpack, slotIndex for bags
uint32_t itemId;
ItemQuality quality;
uint32_t stackCount;
};
std::vector<Entry> entries;
entries.reserve(BACKPACK_SLOTS + NUM_BAG_SLOTS * MAX_BAG_SIZE);
for (int i = 0; i < BACKPACK_SLOTS; ++i) {
entries.push_back({0xFF, static_cast<uint8_t>(23 + i),
backpack[i].item.itemId, backpack[i].item.quality,
backpack[i].item.stackCount});
}
for (int b = 0; b < NUM_BAG_SLOTS; ++b) {
for (int s = 0; s < bags[b].size; ++s) {
entries.push_back({static_cast<uint8_t>(19 + b), static_cast<uint8_t>(s),
bags[b].slots[s].item.itemId, bags[b].slots[s].item.quality,
bags[b].slots[s].item.stackCount});
}
}
// Build a sorted index array using the same comparator as sortBags().
int n = static_cast<int>(entries.size());
std::vector<int> sortedIdx(n);
for (int i = 0; i < n; ++i) sortedIdx[i] = i;
// Separate non-empty items and empty slots, then sort the non-empty items.
// Items are sorted by quality desc -> itemId asc -> stackCount desc.
// Empty slots go to the end.
std::stable_sort(sortedIdx.begin(), sortedIdx.end(), [&](int a, int b) {
bool aEmpty = (entries[a].itemId == 0);
bool bEmpty = (entries[b].itemId == 0);
if (aEmpty != bEmpty) return bEmpty; // non-empty before empty
if (aEmpty) return false; // both empty: preserve order
// Both non-empty: same comparator as sortBags()
if (entries[a].quality != entries[b].quality)
return static_cast<int>(entries[a].quality) > static_cast<int>(entries[b].quality);
if (entries[a].itemId != entries[b].itemId)
return entries[a].itemId < entries[b].itemId;
return entries[a].stackCount > entries[b].stackCount;
});
// sortedIdx[targetPos] = sourcePos means the item currently at sourcePos
// needs to end up at targetPos. We use selection-sort-style swaps to
// permute current positions into sorted order, tracking where items move.
// posOf[i] = current position of the item that was originally at index i
std::vector<int> posOf(n);
for (int i = 0; i < n; ++i) posOf[i] = i;
// invPos[p] = which original item index is currently sitting at position p
std::vector<int> invPos(n);
for (int i = 0; i < n; ++i) invPos[i] = i;
std::vector<SwapOp> swaps;
for (int target = 0; target < n; ++target) {
int need = sortedIdx[target]; // original index that should be at 'target'
int cur = invPos[target]; // original index currently at 'target'
if (cur == need) continue; // already in place
// Skip swaps between two empty slots
if (entries[cur].itemId == 0 && entries[need].itemId == 0) continue;
int srcPos = posOf[need]; // current position of the item we need
// Emit a swap between position srcPos and position target
swaps.push_back({entries[srcPos].bag, entries[srcPos].slot,
entries[target].bag, entries[target].slot});
// Update tracking arrays
posOf[cur] = srcPos;
posOf[need] = target;
invPos[srcPos] = cur;
invPos[target] = need;
}
return swaps;
}
void Inventory::populateTestItems() {
// Equipment
{
ItemDef sword;
sword.itemId = 25;
sword.name = "Worn Shortsword";
sword.quality = ItemQuality::COMMON;
sword.inventoryType = 21; // Main Hand
sword.strength = 1;
sword.displayInfoId = 1542; // Sword_1H_Short_A_02.m2
sword.subclassName = "Sword";
setEquipSlot(EquipSlot::MAIN_HAND, sword);
}
{
ItemDef shield;
shield.itemId = 2129;
shield.name = "Large Round Shield";
shield.quality = ItemQuality::COMMON;
shield.inventoryType = 14; // Off Hand (Shield)
shield.armor = 18;
shield.stamina = 1;
shield.displayInfoId = 18662; // Shield_Round_A_01.m2
shield.subclassName = "Shield";
setEquipSlot(EquipSlot::OFF_HAND, shield);
}
// Shirt/pants/boots in backpack (character model renders bare geosets)
{
ItemDef shirt;
shirt.itemId = 38;
shirt.name = "Recruit's Shirt";
shirt.quality = ItemQuality::COMMON;
shirt.inventoryType = 4; // Shirt
shirt.displayInfoId = 2163;
addItem(shirt);
}
{
ItemDef pants;
pants.itemId = 39;
pants.name = "Recruit's Pants";
pants.quality = ItemQuality::COMMON;
pants.inventoryType = 7; // Legs
pants.armor = 4;
pants.displayInfoId = 1883;
addItem(pants);
}
{
ItemDef boots;
boots.itemId = 40;
boots.name = "Recruit's Boots";
boots.quality = ItemQuality::COMMON;
boots.inventoryType = 8; // Feet
boots.armor = 3;
boots.displayInfoId = 2166;
addItem(boots);
}
// Backpack items
{
ItemDef potion;
potion.itemId = 118;
potion.name = "Minor Healing Potion";
potion.quality = ItemQuality::COMMON;
potion.stackCount = 3;
potion.maxStack = 5;
addItem(potion);
}
{
ItemDef hearthstone;
hearthstone.itemId = 6948;
hearthstone.name = "Hearthstone";
hearthstone.quality = ItemQuality::COMMON;
addItem(hearthstone);
}
{
ItemDef leather;
leather.itemId = 2318;
leather.name = "Light Leather";
leather.quality = ItemQuality::COMMON;
leather.stackCount = 5;
leather.maxStack = 20;
addItem(leather);
}
{
ItemDef cloth;
cloth.itemId = 2589;
cloth.name = "Linen Cloth";
cloth.quality = ItemQuality::COMMON;
cloth.stackCount = 8;
cloth.maxStack = 20;
addItem(cloth);
}
{
ItemDef quest;
quest.itemId = 50000;
quest.name = "Kobold Candle";
quest.quality = ItemQuality::COMMON;
quest.stackCount = 4;
quest.maxStack = 10;
addItem(quest);
}
{
ItemDef ring;
ring.itemId = 11287;
ring.name = "Verdant Ring";
ring.quality = ItemQuality::UNCOMMON;
ring.inventoryType = 11; // Ring
ring.stamina = 3;
ring.spirit = 2;
addItem(ring);
}
{
ItemDef cloak;
cloak.itemId = 2570;
cloak.name = "Linen Cloak";
cloak.quality = ItemQuality::UNCOMMON;
cloak.inventoryType = 16; // Back
cloak.armor = 10;
cloak.agility = 1;
cloak.displayInfoId = 15055;
addItem(cloak);
}
{
ItemDef rareAxe;
rareAxe.itemId = 15268;
rareAxe.name = "Stoneslayer";
rareAxe.quality = ItemQuality::RARE;
rareAxe.inventoryType = 17; // Two-Hand
rareAxe.strength = 8;
rareAxe.stamina = 7;
rareAxe.subclassName = "Axe";
rareAxe.displayInfoId = 782; // Axe_2H_Battle_B_01.m2
addItem(rareAxe);
}
LOG_INFO("Inventory: populated test items (2 equipped, 11 backpack)");
}
const char* getQualityName(ItemQuality quality) {
switch (quality) {
case ItemQuality::POOR: return "Poor";
case ItemQuality::COMMON: return "Common";
case ItemQuality::UNCOMMON: return "Uncommon";
case ItemQuality::RARE: return "Rare";
case ItemQuality::EPIC: return "Epic";
case ItemQuality::LEGENDARY: return "Legendary";
case ItemQuality::ARTIFACT: return "Artifact";
case ItemQuality::HEIRLOOM: return "Heirloom";
default: return "Unknown";
}
}
const char* getEquipSlotName(EquipSlot slot) {
switch (slot) {
case EquipSlot::HEAD: return "Head";
case EquipSlot::NECK: return "Neck";
case EquipSlot::SHOULDERS: return "Shoulders";
case EquipSlot::SHIRT: return "Shirt";
case EquipSlot::CHEST: return "Chest";
case EquipSlot::WAIST: return "Waist";
case EquipSlot::LEGS: return "Legs";
case EquipSlot::FEET: return "Feet";
case EquipSlot::WRISTS: return "Wrists";
case EquipSlot::HANDS: return "Hands";
case EquipSlot::RING1: return "Ring 1";
case EquipSlot::RING2: return "Ring 2";
case EquipSlot::TRINKET1: return "Trinket 1";
case EquipSlot::TRINKET2: return "Trinket 2";
case EquipSlot::BACK: return "Back";
case EquipSlot::MAIN_HAND: return "Main Hand";
case EquipSlot::OFF_HAND: return "Off Hand";
case EquipSlot::RANGED: return "Ranged";
case EquipSlot::TABARD: return "Tabard";
case EquipSlot::BAG1: return "Bag 1";
case EquipSlot::BAG2: return "Bag 2";
case EquipSlot::BAG3: return "Bag 3";
case EquipSlot::BAG4: return "Bag 4";
default: return "Unknown";
}
}
} // namespace game
} // namespace wowee