Skip to content

Commit 67c6620

Browse files
implement nanocom booster helpers, save and expiry
1 parent 7ccd4ea commit 67c6620

9 files changed

Lines changed: 226 additions & 97 deletions

File tree

src/Entities.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,25 @@ sPCAppearanceData Player::getAppearanceData() {
117117
return data;
118118
}
119119

120+
bool Player::hasQuestBoost() const {
121+
const sItemBase& booster = Equip[10];
122+
return booster.iID == 153 && booster.iOpt > 0;
123+
}
124+
125+
bool Player::hasHunterBoost() const {
126+
const sItemBase& booster = Equip[11];
127+
return booster.iID == 154 && booster.iOpt > 0;
128+
}
129+
130+
bool Player::hasRacerBoost() const {
131+
const sItemBase& booster = Equip[9];
132+
return booster.iID == 155 && booster.iOpt > 0;
133+
}
134+
135+
bool Player::hasSuperBoost() const {
136+
return Player::hasQuestBoost() && Player::hasHunterBoost() && Player::hasRacerBoost();
137+
}
138+
120139
// TODO: this is less effiecient than it was, because of memset()
121140
void Player::enterIntoViewOf(CNSocket *sock) {
122141
INITSTRUCT(sP_FE2CL_PC_NEW, pkt);

src/Items.cpp

Lines changed: 150 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ std::map<int32_t, int32_t> Items::EventToDropMap;
3333
std::map<int32_t, int32_t> Items::MobToDropMap;
3434
std::map<int32_t, ItemSet> Items::ItemSets;
3535

36+
// 1 week
37+
#define NANOCOM_BOOSTER_DURATION 604800
38+
3639
#ifdef ACADEMY
3740
std::map<int32_t, int32_t> Items::NanoCapsules; // crate id -> nano id
3841

@@ -502,7 +505,81 @@ static void useGumball(CNSocket* sock, CNPacketData* data) {
502505
}
503506

504507
static void useNanocomBooster(CNSocket* sock, CNPacketData* data) {
508+
// Guard against using nanocom boosters in before and including 0104
509+
// either path should be optimized by the compiler, effectively a no-op
510+
if (AEQUIP_COUNT < 12) {
511+
std::cout << "[WARN] Nanocom Booster use not supported in this version" << std::endl;
512+
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, respFail);
513+
sock->sendPacket(respFail, P_FE2CL_REP_PC_ITEM_USE_FAIL);
514+
return;
515+
}
516+
517+
auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf;
518+
Player* player = PlayerManager::getPlayer(sock);
519+
sItemBase item = player->Inven[request->iSlotNum];
520+
521+
// consume item
522+
item.iOpt -= 1;
523+
if (item.iOpt == 0)
524+
item = {};
525+
526+
// decide on the booster to activate
527+
std::vector<int16_t> boosterIDs;
528+
switch(item.iID) {
529+
case 153:
530+
case 154:
531+
case 155:
532+
boosterIDs.push_back(item.iID);
533+
break;
534+
case 156:
535+
boosterIDs.push_back(153);
536+
boosterIDs.push_back(154);
537+
boosterIDs.push_back(155);
538+
break;
539+
}
540+
541+
// client wants to subtract server time in seconds from the time limit for display purposes
542+
int32_t timeLimitDisplayed = (getTime() / 1000UL) + NANOCOM_BOOSTER_DURATION;
543+
// in actuality we will use the timestamp of booster activation to the item time limit similar to vehicles
544+
// and this is how it will be saved to the database
545+
int32_t timeLimit = getTimestamp() + NANOCOM_BOOSTER_DURATION;
546+
547+
// give item(s) to inv slots
548+
for (int16_t itemID : boosterIDs) {
549+
sItemBase boosterItem = { 7, itemID, 1, timeLimitDisplayed };
550+
551+
// 155 -> 9, 153 -> 10, 154 -> 11
552+
int slot = 9 + ((itemID - 152) % 3);
553+
554+
// give item to the equip slot
555+
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp);
556+
resp.eIL = (int)SlotType::EQUIP;
557+
resp.iSlotNum = slot;
558+
resp.Item = boosterItem;
559+
sock->sendPacket(resp, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
560+
561+
// inform client of equip change (non visible so it's okay to just send to the player)
562+
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, equipChange);
563+
equipChange.iPC_ID = player->iID;
564+
equipChange.iEquipSlotNum = slot;
565+
equipChange.EquipSlotItem = boosterItem;
566+
sock->sendPacket(equipChange, P_FE2CL_PC_EQUIP_CHANGE);
567+
568+
boosterItem.iTimeLimit = timeLimit;
569+
// should replace existing booster in slot if it exists, i.e. you can refresh your boosters
570+
player->Equip[slot] = boosterItem;
571+
}
572+
573+
// send item use success packet
574+
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_SUCC, respUse);
575+
respUse.iPC_ID = player->iID;
576+
respUse.eIL = (int)SlotType::INVENTORY;
577+
respUse.iSlotNum = request->iSlotNum;
578+
respUse.RemainItem = item;
579+
sock->sendPacket(respUse, P_FE2CL_REP_PC_ITEM_USE_SUCC);
505580

581+
// update inventory serverside
582+
player->Inven[request->iSlotNum] = item;
506583
}
507584

508585
static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
@@ -666,39 +743,87 @@ Item* Items::getItemData(int32_t id, int32_t type) {
666743
return nullptr;
667744
}
668745

669-
void Items::checkItemExpire(CNSocket* sock, Player* player) {
670-
if (player->expiringItem.eIL == 0 && player->expiringItem.iSlotNum == 0)
671-
return;
746+
size_t Items::checkAndRemoveExpiredItems(CNSocket* sock, Player* player) {
747+
int32_t currentTime = getTimestamp();
748+
749+
// if there are expired items in bank just remove them silently
750+
for (int i = 0; i < ABANK_COUNT; i++) {
751+
if (player->Bank[i].iTimeLimit < currentTime && player->Bank[i].iTimeLimit != 0) {
752+
memset(&player->Bank[i], 0, sizeof(sItemBase));
753+
}
754+
}
755+
756+
// collect items to remove and data for the packet
757+
std::vector<sItemBase*> toRemove;
758+
std::vector<sTimeLimitItemDeleteInfo2CL> itemData;
759+
760+
// equipped items
761+
for (int i = 0; i < AEQUIP_COUNT; i++) {
762+
if (player->Equip[i].iOpt > 0 && player->Equip[i].iTimeLimit < currentTime && player->Equip[i].iTimeLimit != 0) {
763+
toRemove.push_back(&player->Equip[i]);
764+
itemData.push_back({ (int)SlotType::EQUIP, i });
765+
}
766+
}
767+
// inventory
768+
for (int i = 0; i < AINVEN_COUNT; i++) {
769+
if (player->Inven[i].iTimeLimit < currentTime && player->Inven[i].iTimeLimit != 0) {
770+
toRemove.push_back(&player->Inven[i]);
771+
itemData.push_back({ (int)SlotType::INVENTORY, i });
772+
}
773+
}
774+
775+
if (itemData.empty())
776+
return 0;
672777

673-
/* prepare packet
674-
* yes, this is a varadic packet, however analyzing client behavior and code
675-
* it only checks takes the first item sent into account
676-
* yes, this is very stupid
677-
* therefore, we delete all but 1 expired item while loading player
678-
* to delete the last one here so player gets a notification
679-
*/
778+
// prepare packet containing all expired items to delete
779+
// this is expected for academy
780+
// pre-academy only checks the first item in the packet
781+
const size_t resplen = sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL) * itemData.size();
680782

681-
const size_t resplen = sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL);
783+
// 8 bytes * 262 items = 2096 bytes, in total this shouldn't exceed 2500 bytes
682784
assert(resplen < CN_PACKET_BODY_SIZE);
683-
// we know it's only one trailing struct, so we can skip full validation
684-
uint8_t respbuf[resplen]; // not a variable length array, don't worry
785+
uint8_t respbuf[CN_PACKET_BODY_SIZE];
786+
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
787+
685788
auto packet = (sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM*)respbuf;
686-
sTimeLimitItemDeleteInfo2CL* itemData = (sTimeLimitItemDeleteInfo2CL*)(respbuf + sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM));
687-
memset(respbuf, 0, resplen);
688789

689-
packet->iItemListCount = 1;
690-
itemData->eIL = player->expiringItem.eIL;
691-
itemData->iSlotNum = player->expiringItem.iSlotNum;
790+
for (size_t i = 0; i < itemData.size(); i++) {
791+
auto itemToDeletePtr = (sTimeLimitItemDeleteInfo2CL*)(
792+
respbuf + sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL) * i
793+
);
794+
itemToDeletePtr->eIL = itemData[i].eIL;
795+
itemToDeletePtr->iSlotNum = itemData[i].iSlotNum;
796+
packet->iItemListCount++;
797+
}
798+
692799
sock->sendPacket((void*)&respbuf, P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM, resplen);
693800

694-
// delete serverside
695-
if (player->expiringItem.eIL == 0)
696-
memset(&player->Equip[player->expiringItem.iSlotNum], 0, sizeof(sItemBase));
697-
else
698-
memset(&player->Inven[player->expiringItem.iSlotNum], 0, sizeof(sItemBase));
801+
// delete items serverside and send unequip packets
802+
for (size_t i = 0; i < itemData.size(); i++) {
803+
sItemBase* item = toRemove[i];
804+
memset(item, 0, sizeof(sItemBase));
805+
806+
// send item delete success packet
807+
// required for pre-academy builds
808+
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_DELETE_SUCC, itemDelete);
809+
itemDelete.eIL = itemData[i].eIL;
810+
itemDelete.iSlotNum = itemData[i].iSlotNum;
811+
sock->sendPacket(itemDelete, P_FE2CL_REP_PC_ITEM_DELETE_SUCC);
812+
813+
// also update item equips if needed
814+
if (itemData[i].eIL == (int)SlotType::EQUIP) {
815+
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, equipChange);
816+
equipChange.iPC_ID = player->iID;
817+
equipChange.iEquipSlotNum = itemData[i].iSlotNum;
818+
sock->sendPacket(equipChange, P_FE2CL_PC_EQUIP_CHANGE);
819+
}
820+
}
821+
822+
// exit vehicle if player no longer has one equipped (function checks pcstyle)
823+
if (player->Equip[8].iID == 0)
824+
PlayerManager::exitPlayerVehicle(sock, nullptr);
699825

700-
player->expiringItem.eIL = 0;
701-
player->expiringItem.iSlotNum = 0;
826+
return itemData.size();
702827
}
703828

704829
void Items::setItemStats(Player* plr) {

src/Items.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ namespace Items {
117117

118118
int findFreeSlot(Player *plr);
119119
Item* getItemData(int32_t id, int32_t type);
120-
void checkItemExpire(CNSocket* sock, Player* player);
120+
size_t checkAndRemoveExpiredItems(CNSocket* sock, Player* player);
121121
void setItemStats(Player* plr);
122122
void updateEquips(CNSocket* sock, Player* plr);
123123

src/Player.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ struct Player : public Entity, public ICombatant {
6565
sItemBase QInven[AQINVEN_COUNT] = {};
6666
int32_t CurrentMissionID = 0;
6767

68-
sTimeLimitItemDeleteInfo2CL expiringItem = {};
69-
7068
Group* group = nullptr;
7169

7270
bool notify = false;
@@ -111,4 +109,8 @@ struct Player : public Entity, public ICombatant {
111109

112110
sNano* getActiveNano();
113111
sPCAppearanceData getAppearanceData();
112+
bool hasQuestBoost() const;
113+
bool hasHunterBoost() const;
114+
bool hasRacerBoost() const;
115+
bool hasSuperBoost() const;
114116
};

src/PlayerManager.cpp

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,19 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
243243

244244
// client doesnt read this, it gets it from charinfo
245245
// response.PCLoadData2CL.PCStyle2 = plr->PCStyle2;
246-
// inventory
247-
for (int i = 0; i < AEQUIP_COUNT; i++)
246+
247+
// equipment (except nanocom boosters)
248+
for (int i = 0; i < 9; i++)
249+
response.PCLoadData2CL.aEquip[i] = plr->Equip[i];
250+
// equipment (nanocom boosters)
251+
int32_t serverTime = getTime() / 1000UL;
252+
int32_t timestamp = getTimestamp();
253+
for (int i = 9; i < AEQUIP_COUNT; i++) {
248254
response.PCLoadData2CL.aEquip[i] = plr->Equip[i];
255+
// client subtracts server time, then adds local timestamp to the item to print expiration time
256+
response.PCLoadData2CL.aEquip[i].iTimeLimit = std::max(0, plr->Equip[i].iTimeLimit - timestamp + serverTime);
257+
}
258+
// inventory
249259
for (int i = 0; i < AINVEN_COUNT; i++)
250260
response.PCLoadData2CL.aInven[i] = plr->Inven[i];
251261
// quest inventory
@@ -384,7 +394,7 @@ static void loadPlayer(CNSocket* sock, CNPacketData* data) {
384394
Chat::sendServerMessage(sock, settings::MOTDSTRING); // MOTD
385395
Missions::failInstancedMissions(sock); // auto-fail missions
386396
Buddies::sendBuddyList(sock); // buddy list
387-
Items::checkItemExpire(sock, plr); // vehicle expiration
397+
Items::checkAndRemoveExpiredItems(sock, plr); // vehicle and booster expiration
388398

389399
plr->initialLoadDone = true;
390400
}
@@ -495,7 +505,6 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
495505
resp2.PCRegenDataForOtherPC.iAngle = plr->angle;
496506

497507
if (plr->group != nullptr) {
498-
499508
resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->getCompositeCondition();
500509
resp2.PCRegenDataForOtherPC.iPCState = plr->iPCState;
501510
resp2.PCRegenDataForOtherPC.iSpecialState = plr->iSpecialState;
@@ -517,9 +526,7 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
517526
if (plr->instanceID != 0)
518527
return;
519528

520-
bool expired = plr->Equip[8].iTimeLimit < getTimestamp() && plr->Equip[8].iTimeLimit != 0;
521-
522-
if (plr->Equip[8].iID > 0 && !expired) {
529+
if (plr->Equip[8].iID > 0) {
523530
INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_SUCC, response);
524531
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_SUCC);
525532

@@ -533,30 +540,6 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
533540
} else {
534541
INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_FAIL, response);
535542
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_FAIL);
536-
537-
// check if vehicle didn't expire
538-
if (expired) {
539-
plr->expiringItem.eIL = 0;
540-
plr->expiringItem.iSlotNum = 8;
541-
Items::checkItemExpire(sock, plr);
542-
}
543-
}
544-
}
545-
546-
static void exitPlayerVehicle(CNSocket* sock, CNPacketData* data) {
547-
Player* plr = getPlayer(sock);
548-
549-
if (plr->iPCState & 8) {
550-
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response);
551-
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC);
552-
553-
// send to other players
554-
plr->iPCState &= ~8;
555-
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, response2);
556-
response2.iPC_ID = plr->iID;
557-
response2.iState = plr->iPCState;
558-
559-
sendToViewable(sock, response2, P_FE2CL_PC_STATE_CHANGE);
560543
}
561544
}
562545

@@ -607,6 +590,23 @@ static void setFirstUseFlag(CNSocket* sock, CNPacketData* data) {
607590
}
608591

609592
#pragma region Helper methods
593+
void PlayerManager::exitPlayerVehicle(CNSocket* sock, CNPacketData* data) {
594+
Player* plr = getPlayer(sock);
595+
596+
if (plr->iPCState & 8) {
597+
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response);
598+
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC);
599+
600+
// send to other players
601+
plr->iPCState &= ~8;
602+
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, response2);
603+
response2.iPC_ID = plr->iID;
604+
response2.iState = plr->iPCState;
605+
606+
sendToViewable(sock, response2, P_FE2CL_PC_STATE_CHANGE);
607+
}
608+
}
609+
610610
Player *PlayerManager::getPlayer(CNSocket* key) {
611611
if (players.find(key) != players.end())
612612
return players[key];

src/PlayerManager.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace PlayerManager {
1414
extern std::map<CNSocket*, Player*> players;
1515
void init();
1616

17+
void exitPlayerVehicle(CNSocket* sock, CNPacketData* data);
18+
1719
void removePlayer(CNSocket* key);
1820

1921
void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, uint64_t I, int angle);

0 commit comments

Comments
 (0)