mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2024-11-16 19:20:05 +00:00
215da0130d
Was getting frustrated by the inconsistency in our include statements, which were causing me problems. As a result, I went through and manually re-organized every include statement in non-core files. I'm just gonna copy my rant from Discord: FOR HEADER FILES (.hpp): - everything you use IN THE HEADER must be EXPLICITLY INCLUDED with the exception of things that fall under Core.hpp - you may NOT include ANYTHING ELSE FOR SOURCE FILES (.cpp): - you can #include whatever you want as long as the partner header is included first - anything that gets included by another include is fair game - redundant includes are ok because they'll be harmless AS LONG AS our header files stay lean. the point of this is NOT to optimize the number of includes used all around or make things more efficient necessarily. it's to improve readability & coherence and make it easier to avoid cyclical issues
406 lines
15 KiB
C++
406 lines
15 KiB
C++
#include "Vendors.hpp"
|
|
|
|
#include "servers/CNShardServer.hpp"
|
|
|
|
#include "PlayerManager.hpp"
|
|
#include "Items.hpp"
|
|
#include "Rand.hpp"
|
|
|
|
// 7 days
|
|
#define VEHICLE_EXPIRY_DURATION 604800
|
|
|
|
using namespace Vendors;
|
|
|
|
std::map<int32_t, std::vector<VendorListing>> Vendors::VendorTables;
|
|
|
|
static void vendorBuy(CNSocket* sock, CNPacketData* data) {
|
|
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_BUY*)data->buf;
|
|
Player* plr = PlayerManager::getPlayer(sock);
|
|
|
|
// prepare fail packet
|
|
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, failResp);
|
|
failResp.iErrorCode = 0;
|
|
|
|
if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end()) {
|
|
std::cout << "[WARN] Vendor with ID " << req->iVendorID << " mismatched or not found (buy)" << std::endl;
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
|
return;
|
|
}
|
|
|
|
std::vector<VendorListing>* listings = &Vendors::VendorTables[req->iVendorID];
|
|
VendorListing reqItem;
|
|
reqItem.id = req->Item.iID;
|
|
reqItem.type = req->Item.iType;
|
|
reqItem.sort = 0; // just to be safe
|
|
|
|
if (std::find(listings->begin(), listings->end(), reqItem) == listings->end()) { // item not found in listing
|
|
std::cout << "[WARN] Player " << PlayerManager::getPlayerName(plr) << " tried to buy an item that wasn't on sale" << std::endl;
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
|
return;
|
|
}
|
|
|
|
Items::Item* itemDat = Items::getItemData(req->Item.iID, req->Item.iType);
|
|
if (itemDat == nullptr) {
|
|
std::cout << "[WARN] Item id " << req->Item.iID << " with type " << req->Item.iType << " not found (buy)" << std::endl;
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
|
return;
|
|
}
|
|
|
|
int itemCost = itemDat->buyPrice * (itemDat->stackSize > 1 ? req->Item.iOpt : 1);
|
|
int slot = Items::findFreeSlot(plr);
|
|
if (itemCost > plr->money || slot == -1) {
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
|
return;
|
|
}
|
|
|
|
// crates don't have a stack size in TableData, so we can't check those
|
|
if (itemDat->stackSize != 0 && req->Item.iOpt > itemDat->stackSize) {
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
|
return;
|
|
}
|
|
|
|
// if vehicle
|
|
if (req->Item.iType == 10) {
|
|
// set time limit: current time + expiry duration
|
|
req->Item.iTimeLimit = getTimestamp() + VEHICLE_EXPIRY_DURATION;
|
|
}
|
|
|
|
if (slot != req->iInvenSlotNum) {
|
|
// possible item stacking?
|
|
std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl;
|
|
}
|
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC, resp);
|
|
|
|
plr->money = plr->money - itemCost;
|
|
plr->Inven[slot] = req->Item;
|
|
|
|
resp.iCandy = plr->money;
|
|
resp.iInvenSlotNum = slot;
|
|
resp.Item = req->Item;
|
|
|
|
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC);
|
|
}
|
|
|
|
static void vendorSell(CNSocket* sock, CNPacketData* data) {
|
|
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_SELL*)data->buf;
|
|
Player* plr = PlayerManager::getPlayer(sock);
|
|
|
|
// prepare a fail packet
|
|
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL, failResp);
|
|
failResp.iErrorCode = 0;
|
|
|
|
if (req->iInvenSlotNum < 0 || req->iInvenSlotNum >= AINVEN_COUNT || req->iItemCnt < 0) {
|
|
std::cout << "[WARN] Client failed to sell item in slot " << req->iInvenSlotNum << std::endl;
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL);
|
|
return;
|
|
}
|
|
|
|
sItemBase* item = &plr->Inven[req->iInvenSlotNum];
|
|
Items::Item* itemData = Items::getItemData(item->iID, item->iType);
|
|
|
|
if (itemData == nullptr || !itemData->sellable || item->iOpt < req->iItemCnt) { // sanity + sellable check
|
|
std::cout << "[WARN] Item id " << item->iID << " with type " << item->iType << " not found (sell)" << std::endl;
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL);
|
|
return;
|
|
}
|
|
|
|
// fail to sell croc-potted items
|
|
if (item->iOpt >= 1 << 16) {
|
|
sock->sendPacket(failResp, P_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);
|
|
|
|
// increment taros
|
|
plr->money += itemData->sellPrice * req->iItemCnt;
|
|
|
|
// modify item
|
|
if (item->iOpt - req->iItemCnt > 0) { // selling part of a stack
|
|
item->iOpt -= req->iItemCnt;
|
|
original.iOpt = req->iItemCnt;
|
|
}
|
|
else { // selling entire slot
|
|
// make sure it's fully zeroed, even the padding and non-104 members
|
|
memset(item, 0, sizeof(*item));
|
|
}
|
|
|
|
// add to buyback list
|
|
plr->buyback.push_back(original);
|
|
// forget oldest member if there's more than 5
|
|
if (plr->buyback.size() > 5)
|
|
plr->buyback.erase(plr->buyback.begin());
|
|
//std::cout << (int)plr->buyback.size() << " items in buyback\n";
|
|
|
|
// response parameters
|
|
resp.iInvenSlotNum = req->iInvenSlotNum;
|
|
resp.iCandy = plr->money;
|
|
resp.Item = original; // the item that gets sent to buyback
|
|
resp.ItemStay = *item; // the void item that gets put in the slot
|
|
|
|
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC);
|
|
}
|
|
|
|
static void vendorBuyback(CNSocket* sock, CNPacketData* data) {
|
|
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY*)data->buf;
|
|
Player* plr = PlayerManager::getPlayer(sock);
|
|
|
|
// prepare fail packet
|
|
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, failResp);
|
|
failResp.iErrorCode = 0;
|
|
|
|
//std::cout << "buying back from index " << (int)req->iListID << " into " << (int)req->iInvenSlotNum <<
|
|
// " from " << (int)req->iNPC_ID << " (vendor = " << (int)req->iVendorID << ")\n";
|
|
|
|
int idx = req->iListID - 1;
|
|
|
|
// sanity check
|
|
if (idx < 0 || idx >= plr->buyback.size()) {
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL);
|
|
return;
|
|
}
|
|
|
|
// get the item out of the buyback list
|
|
sItemBase item = plr->buyback[idx];
|
|
/*
|
|
* NOTE: The client sends the index of the exact item the user clicked on.
|
|
* We then operate on that item, but we remove the *first* identical item
|
|
* from the buyback list, instead of the one at the supplied index.
|
|
*
|
|
* This was originally a mistake on my part, but it turns out the client
|
|
* does the exact same thing, so this *is* the correct thing to do to keep
|
|
* them in sync.
|
|
*/
|
|
for (auto it = plr->buyback.begin(); it != plr->buyback.end(); it++) {
|
|
/*
|
|
* XXX: we really need a standard item comparison function that
|
|
* will work properly across all builds (ex. with iSerial)
|
|
*/
|
|
if (it->iType == item.iType && it->iID == item.iID && it->iOpt == item.iOpt
|
|
&& it->iTimeLimit == item.iTimeLimit) {
|
|
plr->buyback.erase(it);
|
|
break;
|
|
}
|
|
}
|
|
//std::cout << (int)plr->buyback.size() << " items in buyback\n";
|
|
|
|
Items::Item* itemDat = Items::getItemData(item.iID, item.iType);
|
|
|
|
if (itemDat == nullptr) {
|
|
std::cout << "[WARN] Item id " << item.iID << " with type " << item.iType << " not found (rebuy)" << std::endl;
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL);
|
|
return;
|
|
}
|
|
|
|
// sell price is used on rebuy. ternary identifies stacked items
|
|
int itemCost = itemDat->sellPrice * (itemDat->stackSize > 1 ? item.iOpt : 1);
|
|
int slot = Items::findFreeSlot(plr);
|
|
if (itemCost > plr->money || slot == -1) {
|
|
sock->sendPacket(failResp, P_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] = item;
|
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, resp);
|
|
// response parameters
|
|
resp.iCandy = plr->money;
|
|
resp.iInvenSlotNum = slot;
|
|
resp.Item = item;
|
|
|
|
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC);
|
|
}
|
|
|
|
static void vendorTable(CNSocket* sock, CNPacketData* data) {
|
|
auto req = (sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE*)data->buf;
|
|
|
|
if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end())
|
|
return;
|
|
|
|
std::vector<VendorListing>& listings = Vendors::VendorTables[req->iVendorID];
|
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC, resp);
|
|
|
|
for (int i = 0; i < (int)listings.size() && i < 20; i++) { // 20 is the max
|
|
sItemBase base = {};
|
|
base.iID = listings[i].id;
|
|
base.iType = listings[i].type;
|
|
|
|
/*
|
|
* Set vehicle expiry value.
|
|
*
|
|
* Note: sItemBase.iTimeLimit in the context of vendor listings contains
|
|
* a duration, unlike in most other contexts where it contains the
|
|
* expiration timestamp.
|
|
*/
|
|
if (listings[i].type == 10)
|
|
base.iTimeLimit = VEHICLE_EXPIRY_DURATION;
|
|
|
|
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
|
|
|
|
resp.item[i] = vItem;
|
|
}
|
|
|
|
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC);
|
|
}
|
|
|
|
static void vendorStart(CNSocket* sock, CNPacketData* data) {
|
|
auto req = (sP_CL2FE_REQ_PC_VENDOR_START*)data->buf;
|
|
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_START_SUCC, resp);
|
|
|
|
resp.iNPC_ID = req->iNPC_ID;
|
|
resp.iVendorID = req->iVendorID;
|
|
|
|
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_START_SUCC);
|
|
}
|
|
|
|
static void vendorBuyBattery(CNSocket* sock, CNPacketData* data) {
|
|
auto 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 || req->Item.iOpt < 0) { // sanity check
|
|
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp);
|
|
failResp.iErrorCode = 0;
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL);
|
|
return;
|
|
}
|
|
|
|
cost = plr->batteryW + plr->batteryN;
|
|
plr->batteryW += req->Item.iID == 3 ? req->Item.iOpt * 100 : 0;
|
|
plr->batteryN += req->Item.iID == 4 ? req->Item.iOpt * 100 : 0;
|
|
|
|
// caps
|
|
if (plr->batteryW > 9999)
|
|
plr->batteryW = 9999;
|
|
if (plr->batteryN > 9999)
|
|
plr->batteryN = 9999;
|
|
|
|
cost = plr->batteryW + plr->batteryN - cost;
|
|
plr->money -= cost;
|
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, resp);
|
|
|
|
resp.iCandy = plr->money;
|
|
resp.iBatteryW = plr->batteryW;
|
|
resp.iBatteryN = plr->batteryN;
|
|
|
|
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC);
|
|
}
|
|
|
|
static void vendorCombineItems(CNSocket* sock, CNPacketData* data) {
|
|
auto req = (sP_CL2FE_REQ_PC_ITEM_COMBINATION*)data->buf;
|
|
Player* plr = PlayerManager::getPlayer(sock);
|
|
|
|
// prepare fail packet
|
|
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_FAIL, failResp);
|
|
failResp.iCostumeItemSlot = req->iCostumeItemSlot;
|
|
failResp.iStatItemSlot = req->iStatItemSlot;
|
|
failResp.iErrorCode = 0;
|
|
|
|
// sanity check slot indices
|
|
if (req->iCostumeItemSlot < 0 || req->iCostumeItemSlot >= AINVEN_COUNT || req->iStatItemSlot < 0 || req->iStatItemSlot >= AINVEN_COUNT) {
|
|
std::cout << "[WARN] Inventory slot(s) out of range (" << req->iStatItemSlot << " and " << req->iCostumeItemSlot << ")" << std::endl;
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
|
|
return;
|
|
}
|
|
|
|
sItemBase* itemStats = &plr->Inven[req->iStatItemSlot];
|
|
sItemBase* itemLooks = &plr->Inven[req->iCostumeItemSlot];
|
|
Items::Item* itemStatsDat = Items::getItemData(itemStats->iID, itemStats->iType);
|
|
Items::Item* itemLooksDat = Items::getItemData(itemLooks->iID, itemLooks->iType);
|
|
|
|
// sanity check item and combination entry existence
|
|
if (itemStatsDat == nullptr || itemLooksDat == nullptr
|
|
|| Items::CrocPotTable.find(abs(itemStatsDat->level - itemLooksDat->level)) == Items::CrocPotTable.end()) {
|
|
std::cout << "[WARN] Either item ids or croc pot value set not found" << std::endl;
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
|
|
return;
|
|
}
|
|
|
|
// sanity check matching item types
|
|
if (itemStats->iType != itemLooks->iType
|
|
|| (itemStats->iType == 0 && itemStatsDat->weaponType != itemLooksDat->weaponType)) {
|
|
std::cout << "[WARN] Player attempted to combine mismatched items" << std::endl;
|
|
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
|
|
return;
|
|
}
|
|
|
|
CrocPotEntry* recipe = &Items::CrocPotTable[abs(itemStatsDat->level - itemLooksDat->level)];
|
|
int cost = itemStatsDat->buyPrice * recipe->multStats + itemLooksDat->buyPrice * recipe->multLooks;
|
|
float successChance = recipe->base / 100.0f; // base success chance
|
|
|
|
// rarity gap multiplier
|
|
switch (abs(itemStatsDat->rarity - itemLooksDat->rarity)) {
|
|
case 0:
|
|
successChance *= recipe->rd0;
|
|
break;
|
|
case 1:
|
|
successChance *= recipe->rd1;
|
|
break;
|
|
case 2:
|
|
successChance *= recipe->rd2;
|
|
break;
|
|
case 3:
|
|
successChance *= recipe->rd3;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
float rolled = Rand::randFloat(100.0f); // success chance out of 100
|
|
//std::cout << rolled << " vs " << successChance << std::endl;
|
|
plr->money -= cost;
|
|
|
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_SUCC, resp);
|
|
if (rolled < successChance) {
|
|
// success
|
|
resp.iSuccessFlag = 1;
|
|
|
|
// modify the looks item with the new stats and set the appearance through iOpt
|
|
itemLooks->iOpt = (int32_t)((itemLooks->iOpt) >> 16 > 0 ? (itemLooks->iOpt >> 16) : itemLooks->iID) << 16;
|
|
itemLooks->iID = itemStats->iID;
|
|
|
|
// delete stats item
|
|
itemStats->iID = 0;
|
|
itemStats->iOpt = 0;
|
|
itemStats->iTimeLimit = 0;
|
|
itemStats->iType = 0;
|
|
}
|
|
else {
|
|
// failure; don't do anything?
|
|
resp.iSuccessFlag = 0;
|
|
}
|
|
resp.iCandy = plr->money;
|
|
resp.iNewItemSlot = req->iCostumeItemSlot;
|
|
resp.iStatItemSlot = req->iStatItemSlot;
|
|
resp.sNewItem = *itemLooks;
|
|
|
|
sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_COMBINATION_SUCC);
|
|
}
|
|
|
|
void Vendors::init() {
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_START, vendorStart);
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE, vendorTable);
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_BUY, vendorBuy);
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_SELL, vendorSell);
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY, vendorBuyback);
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_BATTERY_BUY, vendorBuyBattery);
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_COMBINATION, vendorCombineItems);
|
|
}
|