diff --git a/src/ItemManager.cpp b/src/ItemManager.cpp index fff60df..d27810f 100644 --- a/src/ItemManager.cpp +++ b/src/ItemManager.cpp @@ -7,6 +7,9 @@ #include // for memset() and memcmp() #include +std::map, Item> ItemManager::ItemData; +std::map> ItemManager::VendorTables; + void ItemManager::init() { REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_DELETE, itemDeleteHandler); @@ -718,6 +721,7 @@ void ItemManager::chestOpenHandler(CNSocket *sock, CNPacketData *data) { // item reward item->sItem.iType = 0; item->sItem.iID = 96; + item->sItem.iOpt = 1; item->iSlotNum = pkt->iSlotNum; item->eIL = pkt->eIL; @@ -747,3 +751,9 @@ int ItemManager::findFreeSlot(Player *plr) { // not found return -1; } + +Item* ItemManager::getItemData(int32_t id, int32_t type) { + if(ItemData.find(std::pair(id, type)) != ItemData.end()) + return &ItemData[std::pair(id, type)]; + return nullptr; +} diff --git a/src/ItemManager.hpp b/src/ItemManager.hpp index cd7a302..218f09c 100644 --- a/src/ItemManager.hpp +++ b/src/ItemManager.hpp @@ -3,13 +3,25 @@ #include "CNShardServer.hpp" #include "Player.hpp" +struct Item { + bool tradeable, sellable; + int buyPrice, sellPrice, stackSize, level; // TODO: implement more as needed +}; +struct VendorListing { + int sort, type, iID; +}; + namespace ItemManager { enum class SlotType { EQUIP = 0, INVENTORY = 1, BANK = 3 }; - void init(); + // hopefully this is fine since it's never modified after load + extern std::map, Item> ItemData; // -> data + extern std::map> VendorTables; + + void init(); void itemMoveHandler(CNSocket* sock, CNPacketData* data); void itemDeleteHandler(CNSocket* sock, CNPacketData* data); @@ -29,4 +41,5 @@ namespace ItemManager { void chestOpenHandler(CNSocket* sock, CNPacketData* data); int findFreeSlot(Player *plr); + Item* getItemData(int32_t id, int32_t type); } diff --git a/src/NPCManager.cpp b/src/NPCManager.cpp index d1e7f76..9e8d617 100644 --- a/src/NPCManager.cpp +++ b/src/NPCManager.cpp @@ -22,6 +22,8 @@ void NPCManager::init() { REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE, npcVendorTable); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_BUY, npcVendorBuy); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_SELL, npcVendorSell); + REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY, npcVendorBuyback); + REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_BATTERY_BUY, npcVendorBuyBattery); } void NPCManager::npcVendorBuy(CNSocket* sock, CNPacketData* data) { @@ -30,11 +32,19 @@ void NPCManager::npcVendorBuy(CNSocket* sock, CNPacketData* data) { sP_CL2FE_REQ_PC_VENDOR_ITEM_BUY* req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_BUY*)data->buf; Player *plr = PlayerManager::getPlayer(sock); + Item* item = ItemManager::getItemData(req->Item.iID, req->Item.iType); - int itemCost = 100; // TODO: placeholder, look up the price of item + if (item == nullptr) { + std::cout << "[WARN] Item id " << req->Item.iID << " with type " << req->Item.iType << " not found (buy)" << std::endl; + // NOTE: VENDOR_ITEM_BUY_FAIL is not actually handled client-side. + INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, failResp); + failResp.iErrorCode = 0; + sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL)); + return; + } + int itemCost = item->sellPrice * (item->stackSize > 1 ? req->Item.iOpt : 1); int slot = ItemManager::findFreeSlot(plr); - if (itemCost > plr->money || slot == -1) { // NOTE: VENDOR_ITEM_BUY_FAIL is not actually handled client-side. INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, failResp); @@ -76,82 +86,120 @@ void NPCManager::npcVendorSell(CNSocket* sock, CNPacketData* data) { } sItemBase* item = &plr->Inven[req->iInvenSlotNum]; + Item* itemData = ItemManager::getItemData(item->iID, item->iType); + + if (itemData == nullptr || !itemData->sellable) { // sanity + sellable check + std::cout << "[WARN] Item id " << item->iID << " with type " << item->iType << " not found (sell)" << std::endl; + INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL, failResp); + failResp.iErrorCode = 0; + sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL)); + return; + } + + sItemBase original; + memcpy(&original, item, sizeof(sItemBase)); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC, resp); - - int sellValue = 100 * req->iItemCnt; // TODO: lookup item price + + int sellValue = itemData->sellPrice * req->iItemCnt; // increment taros plr->money = plr->money + sellValue; - // empty the slot; probably needs to be changed for stacks - item->iID = 0; - item->iOpt = 0; - item->iType = 0; - item->iTimeLimit = 0; + // modify item + if (plr->Inven[req->iInvenSlotNum].iOpt - req->iItemCnt > 0) { // selling part of a stack + item->iOpt -= req->iItemCnt; + original.iOpt = req->iItemCnt; + } + else { // selling entire slot + item->iID = 0; + item->iOpt = 0; + item->iType = 0; + item->iTimeLimit = 0; + } // response parameters resp.iInvenSlotNum = req->iInvenSlotNum; resp.iCandy = plr->money; - resp.Item = *item; + resp.Item = original; // the item that gets sent to buyback + resp.ItemStay = *item; // the void item that gets put in the slot sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC)); } +void NPCManager::npcVendorBuyback(CNSocket* sock, CNPacketData* data) { + if (data->size != sizeof(sP_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY)) + return; // malformed packet + + sP_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY* req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY*)data->buf; + Player* plr = PlayerManager::getPlayer(sock); + Item* item = ItemManager::getItemData(req->Item.iID, req->Item.iType); + + if (item == nullptr) { + std::cout << "[WARN] Item id " << req->Item.iID << " with type " << req->Item.iType << " not found (rebuy)" << std::endl; + // NOTE: VENDOR_ITEM_BUY_FAIL is not actually handled client-side. + INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, failResp); + failResp.iErrorCode = 0; + sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL)); + return; + } + + // sell price is used on rebuy. ternary identifies stacked items + int itemCost = item->sellPrice * (item->stackSize > 1 ? req->Item.iOpt : 1); + int slot = ItemManager::findFreeSlot(plr); + if (itemCost > plr->money || slot == -1) { + // NOTE: VENDOR_ITEM_BUY_FAIL is not actually handled client-side. + INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, failResp); + failResp.iErrorCode = 0; + sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL)); + return; + } + + if (slot != req->iInvenSlotNum) { + // possible item stacking? + std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl; + } + + plr->money = plr->money - itemCost; + plr->Inven[slot] = req->Item; + + INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, resp); + // response parameters + resp.iCandy = plr->money; + resp.iInvenSlotNum = slot; + resp.Item = req->Item; + + sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC)); +} + void NPCManager::npcVendorTable(CNSocket* sock, CNPacketData* data) { if (data->size != sizeof(sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE)) return; // malformed packet - //sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE* req = (sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE*)data->buf; + sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE* req = (sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE*)data->buf; + + if (req->iVendorID != req->iNPC_ID || ItemManager::VendorTables.find(req->iNPC_ID) == ItemManager::VendorTables.end()) + return; + + std::vector listings = ItemManager::VendorTables[req->iNPC_ID]; // maybe use iVendorID instead...? + INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC, resp); - // TODO: data needs to be read from shopkeeper tabledata - // check req->iVendorID and req->iNPC_ID - // Exaple Items - // shirt - sItemBase base1; - base1.iID = 60; - base1.iOpt = 0; - // expire date - base1.iTimeLimit = 0; - base1.iType = 1; + for (int i = 0; i < listings.size() && i < 20; i++) { // 20 is the max + sItemBase base; + base.iID = listings[i].iID; + base.iOpt = 0; + base.iTimeLimit = 0; + base.iType = listings[i].type; - sItemVendor item1; - item1.item = base1; - item1.iSortNum = 0; - item1.iVendorID = 1; - // cost amount in float? (doesn't work rn, need to figure out) - item1.fBuyCost = 100.0; + sItemVendor vItem; + vItem.item = base; + vItem.iSortNum = listings[i].sort; + vItem.iVendorID = req->iVendorID; + //vItem.fBuyCost = listings[i].price; this value is not actually the one that is used - // pants - sItemBase base2; - base2.iID = 61; - base2.iOpt = 0; - base2.iTimeLimit = 0; - base2.iType = 2; - - sItemVendor item2; - item2.item = base2; - item2.iSortNum = 1; - item2.iVendorID = 1; - item2.fBuyCost = 250.0; - - // shoes - sItemBase base3; - base3.iID = 51; - base3.iOpt = 0; - base3.iTimeLimit = 0; - base3.iType = 3; - - sItemVendor item3; - item3.item = base3; - item3.iSortNum = 2; - item3.iVendorID = 1; - item3.fBuyCost = 350.0; - - resp.item[0] = item1; - resp.item[1] = item2; - resp.item[2] = item3; + resp.item[i] = vItem; + } sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC, sizeof(sP_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC)); } @@ -169,6 +217,39 @@ void NPCManager::npcVendorStart(CNSocket* sock, CNPacketData* data) { sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_VENDOR_START_SUCC, sizeof(sP_FE2CL_REP_PC_VENDOR_START_SUCC)); } +void NPCManager::npcVendorBuyBattery(CNSocket* sock, CNPacketData* data) { + if (data->size != sizeof(sP_CL2FE_REQ_PC_VENDOR_BATTERY_BUY)) + return; // malformed packet + + sP_CL2FE_REQ_PC_VENDOR_BATTERY_BUY* req = (sP_CL2FE_REQ_PC_VENDOR_BATTERY_BUY*)data->buf; + Player* plr = PlayerManager::getPlayer(sock); + + int cost = req->Item.iOpt * 100; + if ((req->Item.iID == 3 ? (plr->batteryW >= 9999) : (plr->batteryN >= 9999)) || plr->money < cost) { // sanity check + INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp); + failResp.iErrorCode = 0; + sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, sizeof(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL)); + } + + plr->money -= cost; + plr->batteryW += req->Item.iID == 3 ? req->Item.iOpt * 10 : 0; + plr->batteryN += req->Item.iID == 4 ? req->Item.iOpt * 10 : 0; + + // caps + if (plr->batteryW > 9999) + plr->batteryW = 9999; + if (plr->batteryN > 9999) + plr->batteryN = 9999; + + INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, resp); + + resp.iCandy = plr->money; + resp.iBatteryW = plr->batteryW; + resp.iBatteryN = plr->batteryN; + + sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, sizeof(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC)); +} + void NPCManager::updatePlayerNPCS(CNSocket* sock, PlayerView& view) { std::list yesView; std::list noView; diff --git a/src/NPCManager.hpp b/src/NPCManager.hpp index b65874d..9231442 100644 --- a/src/NPCManager.hpp +++ b/src/NPCManager.hpp @@ -28,6 +28,8 @@ namespace NPCManager { void npcVendorTable(CNSocket* sock, CNPacketData* data); void npcVendorBuy(CNSocket* sock, CNPacketData* data); void npcVendorSell(CNSocket* sock, CNPacketData* data); + void npcVendorBuyback(CNSocket* sock, CNPacketData* data); + void npcVendorBuyBattery(CNSocket* sock, CNPacketData* data); void updatePlayerNPCS(CNSocket* sock, PlayerView& plr); } diff --git a/src/Player.hpp b/src/Player.hpp index 7bb514c..42536fa 100644 --- a/src/Player.hpp +++ b/src/Player.hpp @@ -19,6 +19,8 @@ struct Player { int slot; // player slot, not nano slot int32_t money; int32_t fusionmatter; + int32_t batteryW; + int32_t batteryN; sPCStyle PCStyle; sPCStyle2 PCStyle2; sNano Nanos[37]; // acquired nanos diff --git a/src/TableData.cpp b/src/TableData.cpp index df367b1..dbe6de0 100644 --- a/src/TableData.cpp +++ b/src/TableData.cpp @@ -1,6 +1,7 @@ #include "TableData.hpp" #include "NPCManager.hpp" #include "TransportManager.hpp" +#include "ItemManager.hpp" #include "settings.hpp" #include "MissionManager.hpp" @@ -119,6 +120,30 @@ void TableData::init() { } std::cout << "[INFO] Loaded mission-related data" << std::endl; + + // load all item data. i'm sorry. it has to be done + const char* setNames[12] = { "m_pBackItemTable", "m_pFaceItemTable", "m_pGlassItemTable", "m_pHatItemTable", + "m_pHeadItemTable", "m_pPantsItemTable", "m_pShirtsItemTable", "m_pShoesItemTable", "m_pWeaponItemTable", + "m_pVehicleItemTable", "m_pGeneralItemTable", "m_pChestItemTable" }; + nlohmann::json itemSet; + for (int i = 0; i < 12; i++) { + itemSet = xdtData[setNames[i]]["m_pItemData"]; + for (nlohmann::json::iterator item = itemSet.begin(); item != itemSet.end(); item++) + ItemManager::ItemData[std::pair(item.value()["m_iItemNumber"], i == 11 ? 9 : (i == 10 ? 7 : (int)item.value()["m_iEquipLoc"]))] + = { item.value()["m_iTradeAble"] == 1, item.value()["m_iSellAble"] == 1, item.value()["m_iItemPrice"], item.value()["m_iItemSellPrice"], item.value()["m_iStackNumber"], i > 9 ? 0 : (int)item.value()["m_iMinReqLev"] }; + } + + std::cout << "[INFO] Loaded " << ItemManager::ItemData.size() << " items" << std::endl; + + // load vendor listings + nlohmann::json listings = xdtData["m_pVendorTable"]["m_pItemData"]; + + for (nlohmann::json::iterator listing = listings.begin(); listing != listings.end(); listing++) { + VendorListing vListing = { listing.value()["m_iSortNumber"], listing.value()["m_iItemType"], listing.value()["m_iitemID"] }; + ItemManager::VendorTables[listing.value()["m_iNpcNumber"]].push_back(vListing); + } + + std::cout << "[INFO] Loaded " << ItemManager::VendorTables.size() << " vendor tables" << std::endl; } catch (const std::exception& err) { std::cerr << "[WARN] Malformed xdt.json file! Reason:" << err.what() << std::endl;