mirror of
https://github.com/brues-code/nampower.git
synced 2026-10-10 15:40:02 +00:00
640 lines
24 KiB
C++
640 lines
24 KiB
C++
//
|
|
// Created by pmacc on 9/21/2024.
|
|
//
|
|
|
|
#include "items.hpp"
|
|
#include "offsets.hpp"
|
|
#include "logging.hpp"
|
|
#include "helper.hpp"
|
|
|
|
#include <cctype>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
|
|
#include "dbc_fields.hpp"
|
|
|
|
namespace Nampower {
|
|
// Item data caches for change detection
|
|
std::array<std::array<CachedItemData, MAX_BAG_SLOTS>, MAX_BAGS> bagItemDataCache{};
|
|
std::array<CachedItemData, MAX_EQUIPPED_SLOTS> equippedItemDataCache{};
|
|
std::array<std::array<CachedTrinketData, MAX_BAG_SLOTS>, MAX_BAGS> bagTrinketDataCache{};
|
|
std::array<CachedTrinketData, MAX_EQUIPPED_SLOTS> equippedTrinketDataCache{};
|
|
|
|
// Global dictionary to store itemId -> ItemStats_C mappings
|
|
static std::unordered_map<uint32_t, game::ItemStats_C *> itemStatsCache;
|
|
|
|
// Local cache for item name lookups
|
|
static std::unordered_map<std::string, uint32_t> itemNameToIdCache;
|
|
|
|
// Track pending async loads (can have multiple at once)
|
|
static std::unordered_set<uint32_t> pendingItemIds;
|
|
|
|
// Export state tracking
|
|
static bool isExporting = false;
|
|
static uint32_t currentExportItemId = 0;
|
|
static constexpr uint32_t MAX_EXPORT_ITEM_ID = 100000;
|
|
static constexpr uint32_t ITEMS_PER_FRAME = 20;
|
|
|
|
auto const getRow = reinterpret_cast<DBCache_ItemCacheDBGetRowT>(Offsets::DBCache_ItemCacheDBGetRow);
|
|
auto const itemCache = reinterpret_cast<void *>(Offsets::ItemDBCache);
|
|
|
|
void ClearItemCaches() {
|
|
// Clear all item data caches (called on UI reload)
|
|
for (auto& bagCache : bagItemDataCache) {
|
|
bagCache.fill(CachedItemData());
|
|
}
|
|
equippedItemDataCache.fill(CachedItemData());
|
|
for (auto& bagCache : bagTrinketDataCache) {
|
|
bagCache.fill(CachedTrinketData());
|
|
}
|
|
equippedTrinketDataCache.fill(CachedTrinketData());
|
|
}
|
|
|
|
std::string ToLowerCase(const char *str) {
|
|
std::string result;
|
|
if (!str) return result;
|
|
|
|
while (*str) {
|
|
result += static_cast<char>(tolower(*str));
|
|
++str;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
uint32_t GetItemIdFromCache(const char *itemName) {
|
|
if (!itemName) return 0;
|
|
|
|
std::string lowerName = ToLowerCase(itemName);
|
|
auto it = itemNameToIdCache.find(lowerName);
|
|
if (it != itemNameToIdCache.end()) {
|
|
return it->second;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void CacheItemNameToId(const char *itemName, uint32_t itemId) {
|
|
if (!itemName || itemId == 0) return;
|
|
|
|
std::string lowerName = ToLowerCase(itemName);
|
|
itemNameToIdCache[lowerName] = itemId;
|
|
}
|
|
|
|
bool DoesItemMatch(uint32_t itemId, uint32_t searchItemId, const char *searchItemName) {
|
|
if (searchItemId != 0) {
|
|
return itemId == searchItemId;
|
|
}
|
|
|
|
if (searchItemName) {
|
|
auto itemStats = GetItemStats(itemId);
|
|
if (itemStats && itemStats->m_displayName[0]) {
|
|
bool matches = _stricmp(itemStats->m_displayName[0], searchItemName) == 0;
|
|
if (matches) {
|
|
CacheItemNameToId(searchItemName, itemId);
|
|
}
|
|
return matches;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
uintptr_t *GetBagPtrFromContainer(uintptr_t *containerPtr) {
|
|
auto vftable = game::GetObjectVFTable(containerPtr);
|
|
using GetBagPtrT = uintptr_t * (__thiscall *)(uintptr_t *);
|
|
auto getBagPtrFunc = reinterpret_cast<GetBagPtrT>(vftable[4]);
|
|
return getBagPtrFunc(containerPtr);
|
|
}
|
|
|
|
PlayerItemSearchResult FindPlayerItem(uint32_t searchItemId, const char *searchItemName) {
|
|
PlayerItemSearchResult result{};
|
|
|
|
auto const getBagItem = reinterpret_cast<CGBag_C_GetItemAtSlotT>(Offsets::CGBag_C_GetItemAtSlot);
|
|
auto const getContainerGuid = reinterpret_cast<GetContainerGuidT>(Offsets::GetContainerGuid);
|
|
|
|
auto playerGuid = game::ClntObjMgrGetActivePlayerGuid();
|
|
auto playerUnit = game::GetObjectPtr(playerGuid);
|
|
if (!playerUnit) {
|
|
return result;
|
|
}
|
|
|
|
auto inventory = game::GetPlayerInventoryPtr(playerUnit);
|
|
|
|
auto matchesItem = [&](game::CGItem_C *item) {
|
|
return item && DoesItemMatch(game::GetItemId(item), searchItemId, searchItemName);
|
|
};
|
|
|
|
for (uint32_t slot = 0; slot <= 18; slot++) {
|
|
auto item = getBagItem(inventory, slot);
|
|
if (matchesItem(item)) {
|
|
result.item = item;
|
|
result.bagIndex = EQUIPPED_BAG_INDEX;
|
|
result.slot = slot;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
for (uint32_t slot = 23; slot <= 38; slot++) {
|
|
auto item = getBagItem(inventory, slot);
|
|
if (matchesItem(item)) {
|
|
result.item = item;
|
|
result.bagIndex = 0;
|
|
result.slot = slot;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
for (int32_t bagIndex = 1; bagIndex <= 4; bagIndex++) {
|
|
uint64_t containerGuid = getContainerGuid(bagIndex - 1);
|
|
if (containerGuid == 0) continue;
|
|
|
|
auto containerPtr = game::ClntObjMgrObjectPtr(game::TYPEMASK_CONTAINER, containerGuid);
|
|
if (!containerPtr) continue;
|
|
|
|
auto bagPtr = GetBagPtrFromContainer(containerPtr);
|
|
if (!bagPtr) continue;
|
|
|
|
auto bagSize = *bagPtr;
|
|
for (uint32_t slot = 0; slot < bagSize; slot++) {
|
|
auto item = getBagItem(bagPtr, slot);
|
|
if (matchesItem(item)) {
|
|
result.item = item;
|
|
result.bagIndex = bagIndex;
|
|
result.slot = slot;
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
uint64_t bankGuid = *reinterpret_cast<uint64_t *>(Offsets::BankGuid);
|
|
if (bankGuid > 0) {
|
|
for (uint32_t slot = 39; slot <= 62; slot++) {
|
|
auto item = getBagItem(inventory, slot);
|
|
if (matchesItem(item)) {
|
|
result.item = item;
|
|
result.bagIndex = BANK_BAG_INDEX;
|
|
result.slot = slot;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
for (int32_t bagIndex = 5; bagIndex <= 9; bagIndex++) {
|
|
uint64_t containerGuid = getContainerGuid(bagIndex - 1); // bagIndex 5-9 maps to container 4-8
|
|
if (containerGuid == 0) continue;
|
|
|
|
auto containerPtr = game::ClntObjMgrObjectPtr(game::TYPEMASK_CONTAINER, containerGuid);
|
|
if (!containerPtr) continue;
|
|
|
|
auto bagPtr = GetBagPtrFromContainer(containerPtr);
|
|
if (!bagPtr) continue;
|
|
|
|
auto bagSize = *bagPtr;
|
|
for (uint32_t slot = 0; slot < bagSize; slot++) {
|
|
auto item = getBagItem(bagPtr, slot);
|
|
if (matchesItem(item)) {
|
|
result.item = item;
|
|
result.bagIndex = bagIndex;
|
|
result.slot = slot;
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (uint32_t slot = 81; slot <= 96; slot++) {
|
|
auto item = getBagItem(inventory, slot);
|
|
if (matchesItem(item)) {
|
|
result.item = item;
|
|
result.bagIndex = KEYRING_BAG_INDEX;
|
|
result.slot = slot;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
PlayerItemSearchResult FindPlayerDisenchantItem(uint32_t qualityBitmask, bool includeSoulbound) {
|
|
PlayerItemSearchResult result{};
|
|
|
|
auto const getBagItem = reinterpret_cast<CGBag_C_GetItemAtSlotT>(Offsets::CGBag_C_GetItemAtSlot);
|
|
auto const getContainerGuid = reinterpret_cast<GetContainerGuidT>(Offsets::GetContainerGuid);
|
|
|
|
auto playerGuid = game::ClntObjMgrGetActivePlayerGuid();
|
|
auto playerUnit = game::GetObjectPtr(playerGuid);
|
|
if (!playerUnit) {
|
|
return result;
|
|
}
|
|
|
|
auto inventory = game::GetPlayerInventoryPtr(playerUnit);
|
|
|
|
auto const itemIsQuestOrSoulbound = reinterpret_cast<CGItem_C_ItemIsQuestOrSoulboundT>(
|
|
Offsets::CGItem_C_ItemIsQuestOrSoulbound);
|
|
|
|
auto matchesItem = [&](game::CGItem_C *item) {
|
|
if (!item) {
|
|
return false;
|
|
}
|
|
|
|
uint32_t itemId = game::GetItemId(item);
|
|
auto *itemStats = GetItemStats(itemId);
|
|
if (!itemStats) {
|
|
return false;
|
|
}
|
|
|
|
// Always skip quest items (check by class or startQuestID)
|
|
if (itemStats->m_class == game::ITEM_CLASS_QUEST || itemStats->m_startQuestID > 0) {
|
|
return false;
|
|
}
|
|
|
|
// Skip soulbound items only if includeSoulbound is false
|
|
if (!includeSoulbound && itemIsQuestOrSoulbound(item)) {
|
|
return false;
|
|
}
|
|
|
|
// Check if item is weapon or armor
|
|
if (itemStats->m_class != game::ITEM_CLASS_WEAPON &&
|
|
itemStats->m_class != game::ITEM_CLASS_ARMOR) {
|
|
return false;
|
|
}
|
|
|
|
// Check if item quality matches the bitmask
|
|
uint32_t qualityBit = 0;
|
|
if (itemStats->m_quality == game::ITEM_QUALITY_UNCOMMON) {
|
|
qualityBit = 0x01; // Green
|
|
} else if (itemStats->m_quality == game::ITEM_QUALITY_RARE) {
|
|
qualityBit = 0x02; // Blue
|
|
} else if (itemStats->m_quality == game::ITEM_QUALITY_EPIC) {
|
|
qualityBit = 0x04; // Purple
|
|
}
|
|
|
|
if ((qualityBitmask & qualityBit) == 0) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// Skip equipped gear slots (0-18) - only search bags
|
|
|
|
for (uint32_t slot = 23; slot <= 38; slot++) {
|
|
auto item = getBagItem(inventory, slot);
|
|
if (matchesItem(item)) {
|
|
result.item = item;
|
|
result.bagIndex = 0;
|
|
result.slot = slot;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
for (int32_t bagIndex = 1; bagIndex <= 4; bagIndex++) {
|
|
uint64_t containerGuid = getContainerGuid(bagIndex - 1);
|
|
if (containerGuid == 0) continue;
|
|
|
|
auto containerPtr = game::ClntObjMgrObjectPtr(game::TYPEMASK_CONTAINER, containerGuid);
|
|
if (!containerPtr) continue;
|
|
|
|
auto bagPtr = GetBagPtrFromContainer(containerPtr);
|
|
if (!bagPtr) continue;
|
|
|
|
auto bagSize = *bagPtr;
|
|
for (uint32_t slot = 0; slot < bagSize; slot++) {
|
|
auto item = getBagItem(bagPtr, slot);
|
|
if (matchesItem(item)) {
|
|
result.item = item;
|
|
result.bagIndex = bagIndex;
|
|
result.slot = slot;
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
// don't search bank
|
|
|
|
return result;
|
|
}
|
|
|
|
std::string escapeJsonString(const char *str) {
|
|
if (!str) return "null";
|
|
|
|
std::string result = "\"";
|
|
for (const char *p = str; *p; ++p) {
|
|
switch (*p) {
|
|
case '"':
|
|
result += "\\\"";
|
|
break;
|
|
case '\\':
|
|
result += "\\\\";
|
|
break;
|
|
case '\b':
|
|
result += "\\b";
|
|
break;
|
|
case '\f':
|
|
result += "\\f";
|
|
break;
|
|
case '\n':
|
|
result += "\\n";
|
|
break;
|
|
case '\r':
|
|
result += "\\r";
|
|
break;
|
|
case '\t':
|
|
result += "\\t";
|
|
break;
|
|
default:
|
|
if (static_cast<unsigned char>(*p) < 0x20) {
|
|
char buf[8];
|
|
snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned char>(*p));
|
|
result += buf;
|
|
} else {
|
|
result += *p;
|
|
}
|
|
}
|
|
}
|
|
result += "\"";
|
|
return result;
|
|
}
|
|
|
|
// Callback function for DBCache
|
|
void __fastcall ItemStatsCallback(uint32_t itemId, const uint64_t *ownerGuid, void *userData, bool resultFlag) {
|
|
if (resultFlag) {
|
|
uint64_t guid;
|
|
auto itemStats = getRow(itemCache, itemId, &guid, &ItemStatsCallback, nullptr, false);
|
|
if (itemStats) {
|
|
// Store in cache
|
|
DEBUG_LOG("Added itemId " << itemId);
|
|
itemStatsCache[itemId] = reinterpret_cast<game::ItemStats_C *>(itemStats);
|
|
} else {
|
|
DEBUG_LOG("Still missing " << itemId);
|
|
}
|
|
}
|
|
|
|
// Remove from pending set
|
|
pendingItemIds.erase(itemId);
|
|
}
|
|
|
|
bool LoadItem(uint32_t itemId) {
|
|
// Check if already in cache
|
|
auto it = itemStatsCache.find(itemId);
|
|
if (it != itemStatsCache.end()) {
|
|
return false; // Already loaded, no wait needed
|
|
}
|
|
|
|
uint64_t guid;
|
|
auto itemStats = getRow(itemCache, itemId, &guid, &ItemStatsCallback, nullptr, true);
|
|
if (itemStats) {
|
|
// Item was available immediately, store in cache
|
|
itemStatsCache[itemId] = reinterpret_cast<game::ItemStats_C *>(itemStats);
|
|
return false; // Loaded immediately, no wait needed
|
|
} else {
|
|
// Item not immediately available, need to wait for callback
|
|
pendingItemIds.insert(itemId);
|
|
return true; // Caller should wait for callback
|
|
}
|
|
}
|
|
|
|
bool ProcessItemExport() {
|
|
if (!isExporting) {
|
|
return false; // Not currently exporting
|
|
}
|
|
|
|
// Try to load items until we have 20 pending or run out of items
|
|
uint32_t itemsRequestedThisFrame = 0;
|
|
while (pendingItemIds.size() < ITEMS_PER_FRAME && currentExportItemId <= MAX_EXPORT_ITEM_ID) {
|
|
LoadItem(currentExportItemId);
|
|
|
|
// Log progress every 1000 items
|
|
if (currentExportItemId % 1000 == 0) {
|
|
DEBUG_LOG("Progress: " << currentExportItemId << " / " << MAX_EXPORT_ITEM_ID
|
|
<< " items requested, " << pendingItemIds.size() << " pending callbacks");
|
|
}
|
|
|
|
currentExportItemId++;
|
|
itemsRequestedThisFrame++;
|
|
|
|
// Safety check - don't request too many in a single frame
|
|
if (itemsRequestedThisFrame >= ITEMS_PER_FRAME) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check if we're done (all items requested and no pending callbacks)
|
|
if (currentExportItemId > MAX_EXPORT_ITEM_ID && pendingItemIds.empty()) {
|
|
// Fall through to export completion
|
|
} else {
|
|
return true; // Still exporting, more items to process or waiting for callbacks
|
|
}
|
|
|
|
// Export complete, write to file
|
|
DEBUG_LOG("Finished fetching items, found " << itemStatsCache.size() << " valid items");
|
|
DEBUG_LOG("Writing items to items.json");
|
|
|
|
// Write all cached items to JSON file (overwrite mode)
|
|
try {
|
|
std::ofstream jsonFile("items.json", std::ios::trunc);
|
|
if (!jsonFile.is_open()) {
|
|
DEBUG_LOG("Failed to open items.json for writing");
|
|
isExporting = false;
|
|
return false;
|
|
}
|
|
|
|
jsonFile << "[\n";
|
|
|
|
bool first = true;
|
|
for (const auto &pair: itemStatsCache) {
|
|
if (!first) {
|
|
jsonFile << ",\n";
|
|
}
|
|
first = false;
|
|
|
|
uint32_t itemId = pair.first;
|
|
game::ItemStats_C *item = pair.second;
|
|
|
|
if (!item) continue;
|
|
|
|
std::ostringstream json;
|
|
json << " {\n";
|
|
json << " \"id\": " << itemId << ",\n";
|
|
json << " \"class\": " << item->m_class << ",\n";
|
|
json << " \"subclass\": " << item->m_subclass << ",\n";
|
|
json << " \"displayName\": " << escapeJsonString(item->m_displayName[0]) << ",\n";
|
|
json << " \"displayInfoID\": " << item->m_displayInfoID << ",\n";
|
|
json << " \"quality\": " << item->m_quality << ",\n";
|
|
json << " \"flags\": " << static_cast<int>(item->m_flags) << ",\n";
|
|
json << " \"buyPrice\": " << item->m_buyPrice << ",\n";
|
|
json << " \"sellPrice\": " << item->m_sellPrice << ",\n";
|
|
json << " \"inventoryType\": " << item->m_inventoryType << ",\n";
|
|
json << " \"allowableClass\": " << item->m_allowableClass << ",\n";
|
|
json << " \"allowableRace\": " << item->m_allowableRace << ",\n";
|
|
json << " \"itemLevel\": " << item->m_itemLevel << ",\n";
|
|
json << " \"requiredLevel\": " << item->m_requiredLevel << ",\n";
|
|
json << " \"requiredSkill\": " << item->m_requiredSkill << ",\n";
|
|
json << " \"requiredSkillRank\": " << item->m_requiredSkillRank << ",\n";
|
|
json << " \"requiredSpell\": " << item->m_requiredSpell << ",\n";
|
|
json << " \"requiredHonorRank\": " << item->m_requiredHonorRank << ",\n";
|
|
json << " \"requiredCityRank\": " << item->m_requiredCityRank << ",\n";
|
|
json << " \"requiredRep\": " << item->m_requiredRep << ",\n";
|
|
json << " \"requiredRepRank\": " << item->m_requiredRepRank << ",\n";
|
|
json << " \"maxCount\": " << item->m_maxCount << ",\n";
|
|
json << " \"stackable\": " << item->m_stackable << ",\n";
|
|
json << " \"containerSlots\": " << item->m_containerSlots << ",\n";
|
|
|
|
// Arrays
|
|
json << " \"bonusStat\": [";
|
|
for (int i = 0; i < 10; ++i) {
|
|
json << item->m_bonusStat[i];
|
|
if (i < 9) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"bonusAmount\": [";
|
|
for (int i = 0; i < 10; ++i) {
|
|
json << item->m_bonusAmount[i];
|
|
if (i < 9) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"minDamage\": [";
|
|
for (int i = 0; i < 5; ++i) {
|
|
json << item->m_minDamage[i];
|
|
if (i < 4) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"maxDamage\": [";
|
|
for (int i = 0; i < 5; ++i) {
|
|
json << item->m_maxDamage[i];
|
|
if (i < 4) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"damageType\": [";
|
|
for (int i = 0; i < 5; ++i) {
|
|
json << item->m_damageType[i];
|
|
if (i < 4) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"resistances\": [";
|
|
for (int i = 0; i < 7; ++i) {
|
|
json << item->m_resistances[i];
|
|
if (i < 6) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"delay\": " << item->m_delay << ",\n";
|
|
json << " \"ammoType\": " << item->m_ammoType << ",\n";
|
|
json << " \"rangedModRange\": " << item->m_rangedModRange << ",\n";
|
|
|
|
json << " \"spellID\": [";
|
|
for (int i = 0; i < 5; ++i) {
|
|
json << item->m_spellID[i];
|
|
if (i < 4) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"spellTrigger\": [";
|
|
for (int i = 0; i < 5; ++i) {
|
|
json << item->m_spellTrigger[i];
|
|
if (i < 4) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"spellCharges\": [";
|
|
for (int i = 0; i < 5; ++i) {
|
|
json << item->m_spellCharges[i];
|
|
if (i < 4) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"spellCooldown\": [";
|
|
for (int i = 0; i < 5; ++i) {
|
|
json << item->m_spellCooldown[i];
|
|
if (i < 4) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"spellCategory\": [";
|
|
for (int i = 0; i < 5; ++i) {
|
|
json << item->m_spellCategory[i];
|
|
if (i < 4) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"spellCategoryCooldown\": [";
|
|
for (int i = 0; i < 5; ++i) {
|
|
json << item->m_spellCategoryCooldown[i];
|
|
if (i < 4) json << ", ";
|
|
}
|
|
json << "],\n";
|
|
|
|
json << " \"bonding\": " << item->m_bonding << ",\n";
|
|
json << " \"description\": " << escapeJsonString(item->m_description) << ",\n";
|
|
json << " \"pageText\": " << item->m_pageText << ",\n";
|
|
json << " \"languageID\": " << item->m_languageID << ",\n";
|
|
json << " \"pageMaterial\": " << item->m_pageMaterial << ",\n";
|
|
json << " \"startQuestID\": " << item->m_startQuestID << ",\n";
|
|
json << " \"lockID\": " << item->m_lockID << ",\n";
|
|
json << " \"material\": " << item->m_material << ",\n";
|
|
json << " \"sheatheType\": " << item->m_sheatheType << ",\n";
|
|
json << " \"randomProperty\": " << item->m_randomProperty << ",\n";
|
|
json << " \"block\": " << item->m_block << ",\n";
|
|
json << " \"itemSet\": " << item->m_itemSet << ",\n";
|
|
json << " \"maxDurability\": " << item->m_maxDurability << ",\n";
|
|
json << " \"area\": " << item->m_area << ",\n";
|
|
json << " \"map\": " << item->m_map << ",\n";
|
|
json << " \"duration\": " << item->m_duration << ",\n";
|
|
json << " \"bagFamily\": " << item->m_bagFamily << "\n";
|
|
json << " }";
|
|
|
|
jsonFile << json.str();
|
|
}
|
|
|
|
jsonFile << "\n]\n";
|
|
jsonFile.close();
|
|
|
|
DEBUG_LOG("Successfully wrote " << itemStatsCache.size() << " items to items.json");
|
|
} catch (const std::exception &e) {
|
|
DEBUG_LOG("Exception writing to items.json: " << e.what());
|
|
}
|
|
|
|
isExporting = false;
|
|
return false; // Export complete
|
|
}
|
|
|
|
game::ItemStats_C *GetItemStats(uint32_t itemId) {
|
|
// Load item if not cached
|
|
LoadItem(itemId);
|
|
|
|
auto it = itemStatsCache.find(itemId);
|
|
if (it != itemStatsCache.end()) {
|
|
return it->second;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Register a function if you want to use this
|
|
// Don't want everyone scraping all items.
|
|
void ExportAllItems() {
|
|
DEBUG_LOG(
|
|
"Starting item export - will fetch items 1 to " << MAX_EXPORT_ITEM_ID << " with up to " << ITEMS_PER_FRAME
|
|
<< " concurrent requests");
|
|
|
|
// Initialize export state
|
|
isExporting = true;
|
|
currentExportItemId = 1;
|
|
pendingItemIds.clear();
|
|
|
|
DEBUG_LOG("Export initialized. ProcessItemExport will be called each frame from processQueues.");
|
|
}
|
|
|
|
void StartItemExport() {
|
|
isExporting = true;
|
|
currentExportItemId = 1;
|
|
pendingItemIds.clear();
|
|
}
|
|
}
|