diff --git a/src/Entities.cpp b/src/Entities.cpp index c3532ded..a9621d71 100644 --- a/src/Entities.cpp +++ b/src/Entities.cpp @@ -117,6 +117,25 @@ sPCAppearanceData Player::getAppearanceData() { return data; } +bool Player::hasQuestBoost() const { + const sItemBase& booster = Equip[10]; + return booster.iID == 153 && booster.iOpt > 0; +} + +bool Player::hasHunterBoost() const { + const sItemBase& booster = Equip[11]; + return booster.iID == 154 && booster.iOpt > 0; +} + +bool Player::hasRacerBoost() const { + const sItemBase& booster = Equip[9]; + return booster.iID == 155 && booster.iOpt > 0; +} + +bool Player::hasSuperBoost() const { + return Player::hasQuestBoost() && Player::hasHunterBoost() && Player::hasRacerBoost(); +} + // TODO: this is less effiecient than it was, because of memset() void Player::enterIntoViewOf(CNSocket *sock) { INITSTRUCT(sP_FE2CL_PC_NEW, pkt); diff --git a/src/Items.cpp b/src/Items.cpp index b11c0862..f39cd7b9 100644 --- a/src/Items.cpp +++ b/src/Items.cpp @@ -33,6 +33,9 @@ std::map Items::EventToDropMap; std::map Items::MobToDropMap; std::map Items::ItemSets; +// 1 week +#define NANOCOM_BOOSTER_DURATION 604800 + #ifdef ACADEMY std::map Items::NanoCapsules; // crate id -> nano id @@ -328,8 +331,8 @@ static void itemMoveHandler(CNSocket* sock, CNPacketData* data) { && !(fromItem->iType == 0 && itemmove->iToSlotNum == 7) && fromItem->iType != itemmove->iToSlotNum) return; // something other than a vehicle or a weapon in a non-matching slot - else if (itemmove->iToSlotNum >= AEQUIP_COUNT) // TODO: reject slots >= 9? - return; // invalid slot + else if (itemmove->iToSlotNum > 8) + return; // any slot higher than 8 is for a booster, and they can't be equipped via move packet } // save items to response @@ -430,25 +433,14 @@ static void itemDeleteHandler(CNSocket* sock, CNPacketData* data) { sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_DELETE_SUCC); } -static void itemUseHandler(CNSocket* sock, CNPacketData* data) { +static void useGumball(CNSocket* sock, CNPacketData* data) { auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf; Player* player = PlayerManager::getPlayer(sock); - if (request->iSlotNum < 0 || request->iSlotNum >= AINVEN_COUNT) - return; // sanity check - // gumball can only be used from inventory, so we ignore eIL sItemBase gumball = player->Inven[request->iSlotNum]; sNano nano = player->Nanos[player->equippedNanos[request->iNanoSlot]]; - // sanity check, check if gumball exists - if (!(gumball.iOpt > 0 && gumball.iType == 7 && gumball.iID>=119 && gumball.iID<=121)) { - std::cout << "[WARN] Gumball not found" << std::endl; - INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response); - sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL); - return; - } - // sanity check, check if gumball type matches nano style int nanoStyle = Nanos::nanoStyle(nano.iID); if (!((gumball.iID == 119 && nanoStyle == 0) || @@ -472,11 +464,8 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) { return; } - if (gumball.iOpt == 0) - gumball = {}; - - uint8_t respbuf[CN_PACKET_BODY_SIZE]; - memset(respbuf, 0, CN_PACKET_BODY_SIZE); + uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; + memset(respbuf, 0, resplen); sP_FE2CL_REP_PC_ITEM_USE_SUCC *resp = (sP_FE2CL_REP_PC_ITEM_USE_SUCC*)respbuf; sSkillResult_Buff *respdata = (sSkillResult_Buff*)(respbuf+sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC)); @@ -515,6 +504,128 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) { player->Inven[resp->iSlotNum] = resp->RemainItem; } +static void useNanocomBooster(CNSocket* sock, CNPacketData* data) { + // Guard against using nanocom boosters in before and including 0104 + // either path should be optimized by the compiler, effectively a no-op + if (AEQUIP_COUNT < 12) { + std::cout << "[WARN] Nanocom Booster use not supported in this version" << std::endl; + INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, respFail); + sock->sendPacket(respFail, P_FE2CL_REP_PC_ITEM_USE_FAIL); + return; + } + + auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf; + Player* player = PlayerManager::getPlayer(sock); + sItemBase item = player->Inven[request->iSlotNum]; + + // consume item + item.iOpt -= 1; + if (item.iOpt == 0) + item = {}; + + // decide on the booster to activate + std::vector boosterIDs; + switch(item.iID) { + case 153: + case 154: + case 155: + boosterIDs.push_back(item.iID); + break; + case 156: + boosterIDs.push_back(153); + boosterIDs.push_back(154); + boosterIDs.push_back(155); + break; + } + + // client wants to subtract server time in seconds from the time limit for display purposes + int32_t timeLimitDisplayed = (getTime() / 1000UL) + NANOCOM_BOOSTER_DURATION; + // in actuality we will use the timestamp of booster activation to the item time limit similar to vehicles + // and this is how it will be saved to the database + int32_t timeLimit = getTimestamp() + NANOCOM_BOOSTER_DURATION; + + // give item(s) to inv slots + for (int16_t itemID : boosterIDs) { + sItemBase boosterItem = { 7, itemID, 1, timeLimitDisplayed }; + + // 155 -> 9, 153 -> 10, 154 -> 11 + int slot = 9 + ((itemID - 152) % 3); + + // give item to the equip slot + INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp); + resp.eIL = (int)SlotType::EQUIP; + resp.iSlotNum = slot; + resp.Item = boosterItem; + sock->sendPacket(resp, P_FE2CL_REP_PC_GIVE_ITEM_SUCC); + + // inform client of equip change (non visible so it's okay to just send to the player) + INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, equipChange); + equipChange.iPC_ID = player->iID; + equipChange.iEquipSlotNum = slot; + equipChange.EquipSlotItem = boosterItem; + sock->sendPacket(equipChange, P_FE2CL_PC_EQUIP_CHANGE); + + boosterItem.iTimeLimit = timeLimit; + // should replace existing booster in slot if it exists, i.e. you can refresh your boosters + player->Equip[slot] = boosterItem; + } + + // send item use success packet + INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_SUCC, respUse); + respUse.iPC_ID = player->iID; + respUse.eIL = (int)SlotType::INVENTORY; + respUse.iSlotNum = request->iSlotNum; + respUse.RemainItem = item; + sock->sendPacket(respUse, P_FE2CL_REP_PC_ITEM_USE_SUCC); + + // update inventory serverside + player->Inven[request->iSlotNum] = item; +} + +static void itemUseHandler(CNSocket* sock, CNPacketData* data) { + auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf; + Player* player = PlayerManager::getPlayer(sock); + + if (request->iSlotNum < 0 || request->iSlotNum >= AINVEN_COUNT) + return; // sanity check + + sItemBase item = player->Inven[request->iSlotNum]; + + // sanity check, check the item exists and has correct iType + if (!(item.iOpt > 0 && item.iType == 7)) { + std::cout << "[WARN] General item not found" << std::endl; + INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response); + sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL); + return; + } + + /* + * TODO: In the XDT, there are subtypes for general-use items + * (m_pGeneralItemTable -> m_pItemData-> m_iItemType) that + * determine their behavior. It would be better to load these + * and use them in this switch, rather than hardcoding by IDs. + */ + + switch(item.iID) { + case 119: + case 120: + case 121: + useGumball(sock, data); + break; + case 153: + case 154: + case 155: + case 156: + useNanocomBooster(sock, data); + break; + default: + std::cout << "[INFO] General item "<< item.iID << " is unimplemented." << std::endl; + INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response); + sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL); + break; + } +} + static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) { Player* plr = PlayerManager::getPlayer(sock); @@ -632,39 +743,87 @@ Item* Items::getItemData(int32_t id, int32_t type) { return nullptr; } -void Items::checkItemExpire(CNSocket* sock, Player* player) { - if (player->toRemoveVehicle.eIL == 0 && player->toRemoveVehicle.iSlotNum == 0) - return; +size_t Items::checkAndRemoveExpiredItems(CNSocket* sock, Player* player) { + int32_t currentTime = getTimestamp(); - /* prepare packet - * yes, this is a varadic packet, however analyzing client behavior and code - * it only checks takes the first item sent into account - * yes, this is very stupid - * therefore, we delete all but 1 expired vehicle while loading player - * to delete the last one here so player gets a notification - */ + // if there are expired items in bank just remove them silently + for (int i = 0; i < ABANK_COUNT; i++) { + if (player->Bank[i].iTimeLimit < currentTime && player->Bank[i].iTimeLimit != 0) { + memset(&player->Bank[i], 0, sizeof(sItemBase)); + } + } + + // collect items to remove and data for the packet + std::vector toRemove; + std::vector itemData; + + // equipped items + for (int i = 0; i < AEQUIP_COUNT; i++) { + if (player->Equip[i].iOpt > 0 && player->Equip[i].iTimeLimit < currentTime && player->Equip[i].iTimeLimit != 0) { + toRemove.push_back(&player->Equip[i]); + itemData.push_back({ (int)SlotType::EQUIP, i }); + } + } + // inventory + for (int i = 0; i < AINVEN_COUNT; i++) { + if (player->Inven[i].iTimeLimit < currentTime && player->Inven[i].iTimeLimit != 0) { + toRemove.push_back(&player->Inven[i]); + itemData.push_back({ (int)SlotType::INVENTORY, i }); + } + } + + if (itemData.empty()) + return 0; - const size_t resplen = sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL); + // prepare packet containing all expired items to delete + // this is expected for academy + // pre-academy only checks the first item in the packet + const size_t resplen = sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL) * itemData.size(); + + // 8 bytes * 262 items = 2096 bytes, in total this shouldn't exceed 2500 bytes assert(resplen < CN_PACKET_BODY_SIZE); - // we know it's only one trailing struct, so we can skip full validation - uint8_t respbuf[resplen]; // not a variable length array, don't worry + uint8_t respbuf[CN_PACKET_BODY_SIZE]; + memset(respbuf, 0, CN_PACKET_BODY_SIZE); + auto packet = (sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM*)respbuf; - sTimeLimitItemDeleteInfo2CL* itemData = (sTimeLimitItemDeleteInfo2CL*)(respbuf + sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM)); - memset(respbuf, 0, resplen); - packet->iItemListCount = 1; - itemData->eIL = player->toRemoveVehicle.eIL; - itemData->iSlotNum = player->toRemoveVehicle.iSlotNum; + for (size_t i = 0; i < itemData.size(); i++) { + auto itemToDeletePtr = (sTimeLimitItemDeleteInfo2CL*)( + respbuf + sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL) * i + ); + itemToDeletePtr->eIL = itemData[i].eIL; + itemToDeletePtr->iSlotNum = itemData[i].iSlotNum; + packet->iItemListCount++; + } + sock->sendPacket((void*)&respbuf, P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM, resplen); - // delete serverside - if (player->toRemoveVehicle.eIL == 0) - memset(&player->Equip[8], 0, sizeof(sItemBase)); - else - memset(&player->Inven[player->toRemoveVehicle.iSlotNum], 0, sizeof(sItemBase)); + // delete items serverside and send unequip packets + for (size_t i = 0; i < itemData.size(); i++) { + sItemBase* item = toRemove[i]; + memset(item, 0, sizeof(sItemBase)); + + // send item delete success packet + // required for pre-academy builds + INITSTRUCT(sP_FE2CL_REP_PC_ITEM_DELETE_SUCC, itemDelete); + itemDelete.eIL = itemData[i].eIL; + itemDelete.iSlotNum = itemData[i].iSlotNum; + sock->sendPacket(itemDelete, P_FE2CL_REP_PC_ITEM_DELETE_SUCC); + + // also update item equips if needed + if (itemData[i].eIL == (int)SlotType::EQUIP) { + INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, equipChange); + equipChange.iPC_ID = player->iID; + equipChange.iEquipSlotNum = itemData[i].iSlotNum; + sock->sendPacket(equipChange, P_FE2CL_PC_EQUIP_CHANGE); + } + } + + // exit vehicle if player no longer has one equipped (function checks pcstyle) + if (player->Equip[8].iID == 0) + PlayerManager::exitPlayerVehicle(sock, nullptr); - player->toRemoveVehicle.eIL = 0; - player->toRemoveVehicle.iSlotNum = 0; + return itemData.size(); } void Items::setItemStats(Player* plr) { @@ -711,7 +870,68 @@ static void getMobDrop(sItemBase* reward, const std::vector& weights, const reward->iID = crateIds[chosenIndex]; } -static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRoll& rolled) { +static int getTaroDrop(Player* plr, int baseAmount, int groupSize) { + double bonus = plr->hasBuff(ECSB_REWARD_CASH) ? (Nanos::getNanoBoost(plr) ? 1.23 : 1.2) : 1.0; + double groupEffect = 1.0 / groupSize; + int amount = baseAmount * bonus * groupEffect; + return amount; +} + +static int getFMDrop(Player* plr, int baseAmount, int levelDiff, int groupSize) { + double bonus = plr->hasBuff(ECSB_REWARD_BLOB) ? (Nanos::getNanoBoost(plr) ? 1.23 : 1.2) : 1.0; + double boosterEffect = plr->hasHunterBoost() ? (plr->hasQuestBoost() && plr->hasRacerBoost() ? 1.75 : 1.5) : 1.0; + + double levelEffect = 1.0; + if (levelDiff >= 6) { + // if player is 6 or more levels above mob, no FM is dropped + levelEffect = 0.0; + } else if (levelDiff <= -3) { + // if player is 3 or more levels below mob, FM is 1.2x + levelEffect = 1.2; + } else { + switch (levelDiff) { + // if player is within 1 level of the mob, FM is untouched + // otherwise, follow the table below + case 5: + levelEffect = 0.25; + break; + case 4: + levelEffect = 0.5; + break; + case 3: + levelEffect = 0.75; + break; + case 2: + // this case is more lenient + levelEffect = 0.899; + break; + case -2: + levelEffect = 1.1; + break; + } + } + + double groupEffect = 1.0; + switch (groupSize) { + // if no group, FM is untouched + // otherwise, follow the table below + case 2: + groupEffect = 0.875; + break; + case 3: + groupEffect = 0.75; + break; + case 4: + // this case is more lenient + groupEffect = 0.688; + break; + } + + int amount = baseAmount * bonus * boosterEffect * levelEffect * groupEffect; + return amount; +} + +static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRoll& rolled, int groupSize) { Player *plr = PlayerManager::getPlayer(sock); const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward); @@ -763,30 +983,11 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo MiscDropType& miscDropType = Items::MiscDropTypes[drop.miscDropTypeId]; if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) { - plr->money += miscDropType.taroAmount; - // money nano boost - if (plr->hasBuff(ECSB_REWARD_CASH)) { - int boost = 0; - if (Nanos::getNanoBoost(plr)) // for gumballs - boost = 1; - plr->money += miscDropType.taroAmount * (5 + boost) / 25; - } + plr->money += getTaroDrop(plr, miscDropType.taroAmount, groupSize); } if (rolled.fm % miscDropChance.fmDropChanceTotal < miscDropChance.fmDropChance) { - // formula for scaling FM with player/mob level difference - // TODO: adjust this better int levelDifference = plr->level - mob->level; - int fm = miscDropType.fmAmount; - if (levelDifference > 0) - fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0; - // scavenger nano boost - if (plr->hasBuff(ECSB_REWARD_BLOB)) { - int boost = 0; - if (Nanos::getNanoBoost(plr)) // for gumballs - boost = 1; - fm += fm * (5 + boost) / 25; - } - + int fm = getFMDrop(plr, miscDropType.fmAmount, levelDifference, groupSize); Missions::updateFusionMatter(sock, fm); } @@ -830,7 +1031,7 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo } } -void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled) { +void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled, int groupSize) { // sanity check if (Items::MobToDropMap.find(mob->type) == Items::MobToDropMap.end()) { std::cout << "[WARN] Mob ID " << mob->type << " has no drops assigned" << std::endl; @@ -839,7 +1040,7 @@ void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const // find mob drop id int mobDropId = Items::MobToDropMap[mob->type]; - giveSingleDrop(sock, mob, mobDropId, rolled); + giveSingleDrop(sock, mob, mobDropId, rolled, groupSize); if (settings::EVENTMODE != 0) { // sanity check @@ -850,14 +1051,13 @@ void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const // find mob drop id int eventMobDropId = Items::EventToDropMap[settings::EVENTMODE]; - giveSingleDrop(sock, mob, eventMobDropId, eventRolled); + giveSingleDrop(sock, mob, eventMobDropId, eventRolled, groupSize); } } void Items::init() { REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_DELETE, itemDeleteHandler); - // this one is for gumballs REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_USE, itemUseHandler); // Bank REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BANK_OPEN, itemBankOpenHandler); diff --git a/src/Items.hpp b/src/Items.hpp index 2156b92e..9a73a79f 100644 --- a/src/Items.hpp +++ b/src/Items.hpp @@ -113,11 +113,11 @@ namespace Items { void init(); // mob drops - void giveMobDrop(CNSocket *sock, Mob *mob, const DropRoll& rolled, const DropRoll& eventRolled); + void giveMobDrop(CNSocket *sock, Mob *mob, const DropRoll& rolled, const DropRoll& eventRolled, int groupSize); int findFreeSlot(Player *plr); Item* getItemData(int32_t id, int32_t type); - void checkItemExpire(CNSocket* sock, Player* player); + size_t checkAndRemoveExpiredItems(CNSocket* sock, Player* player); void setItemStats(Player* plr); void updateEquips(CNSocket* sock, Player* plr); diff --git a/src/MobAI.cpp b/src/MobAI.cpp index 50c87422..be367883 100644 --- a/src/MobAI.cpp +++ b/src/MobAI.cpp @@ -813,7 +813,7 @@ void MobAI::onDeath(CombatNPC* npc, EntityRef src) { if (plr->group == nullptr) { playerRefs.push_back(plr); Combat::genQItemRolls(playerRefs, qitemRolls); - Items::giveMobDrop(src.sock, self, rolled, eventRolled); + Items::giveMobDrop(src.sock, self, rolled, eventRolled, 1); Missions::mobKilled(src.sock, self->type, qitemRolls); } else { @@ -829,7 +829,7 @@ void MobAI::onDeath(CombatNPC* npc, EntityRef src) { if (dist > 5000) continue; - Items::giveMobDrop(sockTo, self, rolled, eventRolled); + Items::giveMobDrop(sockTo, self, rolled, eventRolled, players.size()); Missions::mobKilled(sockTo, self->type, qitemRolls); } } diff --git a/src/Player.hpp b/src/Player.hpp index edf97488..59deaef5 100644 --- a/src/Player.hpp +++ b/src/Player.hpp @@ -65,8 +65,6 @@ struct Player : public Entity, public ICombatant { sItemBase QInven[AQINVEN_COUNT] = {}; int32_t CurrentMissionID = 0; - sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {}; - Group* group = nullptr; bool notify = false; @@ -111,4 +109,8 @@ struct Player : public Entity, public ICombatant { sNano* getActiveNano(); sPCAppearanceData getAppearanceData(); + bool hasQuestBoost() const; + bool hasHunterBoost() const; + bool hasRacerBoost() const; + bool hasSuperBoost() const; }; diff --git a/src/PlayerManager.cpp b/src/PlayerManager.cpp index 7f06ed4d..07bb13ab 100644 --- a/src/PlayerManager.cpp +++ b/src/PlayerManager.cpp @@ -243,9 +243,19 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) { // client doesnt read this, it gets it from charinfo // response.PCLoadData2CL.PCStyle2 = plr->PCStyle2; - // inventory - for (int i = 0; i < AEQUIP_COUNT; i++) + + // equipment (except nanocom boosters) + for (int i = 0; i < 9; i++) + response.PCLoadData2CL.aEquip[i] = plr->Equip[i]; + // equipment (nanocom boosters) + int32_t serverTime = getTime() / 1000UL; + int32_t timestamp = getTimestamp(); + for (int i = 9; i < AEQUIP_COUNT; i++) { response.PCLoadData2CL.aEquip[i] = plr->Equip[i]; + // client subtracts server time, then adds local timestamp to the item to print expiration time + response.PCLoadData2CL.aEquip[i].iTimeLimit = std::max(0, plr->Equip[i].iTimeLimit - timestamp + serverTime); + } + // inventory for (int i = 0; i < AINVEN_COUNT; i++) response.PCLoadData2CL.aInven[i] = plr->Inven[i]; // quest inventory @@ -384,7 +394,7 @@ static void loadPlayer(CNSocket* sock, CNPacketData* data) { Chat::sendServerMessage(sock, settings::MOTDSTRING); // MOTD Missions::failInstancedMissions(sock); // auto-fail missions Buddies::sendBuddyList(sock); // buddy list - Items::checkItemExpire(sock, plr); // vehicle expiration + Items::checkAndRemoveExpiredItems(sock, plr); // vehicle and booster expiration plr->initialLoadDone = true; } @@ -495,7 +505,6 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) { resp2.PCRegenDataForOtherPC.iAngle = plr->angle; if (plr->group != nullptr) { - resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->getCompositeCondition(); resp2.PCRegenDataForOtherPC.iPCState = plr->iPCState; resp2.PCRegenDataForOtherPC.iSpecialState = plr->iSpecialState; @@ -517,9 +526,7 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) { if (plr->instanceID != 0) return; - bool expired = plr->Equip[8].iTimeLimit < getTimestamp() && plr->Equip[8].iTimeLimit != 0; - - if (plr->Equip[8].iID > 0 && !expired) { + if (plr->Equip[8].iID > 0) { INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_SUCC, response); sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_SUCC); @@ -533,30 +540,6 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) { } else { INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_FAIL, response); sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_FAIL); - - // check if vehicle didn't expire - if (expired) { - plr->toRemoveVehicle.eIL = 0; - plr->toRemoveVehicle.iSlotNum = 8; - Items::checkItemExpire(sock, plr); - } - } -} - -static void exitPlayerVehicle(CNSocket* sock, CNPacketData* data) { - Player* plr = getPlayer(sock); - - if (plr->iPCState & 8) { - INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response); - sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC); - - // send to other players - plr->iPCState &= ~8; - INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, response2); - response2.iPC_ID = plr->iID; - response2.iState = plr->iPCState; - - sendToViewable(sock, response2, P_FE2CL_PC_STATE_CHANGE); } } @@ -607,6 +590,23 @@ static void setFirstUseFlag(CNSocket* sock, CNPacketData* data) { } #pragma region Helper methods +void PlayerManager::exitPlayerVehicle(CNSocket* sock, CNPacketData* data) { + Player* plr = getPlayer(sock); + + if (plr->iPCState & 8) { + INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response); + sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC); + + // send to other players + plr->iPCState &= ~8; + INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, response2); + response2.iPC_ID = plr->iID; + response2.iState = plr->iPCState; + + sendToViewable(sock, response2, P_FE2CL_PC_STATE_CHANGE); + } +} + Player *PlayerManager::getPlayer(CNSocket* key) { if (players.find(key) != players.end()) return players[key]; diff --git a/src/PlayerManager.hpp b/src/PlayerManager.hpp index ad9c7671..1fe78214 100644 --- a/src/PlayerManager.hpp +++ b/src/PlayerManager.hpp @@ -14,6 +14,8 @@ namespace PlayerManager { extern std::map players; void init(); + void exitPlayerVehicle(CNSocket* sock, CNPacketData* data); + void removePlayer(CNSocket* key); void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, uint64_t I, int angle); diff --git a/src/db/player.cpp b/src/db/player.cpp index 8396e1a3..ed01c67f 100644 --- a/src/db/player.cpp +++ b/src/db/player.cpp @@ -2,40 +2,6 @@ // Loading and saving players to/from the DB -static void removeExpiredVehicles(Player* player) { - int32_t currentTime = getTimestamp(); - - // if there are expired vehicles in bank just remove them silently - for (int i = 0; i < ABANK_COUNT; i++) { - if (player->Bank[i].iType == 10 && player->Bank[i].iTimeLimit < currentTime && player->Bank[i].iTimeLimit != 0) { - memset(&player->Bank[i], 0, sizeof(sItemBase)); - } - } - - // we want to leave only 1 expired vehicle on player to delete it with the client packet - std::vector toRemove; - - // equipped vehicle - if (player->Equip[8].iOpt > 0 && player->Equip[8].iTimeLimit < currentTime && player->Equip[8].iTimeLimit != 0) { - toRemove.push_back(&player->Equip[8]); - player->toRemoveVehicle.eIL = 0; - player->toRemoveVehicle.iSlotNum = 8; - } - // inventory - for (int i = 0; i < AINVEN_COUNT; i++) { - if (player->Inven[i].iType == 10 && player->Inven[i].iTimeLimit < currentTime && player->Inven[i].iTimeLimit != 0) { - toRemove.push_back(&player->Inven[i]); - player->toRemoveVehicle.eIL = 1; - player->toRemoveVehicle.iSlotNum = i; - } - } - - // delete all but one vehicles, leave last one for ceremonial deletion - for (int i = 0; i < (int)toRemove.size()-1; i++) { - memset(toRemove[i], 0, sizeof(sItemBase)); - } -} - void Database::getPlayer(Player* plr, int id) { std::lock_guard lock(dbCrit); @@ -160,8 +126,6 @@ void Database::getPlayer(Player* plr, int id) { sqlite3_finalize(stmt); - removeExpiredVehicles(plr); - // get quest inventory sql = R"( SELECT Slot, ID, Opt diff --git a/src/servers/CNShardServer.cpp b/src/servers/CNShardServer.cpp index c6d93b3e..de726323 100644 --- a/src/servers/CNShardServer.cpp +++ b/src/servers/CNShardServer.cpp @@ -9,6 +9,7 @@ #include "MobAI.hpp" #include "settings.hpp" #include "TableData.hpp" // for flush() +#include "Items.hpp" // for checkAndRemoveExpiredItems() #include #include @@ -23,6 +24,7 @@ CNShardServer::CNShardServer(uint16_t p) { pHandler = &CNShardServer::handlePacket; REGISTER_SHARD_TIMER(keepAliveTimer, 4000); REGISTER_SHARD_TIMER(periodicSaveTimer, settings::DBSAVEINTERVAL*1000); + REGISTER_SHARD_TIMER(periodicItemExpireTimer, 60000); init(); if (settings::MONITORENABLED) @@ -88,6 +90,22 @@ void CNShardServer::periodicSaveTimer(CNServer* serv, time_t currTime) { std::cout << "[INFO] Done." << std::endl; } +void CNShardServer::periodicItemExpireTimer(CNServer* serv, time_t currTime) { + size_t playersWithExpiredItems = 0; + size_t itemsRemoved = 0; + + for (const auto& [sock, player] : PlayerManager::players) { + // check and remove expired items + size_t removed = Items::checkAndRemoveExpiredItems(sock, player); + itemsRemoved += removed; + playersWithExpiredItems += (removed == 0 ? 0 : 1); + } + + if (playersWithExpiredItems > 0) { + std::cout << "[INFO] Removed " << itemsRemoved << " expired items from " << playersWithExpiredItems << " players." << std::endl; + } +} + bool CNShardServer::checkExtraSockets(int i) { return Monitor::acceptConnection(fds[i].fd, fds[i].revents); } diff --git a/src/servers/CNShardServer.hpp b/src/servers/CNShardServer.hpp index 7ed4e3f9..3f0f2b12 100644 --- a/src/servers/CNShardServer.hpp +++ b/src/servers/CNShardServer.hpp @@ -16,6 +16,7 @@ class CNShardServer : public CNServer { static void keepAliveTimer(CNServer*, time_t); static void periodicSaveTimer(CNServer* serv, time_t currTime); + static void periodicItemExpireTimer(CNServer* serv, time_t currTime); public: static std::map ShardPackets;