Nanocom Boosters and Authentic FM-Taro Scale Logic (#315)

* Groundwork for nanocom boosters

* The item use handler now has a switch for multiple item types (currently gumballs, and a stub for boosters)
* All item types are now checked for expiration, not just vehicles

* implement nanocom booster helpers, save and expiry

* implement authentic taro and fm modfication

* magic number and code refactor

* make sure only close by group members are counted

* add safe taro fm handling, rate command, race and mission booster logic

* add config option to disable authentic group scaling

* rename for consistency

* make rates percentages, fix chat message, add config options

* add config option to the ini file

* add index guard for hasBoost functions

* reorder config ini options

* add bank item expiry option

* fix trade oversight

---------

Co-authored-by: CakeLancelot <CakeLancelot@users.noreply.github.com>
This commit is contained in:
FinnHornhoover
2026-03-25 20:09:40 +03:00
committed by GitHub
parent 9a62ec61c9
commit 113bc0bc1b
26 changed files with 691 additions and 258 deletions

View File

@@ -43,10 +43,6 @@ simulatemobs=true
# little message players see when they enter the game # little message players see when they enter the game
motd=Welcome to OpenFusion! motd=Welcome to OpenFusion!
# The following are the default locations of the JSON files the server
# requires to run. You can override them by changing their values and
# uncommenting them (removing the leading # character from that line).
# Should drop fixes be enabled? # Should drop fixes be enabled?
# This will add drops to (mostly Academy-specific) mobs that don't have drops # This will add drops to (mostly Academy-specific) mobs that don't have drops
# and rearrange drop tables that are either unassigned or stranded in difficult to reach mobs # and rearrange drop tables that are either unassigned or stranded in difficult to reach mobs
@@ -54,6 +50,30 @@ motd=Welcome to OpenFusion!
# This is a polish option that is slightly inauthentic to the original game. # This is a polish option that is slightly inauthentic to the original game.
#dropfixesenabled=true #dropfixesenabled=true
# Should groups have to divide up gained Taros / FM among themselves?
# Taros is divided up, FM gets diminished per group member, roughly -12.5% per group member
# Original game worked like this. Uncomment below to disable this behavior.
#lesstarofmingroupdisabled=true
# General reward percentages
# You can change the rate of taro and fusion matter gains for all players.
# The numbers are in percentages, i.e. 1000 is 1000%. You should only use whole numbers, no decimals.
# Uncomment and change below to your desired rate. Defaults are 100%, regular gain rates.
#tarorate=100
#fusionmatterrate=100
# Should expired items in the bank disappear automatically?
# Original game let you kep expired items in the bank until you take them out.
# Uncomment below to enable this behavior.
#removeexpireditemsfrombank=true
# Should there be a score cap for infected zone races?
#izracescorecapped=true
# The following are the default locations of the JSON files the server
# requires to run. You can override them by changing their values and
# uncommenting them (removing the leading # character from that line).
# location of the tabledata folder # location of the tabledata folder
#tdatadir=tdata/ #tdatadir=tdata/
# location of the patch folder # location of the patch folder
@@ -79,9 +99,6 @@ motd=Welcome to OpenFusion!
# location of the database # location of the database
#dbpath=database.db #dbpath=database.db
# should there be a score cap for infected zone races?
#izracescorecapped=true
# should tutorial flags be disabled off the bat? # should tutorial flags be disabled off the bat?
disablefirstuseflag=true disablefirstuseflag=true

View File

@@ -170,11 +170,11 @@ static SkillResult handleSkillBatteryDrain(SkillData* skill, int power, ICombata
if(!blocked) { if(!blocked) {
boostDrain = (int)(skill->values[0][power] * scalingFactor); boostDrain = (int)(skill->values[0][power] * scalingFactor);
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW; if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
plr->batteryW -= boostDrain; plr->subtractCapped(CappedValueType::BATTERY_W, boostDrain);
potionDrain = (int)(skill->values[1][power] * scalingFactor); potionDrain = (int)(skill->values[1][power] * scalingFactor);
if(potionDrain > plr->batteryN) potionDrain = plr->batteryN; if(potionDrain > plr->batteryN) potionDrain = plr->batteryN;
plr->batteryN -= potionDrain; plr->subtractCapped(CappedValueType::BATTERY_N, potionDrain);
} }
sSkillResult_BatteryDrain result{}; sSkillResult_BatteryDrain result{};

View File

@@ -75,30 +75,22 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
case CN_GM_SET_VALUE_TYPE__HP: case CN_GM_SET_VALUE_TYPE__HP:
response.iSetValue = plr->HP = setData->iSetValue; response.iSetValue = plr->HP = setData->iSetValue;
break; break;
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY : case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY:
plr->batteryW = setData->iSetValue; plr->setCapped(CappedValueType::BATTERY_W, setData->iSetValue);
// caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
response.iSetValue = plr->batteryW; response.iSetValue = plr->batteryW;
break; break;
case CN_GM_SET_VALUE_TYPE__NANO_BATTERY: case CN_GM_SET_VALUE_TYPE__NANO_BATTERY:
plr->batteryN = setData->iSetValue; plr->setCapped(CappedValueType::BATTERY_N, setData->iSetValue);
// caps
if (plr->batteryN > 9999)
plr->batteryN = 9999;
response.iSetValue = plr->batteryN; response.iSetValue = plr->batteryN;
break; break;
case CN_GM_SET_VALUE_TYPE__FUSION_MATTER: case CN_GM_SET_VALUE_TYPE__FUSION_MATTER:
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter); plr->setCapped(CappedValueType::FUSIONMATTER, setData->iSetValue);
Missions::updateFusionMatter(sock);
response.iSetValue = plr->fusionmatter; response.iSetValue = plr->fusionmatter;
break; break;
case CN_GM_SET_VALUE_TYPE__CANDY: case CN_GM_SET_VALUE_TYPE__CANDY:
response.iSetValue = plr->money = setData->iSetValue; plr->setCapped(CappedValueType::TAROS, setData->iSetValue);
response.iSetValue = plr->money;
break; break;
case CN_GM_SET_VALUE_TYPE__SPEED: case CN_GM_SET_VALUE_TYPE__SPEED:
case CN_GM_SET_VALUE_TYPE__JUMP: case CN_GM_SET_VALUE_TYPE__JUMP:
@@ -148,6 +140,60 @@ static void setGMSpecialOnOff(CNSocket *sock, CNPacketData *data) {
// this is only used for muting players, so no need to update the client since that logic is server-side // this is only used for muting players, so no need to update the client since that logic is server-side
} }
static void setGMRewardRate(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock);
// access check
if (plr->accountLevel > 30)
return;
auto req = (sP_CL2FE_GM_REQ_REWARD_RATE*)data->buf;
if (req->iGetSet != 0) {
double *rate = nullptr;
switch (req->iRewardType) {
case REWARD_TYPE_TAROS:
rate = plr->rateT;
break;
case REWARD_TYPE_FUSIONMATTER:
rate = plr->rateF;
break;
}
if (rate == nullptr) {
std::cout << "[WARN] Invalid reward type for setGMRewardRate(): " << req->iRewardType << std::endl;
return;
}
if (req->iSetRateValue < 0 || req->iSetRateValue > 1000) {
std::cout << "[WARN] Invalid rate value for setGMRewardRate(): " << req->iSetRateValue << " (must be between 0 and 1000)" << std::endl;
return;
}
switch (req->iRewardRateIndex) {
case RATE_SLOT_ALL:
for (int i = 0; i < 5; i++)
rate[i] = req->iSetRateValue / 100.0;
break;
case RATE_SLOT_COMBAT:
case RATE_SLOT_MISSION:
case RATE_SLOT_EGG:
case RATE_SLOT_RACING:
rate[req->iRewardRateIndex] = req->iSetRateValue / 100.0;
break;
}
}
INITSTRUCT(sP_FE2CL_GM_REP_REWARD_RATE_SUCC, resp);
for (int i = 0; i < 5; i++) {
// double to float
resp.afRewardRate_Taros[i] = plr->rateT[i];
resp.afRewardRate_FusionMatter[i] = plr->rateF[i];
}
sock->sendPacket(resp, P_FE2CL_GM_REP_REWARD_RATE_SUCC);
}
static void locatePlayer(CNSocket *sock, CNPacketData *data) { static void locatePlayer(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
@@ -371,6 +417,7 @@ void BuiltinCommands::init() {
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH, setGMSpecialSwitchPlayer); REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH, setGMSpecialSwitchPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF, setGMSpecialOnOff); REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF, setGMSpecialOnOff);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_REWARD_RATE, setGMRewardRate);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_LOCATION, locatePlayer); REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_LOCATION, locatePlayer);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_KICK_PLAYER, kickPlayer); REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_KICK_PLAYER, kickPlayer);

View File

@@ -441,11 +441,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty), damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty); Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
if (plr->batteryW >= 6 + difficulty) plr->subtractCapped(CappedValueType::BATTERY_W, 6 + difficulty);
plr->batteryW -= 6 + difficulty;
else
plr->batteryW = 0;
damage.first = mob->takeDamage(sock, damage.first); damage.first = mob->takeDamage(sock, damage.first);
respdata[i].iID = mob->id; respdata[i].iID = mob->id;
@@ -690,10 +686,7 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty); Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
} }
if (plr->batteryW >= 6 + plr->level) plr->subtractCapped(CappedValueType::BATTERY_W, 6 + plr->level);
plr->batteryW -= 6 + plr->level;
else
plr->batteryW = 0;
damage.first = target->takeDamage(sock, damage.first); damage.first = target->takeDamage(sock, damage.first);
@@ -742,7 +735,7 @@ static int8_t addBullet(Player* plr, bool isGrenade) {
toAdd.weaponBoost = plr->batteryW > 0; toAdd.weaponBoost = plr->batteryW > 0;
if (toAdd.weaponBoost) { if (toAdd.weaponBoost) {
int boostCost = Rand::rand(11) + 20; int boostCost = Rand::rand(11) + 20;
plr->batteryW = boostCost > plr->batteryW ? 0 : plr->batteryW - boostCost; plr->subtractCapped(CappedValueType::BATTERY_W, boostCost);
} }
Bullets[plr->iID][findId] = toAdd; Bullets[plr->iID][findId] = toAdd;

View File

@@ -75,7 +75,7 @@ static void emailReceiveTaros(CNSocket* sock, CNPacketData* data) {
Database::EmailData email = Database::getEmail(plr->iID, pkt->iEmailIndex); Database::EmailData email = Database::getEmail(plr->iID, pkt->iEmailIndex);
// money transfer // money transfer
plr->money += email.Taros; plr->addCapped(CappedValueType::TAROS, email.Taros);
email.Taros = 0; email.Taros = 0;
// update Taros in email // update Taros in email
Database::updateEmailContent(&email); Database::updateEmailContent(&email);
@@ -274,7 +274,7 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
} }
int cost = pkt->iCash + 50 + 20 * attachments.size(); // attached taros + postage int cost = pkt->iCash + 50 + 20 * attachments.size(); // attached taros + postage
plr->money -= cost; plr->subtractCapped(CappedValueType::TAROS, cost);
Database::EmailData email = { Database::EmailData email = {
(int)pkt->iTo_PCUID, // PlayerId (int)pkt->iTo_PCUID, // PlayerId
Database::getNextEmailIndex(pkt->iTo_PCUID), // MsgIndex Database::getNextEmailIndex(pkt->iTo_PCUID), // MsgIndex
@@ -291,7 +291,7 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
}; };
if (!Database::sendEmail(&email, attachments, plr)) { if (!Database::sendEmail(&email, attachments, plr)) {
plr->money += cost; // give money back plr->addCapped(CappedValueType::TAROS, cost); // give money back
// give items back // give items back
while (!attachments.empty()) { while (!attachments.empty()) {
sItemBase attachment = attachments.back(); sItemBase attachment = attachments.back();

View File

@@ -117,6 +117,121 @@ sPCAppearanceData Player::getAppearanceData() {
return data; return data;
} }
bool Player::hasQuestBoost() const {
if (AEQUIP_COUNT < AEQUIP_COUNT_WITH_BOOSTERS)
return false;
const sItemBase& booster = Equip[10];
return booster.iID == 153 && booster.iOpt > 0;
}
bool Player::hasHunterBoost() const {
if (AEQUIP_COUNT < AEQUIP_COUNT_WITH_BOOSTERS)
return false;
const sItemBase& booster = Equip[11];
return booster.iID == 154 && booster.iOpt > 0;
}
bool Player::hasRacerBoost() const {
if (AEQUIP_COUNT < AEQUIP_COUNT_WITH_BOOSTERS)
return false;
const sItemBase& booster = Equip[9];
return booster.iID == 155 && booster.iOpt > 0;
}
bool Player::hasSuperBoost() const {
return Player::hasQuestBoost() && Player::hasHunterBoost() && Player::hasRacerBoost();
}
static int32_t getCap(CappedValueType type) {
switch (type) {
case CappedValueType::TAROS:
return PC_CANDY_MAX;
case CappedValueType::FUSIONMATTER:
return PC_FUSIONMATTER_MAX;
case CappedValueType::BATTERY_W:
return PC_BATTERY_MAX;
case CappedValueType::BATTERY_N:
return PC_BATTERY_MAX;
case CappedValueType::TAROS_IN_TRADE:
return PC_CANDY_MAX;
default:
return INT32_MAX;
}
}
static int32_t *getCappedValue(Player *player, CappedValueType type) {
switch (type) {
case CappedValueType::TAROS:
return &player->money;
case CappedValueType::FUSIONMATTER:
return &player->fusionmatter;
case CappedValueType::BATTERY_W:
return &player->batteryW;
case CappedValueType::BATTERY_N:
return &player->batteryN;
case CappedValueType::TAROS_IN_TRADE:
return &player->moneyInTrade;
default:
return nullptr;
}
}
void Player::addCapped(CappedValueType type, int32_t diff) {
if (diff <= 0)
return;
int32_t max = getCap(type);
int32_t *value = getCappedValue(this, type);
if (value == nullptr)
return;
if (diff > max)
diff = max;
if (*value + diff > max)
*value = max;
else
*value += diff;
}
void Player::subtractCapped(CappedValueType type, int32_t diff) {
if (diff <= 0)
return;
int32_t max = getCap(type);
int32_t *value = getCappedValue(this, type);
if (value == nullptr)
return;
if (diff > max)
diff = max;
if (*value - diff < 0)
*value = 0;
else
*value -= diff;
}
void Player::setCapped(CappedValueType type, int32_t value) {
int32_t max = getCap(type);
int32_t *valToSet = getCappedValue(this, type);
if (valToSet == nullptr)
return;
if (value < 0)
value = 0;
else if (value > max)
value = max;
*valToSet = value;
}
// TODO: this is less effiecient than it was, because of memset() // TODO: this is less effiecient than it was, because of memset()
void Player::enterIntoViewOf(CNSocket *sock) { void Player::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_PC_NEW, pkt); INITSTRUCT(sP_FE2CL_PC_NEW, pkt);

View File

@@ -33,6 +33,19 @@ std::map<int32_t, int32_t> Items::EventToDropMap;
std::map<int32_t, int32_t> Items::MobToDropMap; std::map<int32_t, int32_t> Items::MobToDropMap;
std::map<int32_t, ItemSet> Items::ItemSets; std::map<int32_t, ItemSet> Items::ItemSets;
// 1 week
#define NANOCOM_BOOSTER_DURATION 604800
// known general item ids
#define GENERALITEM_GUMBALL_ADAPTIUM 119
#define GENERALITEM_GUMBALL_BLASTONS 120
#define GENERALITEM_GUMBALL_COSMIX 121
#define GENERALITEM_FUSION_HUNTER_BOOSTER 153
#define GENERALITEM_IZ_RACER_BOOSTER 154
#define GENERALITEM_QUESTER_BOOSTER 155
#define GENERALITEM_SUPER_BOOSTER_DX 156
#ifdef ACADEMY #ifdef ACADEMY
std::map<int32_t, int32_t> Items::NanoCapsules; // crate id -> nano id std::map<int32_t, int32_t> Items::NanoCapsules; // crate id -> nano id
@@ -322,14 +335,14 @@ static void itemMoveHandler(CNSocket* sock, CNPacketData* data) {
// if equipping an item, validate that it's of the correct type for the slot // if equipping an item, validate that it's of the correct type for the slot
if ((SlotType)itemmove->eTo == SlotType::EQUIP) { if ((SlotType)itemmove->eTo == SlotType::EQUIP) {
if (fromItem->iType == 10 && itemmove->iToSlotNum != 8) if (fromItem->iType == 10 && itemmove->iToSlotNum != EQUIP_SLOT_VEHICLE)
return; // vehicle in wrong slot return; // vehicle in wrong slot
else if (fromItem->iType != 10 else if (fromItem->iType != 10
&& !(fromItem->iType == 0 && itemmove->iToSlotNum == 7) && !(fromItem->iType == 0 && itemmove->iToSlotNum == 7)
&& fromItem->iType != itemmove->iToSlotNum) && fromItem->iType != itemmove->iToSlotNum)
return; // something other than a vehicle or a weapon in a non-matching slot return; // something other than a vehicle or a weapon in a non-matching slot
else if (itemmove->iToSlotNum >= AEQUIP_COUNT) // TODO: reject slots >= 9? else if (itemmove->iToSlotNum >= AEQUIP_COUNT_MINUS_BOOSTERS)
return; // invalid slot return; // boosters can't be equipped via move packet
} }
// save items to response // save items to response
@@ -386,7 +399,7 @@ static void itemMoveHandler(CNSocket* sock, CNPacketData* data) {
} }
// unequip vehicle if equip slot 8 is 0 // unequip vehicle if equip slot 8 is 0
if (plr->Equip[8].iID == 0 && plr->iPCState & 8) { if (plr->Equip[EQUIP_SLOT_VEHICLE].iID == 0 && plr->iPCState & 8) {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response); INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC); sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC);
@@ -430,30 +443,19 @@ static void itemDeleteHandler(CNSocket* sock, CNPacketData* data) {
sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_DELETE_SUCC); 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; auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf;
Player* player = PlayerManager::getPlayer(sock); 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 // gumball can only be used from inventory, so we ignore eIL
sItemBase gumball = player->Inven[request->iSlotNum]; sItemBase gumball = player->Inven[request->iSlotNum];
sNano nano = player->Nanos[player->equippedNanos[request->iNanoSlot]]; 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 // sanity check, check if gumball type matches nano style
int nanoStyle = Nanos::nanoStyle(nano.iID); int nanoStyle = Nanos::nanoStyle(nano.iID);
if (!((gumball.iID == 119 && nanoStyle == 0) || if (!((gumball.iID == GENERALITEM_GUMBALL_ADAPTIUM && nanoStyle == 0) ||
( gumball.iID == 120 && nanoStyle == 1) || ( gumball.iID == GENERALITEM_GUMBALL_BLASTONS && nanoStyle == 1) ||
( gumball.iID == 121 && nanoStyle == 2))) { ( gumball.iID == GENERALITEM_GUMBALL_COSMIX && nanoStyle == 2))) {
std::cout << "[WARN] Gumball type doesn't match nano type" << std::endl; std::cout << "[WARN] Gumball type doesn't match nano type" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response); INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL); sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
@@ -472,11 +474,8 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
return; return;
} }
if (gumball.iOpt == 0) uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
gumball = {}; memset(respbuf, 0, resplen);
uint8_t respbuf[CN_PACKET_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
sP_FE2CL_REP_PC_ITEM_USE_SUCC *resp = (sP_FE2CL_REP_PC_ITEM_USE_SUCC*)respbuf; 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)); sSkillResult_Buff *respdata = (sSkillResult_Buff*)(respbuf+sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC));
@@ -515,6 +514,128 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
player->Inven[resp->iSlotNum] = resp->RemainItem; 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 < AEQUIP_COUNT_WITH_BOOSTERS) {
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];
// decide on the booster to activate
std::vector<int16_t> boosterIDs;
switch(item.iID) {
case GENERALITEM_FUSION_HUNTER_BOOSTER:
case GENERALITEM_IZ_RACER_BOOSTER:
case GENERALITEM_QUESTER_BOOSTER:
boosterIDs.push_back(item.iID);
break;
case GENERALITEM_SUPER_BOOSTER_DX:
boosterIDs.push_back(GENERALITEM_FUSION_HUNTER_BOOSTER);
boosterIDs.push_back(GENERALITEM_IZ_RACER_BOOSTER);
boosterIDs.push_back(GENERALITEM_QUESTER_BOOSTER);
break;
}
// consume item
item.iOpt -= 1;
if (item.iOpt == 0)
item = {};
// 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 };
// quester 155 -> 9, hunter 153 -> 10, racer 154 -> 11
int slot = 9 + ((itemID - GENERALITEM_FUSION_HUNTER_BOOSTER + 1) % 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 GENERALITEM_GUMBALL_ADAPTIUM:
case GENERALITEM_GUMBALL_BLASTONS:
case GENERALITEM_GUMBALL_COSMIX:
useGumball(sock, data);
break;
case GENERALITEM_FUSION_HUNTER_BOOSTER:
case GENERALITEM_IZ_RACER_BOOSTER:
case GENERALITEM_QUESTER_BOOSTER:
case GENERALITEM_SUPER_BOOSTER_DX:
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) { static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
@@ -598,7 +719,7 @@ static void chestOpenHandler(CNSocket *sock, CNPacketData *data) {
// if we failed to open a crate, at least give the player a gumball (suggested by Jade) // if we failed to open a crate, at least give the player a gumball (suggested by Jade)
if (failing) { if (failing) {
item->sItem.iType = 7; item->sItem.iType = 7;
item->sItem.iID = 119 + Rand::rand(3); item->sItem.iID = GENERALITEM_GUMBALL_ADAPTIUM + Rand::rand(3);
item->sItem.iOpt = 1; item->sItem.iOpt = 1;
std::cout << "[WARN] Crate open failed, giving a Gumball..." << std::endl; std::cout << "[WARN] Crate open failed, giving a Gumball..." << std::endl;
@@ -632,39 +753,89 @@ Item* Items::getItemData(int32_t id, int32_t type) {
return nullptr; return nullptr;
} }
void Items::checkItemExpire(CNSocket* sock, Player* player) { size_t Items::checkAndRemoveExpiredItems(CNSocket* sock, Player* player) {
if (player->toRemoveVehicle.eIL == 0 && player->toRemoveVehicle.iSlotNum == 0) int32_t currentTime = getTimestamp();
return;
/* prepare packet // if there are expired items in bank just remove them silently
* yes, this is a varadic packet, however analyzing client behavior and code if (settings::REMOVEEXPIREDITEMSFROMBANK) {
* it only checks takes the first item sent into account for (int i = 0; i < ABANK_COUNT; i++) {
* yes, this is very stupid if (player->Bank[i].iTimeLimit < currentTime && player->Bank[i].iTimeLimit != 0) {
* therefore, we delete all but 1 expired vehicle while loading player memset(&player->Bank[i], 0, sizeof(sItemBase));
* to delete the last one here so player gets a notification }
*/ }
}
const size_t resplen = sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL); // collect items to remove and data for the packet
std::vector<sItemBase*> toRemove;
std::vector<sTimeLimitItemDeleteInfo2CL> 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;
// 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); assert(resplen < CN_PACKET_BODY_SIZE);
// we know it's only one trailing struct, so we can skip full validation uint8_t respbuf[CN_PACKET_BODY_SIZE];
uint8_t respbuf[resplen]; // not a variable length array, don't worry 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)); auto packet = (sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM*)respbuf;
memset(respbuf, 0, resplen);
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++;
}
packet->iItemListCount = 1;
itemData->eIL = player->toRemoveVehicle.eIL;
itemData->iSlotNum = player->toRemoveVehicle.iSlotNum;
sock->sendPacket((void*)&respbuf, P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM, resplen); sock->sendPacket((void*)&respbuf, P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM, resplen);
// delete serverside // delete items serverside and send unequip packets
if (player->toRemoveVehicle.eIL == 0) for (size_t i = 0; i < itemData.size(); i++) {
memset(&player->Equip[8], 0, sizeof(sItemBase)); sItemBase* item = toRemove[i];
else memset(item, 0, sizeof(sItemBase));
memset(&player->Inven[player->toRemoveVehicle.iSlotNum], 0, sizeof(sItemBase));
player->toRemoveVehicle.eIL = 0; // send item delete success packet
player->toRemoveVehicle.iSlotNum = 0; // 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[EQUIP_SLOT_VEHICLE].iID == 0)
PlayerManager::exitPlayerVehicle(sock, nullptr);
return itemData.size();
} }
void Items::setItemStats(Player* plr) { void Items::setItemStats(Player* plr) {
@@ -711,7 +882,69 @@ static void getMobDrop(sItemBase* reward, const std::vector<int>& weights, const
reward->iID = crateIds[chosenIndex]; reward->iID = crateIds[chosenIndex];
} }
static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRoll& rolled) { static int32_t calculateTaroReward(Player* plr, int baseAmount, int groupSize) {
double bonus = plr->hasBuff(ECSB_REWARD_CASH) ? (Nanos::getNanoBoost(plr) ? 1.23 : 1.2) : 1.0;
double groupEffect = settings::LESSTAROFMINGROUPDISABLED ? 1.0 : 1.0 / groupSize;
return baseAmount * plr->rateT[RATE_SLOT_COMBAT] * bonus * groupEffect;
}
static int32_t calculateFMReward(Player* plr, int baseAmount, int levelDiff, int groupSize) {
double scavenge = 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;
// if player is within 1 level of the mob, FM is untouched
double levelEffect = 1.0;
// otherwise, follow the table below
switch (std::clamp(levelDiff, -3, 6)) {
case 6:
// if player is 6 or more levels above mob, no FM is dropped
levelEffect = 0.0;
break;
case 5:
levelEffect = 0.25;
break;
case 4:
levelEffect = 0.5;
break;
case 3:
levelEffect = 0.75;
break;
case 2:
levelEffect = 0.899;
break;
case -2:
levelEffect = 1.1;
break;
case -3:
// if player is 3 or more levels below mob, FM is 1.2x
levelEffect = 1.2;
break;
}
// if no group, FM is untouched
double groupEffect = 1.0;
// otherwise, follow the table below
if (!settings::LESSTAROFMINGROUPDISABLED) {
switch (groupSize) {
case 2:
groupEffect = 0.875;
break;
case 3:
groupEffect = 0.75;
break;
case 4:
// this case is more lenient
groupEffect = 0.688;
break;
}
}
int32_t amount = baseAmount * plr->rateF[RATE_SLOT_COMBAT] * scavenge * levelEffect * groupEffect;
amount *= boosterEffect;
return amount;
}
static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRoll& rolled, int groupSize) {
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward); const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
@@ -763,43 +996,20 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
MiscDropType& miscDropType = Items::MiscDropTypes[drop.miscDropTypeId]; MiscDropType& miscDropType = Items::MiscDropTypes[drop.miscDropTypeId];
if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) { if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) {
plr->money += miscDropType.taroAmount; int32_t taros = calculateTaroReward(plr, miscDropType.taroAmount, groupSize);
// money nano boost plr->addCapped(CappedValueType::TAROS, taros);
if (plr->hasBuff(ECSB_REWARD_CASH)) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
plr->money += miscDropType.taroAmount * (5 + boost) / 25;
}
} }
if (rolled.fm % miscDropChance.fmDropChanceTotal < miscDropChance.fmDropChance) { 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 levelDifference = plr->level - mob->level;
int fm = miscDropType.fmAmount; int32_t fm = calculateFMReward(plr, miscDropType.fmAmount, levelDifference, groupSize);
if (levelDifference > 0) plr->addCapped(CappedValueType::FUSIONMATTER, fm);
fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0; Missions::updateFusionMatter(sock);
// 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;
}
Missions::updateFusionMatter(sock, fm);
} }
if (rolled.potions % miscDropChance.potionDropChanceTotal < miscDropChance.potionDropChance) if (rolled.potions % miscDropChance.potionDropChanceTotal < miscDropChance.potionDropChance)
plr->batteryN += miscDropType.potionAmount; plr->addCapped(CappedValueType::BATTERY_N, miscDropType.potionAmount);
if (rolled.boosts % miscDropChance.boostDropChanceTotal < miscDropChance.boostDropChance) if (rolled.boosts % miscDropChance.boostDropChanceTotal < miscDropChance.boostDropChance)
plr->batteryW += miscDropType.boostAmount; plr->addCapped(CappedValueType::BATTERY_W, miscDropType.boostAmount);
// caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
if (plr->batteryN > 9999)
plr->batteryN = 9999;
// simple rewards // simple rewards
reward->m_iCandy = plr->money; reward->m_iCandy = plr->money;
@@ -830,7 +1040,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 // sanity check
if (Items::MobToDropMap.find(mob->type) == Items::MobToDropMap.end()) { if (Items::MobToDropMap.find(mob->type) == Items::MobToDropMap.end()) {
std::cout << "[WARN] Mob ID " << mob->type << " has no drops assigned" << std::endl; std::cout << "[WARN] Mob ID " << mob->type << " has no drops assigned" << std::endl;
@@ -839,7 +1049,7 @@ void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const
// find mob drop id // find mob drop id
int mobDropId = Items::MobToDropMap[mob->type]; int mobDropId = Items::MobToDropMap[mob->type];
giveSingleDrop(sock, mob, mobDropId, rolled); giveSingleDrop(sock, mob, mobDropId, rolled, groupSize);
if (settings::EVENTMODE != 0) { if (settings::EVENTMODE != 0) {
// sanity check // sanity check
@@ -850,14 +1060,13 @@ void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const
// find mob drop id // find mob drop id
int eventMobDropId = Items::EventToDropMap[settings::EVENTMODE]; int eventMobDropId = Items::EventToDropMap[settings::EVENTMODE];
giveSingleDrop(sock, mob, eventMobDropId, eventRolled); giveSingleDrop(sock, mob, eventMobDropId, eventRolled, groupSize);
} }
} }
void Items::init() { void Items::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_DELETE, itemDeleteHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_DELETE, itemDeleteHandler);
// this one is for gumballs
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_USE, itemUseHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_USE, itemUseHandler);
// Bank // Bank
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BANK_OPEN, itemBankOpenHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BANK_OPEN, itemBankOpenHandler);

View File

@@ -113,11 +113,11 @@ namespace Items {
void init(); void init();
// mob drops // 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); int findFreeSlot(Player *plr);
Item* getItemData(int32_t id, int32_t type); 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 setItemStats(Player* plr);
void updateEquips(CNSocket* sock, Player* plr); void updateEquips(CNSocket* sock, Player* plr);

View File

@@ -12,6 +12,20 @@ std::map<int32_t, Reward*> Missions::Rewards;
std::map<int32_t, TaskData*> Missions::Tasks; std::map<int32_t, TaskData*> Missions::Tasks;
nlohmann::json Missions::AvatarGrowth[37]; nlohmann::json Missions::AvatarGrowth[37];
static int32_t calculateTaroReward(Player* plr, int32_t baseAmount) {
double bonus = plr->hasBuff(ECSB_REWARD_CASH) ? (Nanos::getNanoBoost(plr) ? 1.23 : 1.2) : 1.0;
return baseAmount * plr->rateT[RATE_SLOT_MISSION] * bonus;
}
static int32_t calculateFMReward(Player* plr, int32_t baseAmount) {
double scavenge = plr->hasBuff(ECSB_REWARD_BLOB) ? (Nanos::getNanoBoost(plr) ? 1.23 : 1.2) : 1.0;
double missionBoost = plr->hasQuestBoost() ? (plr->hasHunterBoost() && plr->hasRacerBoost() ? 1.75 : 1.5) : 1.0;
int32_t reward = baseAmount * plr->rateF[RATE_SLOT_MISSION] * scavenge;
reward *= missionBoost;
return reward;
}
static void saveMission(Player* player, int missionId) { static void saveMission(Player* player, int missionId) {
// sanity check missionID so we don't get exceptions // sanity check missionID so we don't get exceptions
if (missionId < 0 || missionId > 1023) { if (missionId < 0 || missionId > 1023) {
@@ -162,21 +176,12 @@ static int giveMissionReward(CNSocket *sock, int task, int choice=0) {
memset(respbuf, 0, CN_PACKET_BODY_SIZE); memset(respbuf, 0, CN_PACKET_BODY_SIZE);
// update player // update player
plr->money += reward->money; int32_t money = calculateTaroReward(plr, reward->money);
if (plr->hasBuff(ECSB_REWARD_CASH)) { // nano boost for taros plr->addCapped(CappedValueType::TAROS, money);
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
plr->money += reward->money * (5 + boost) / 25;
}
if (plr->hasBuff(ECSB_REWARD_BLOB)) { // nano boost for fm int32_t fusionMatter = calculateFMReward(plr, reward->fusionmatter);
int boost = 0; plr->addCapped(CappedValueType::FUSIONMATTER, fusionMatter);
if (Nanos::getNanoBoost(plr)) // for gumballs Missions::updateFusionMatter(sock);
boost = 1;
updateFusionMatter(sock, reward->fusionmatter * (30 + boost) / 25);
} else
updateFusionMatter(sock, reward->fusionmatter);
// simple rewards // simple rewards
resp->m_iCandy = plr->money; resp->m_iCandy = plr->money;
@@ -511,17 +516,13 @@ void Missions::quitTask(CNSocket* sock, int32_t taskNum, bool manual) {
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_STOP_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_STOP_SUCC)); sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_STOP_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_STOP_SUCC));
} }
void Missions::updateFusionMatter(CNSocket* sock, int fusion) { void Missions::updateFusionMatter(CNSocket* sock) {
Player *plr = PlayerManager::getPlayer(sock); Player *plr = PlayerManager::getPlayer(sock);
plr->fusionmatter += fusion;
// there's a much lower FM cap in the Future // there's a much lower FM cap in the Future
int fmCap = AvatarGrowth[plr->level]["m_iFMLimit"]; int fmCap = AvatarGrowth[plr->level]["m_iFMLimit"];
if (plr->fusionmatter > fmCap) if (plr->fusionmatter > fmCap)
plr->fusionmatter = fmCap; plr->fusionmatter = fmCap;
else if (plr->fusionmatter < 0) // if somehow lowered too far
plr->fusionmatter = 0;
// don't run nano mission logic at level 36 // don't run nano mission logic at level 36
if (plr->level >= 36) if (plr->level >= 36)
@@ -551,7 +552,7 @@ void Missions::updateFusionMatter(CNSocket* sock, int fusion) {
response.iTaskNum = AvatarGrowth[plr->level]["m_iNanoQuestTaskID"]; response.iTaskNum = AvatarGrowth[plr->level]["m_iNanoQuestTaskID"];
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC)); sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
#else #else
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"]; plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"]);
plr->level++; plr->level++;
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, response); INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, response);

View File

@@ -47,7 +47,7 @@ namespace Missions {
bool startTask(Player* plr, int TaskID); bool startTask(Player* plr, int TaskID);
// checks if player doesn't have n/n quest items // checks if player doesn't have n/n quest items
void updateFusionMatter(CNSocket* sock, int fusion); void updateFusionMatter(CNSocket* sock);
void mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls); void mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls);

View File

@@ -808,18 +808,17 @@ void MobAI::onDeath(CombatNPC* npc, EntityRef src) {
Items::DropRoll rolled; Items::DropRoll rolled;
Items::DropRoll eventRolled; Items::DropRoll eventRolled;
std::map<int, int> qitemRolls; std::map<int, int> qitemRolls;
std::vector<EntityRef> playersInRange;
std::vector<Player*> playerRefs; std::vector<Player*> playerRefs;
if (plr->group == nullptr) { if (plr->group == nullptr) {
playerRefs.push_back(plr); playerRefs.push_back(plr);
Combat::genQItemRolls(playerRefs, qitemRolls); 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); Missions::mobKilled(src.sock, self->type, qitemRolls);
} }
else { else {
auto players = plr->group->filter(EntityKind::PLAYER); auto players = plr->group->filter(EntityKind::PLAYER);
for (EntityRef pRef : players) playerRefs.push_back(PlayerManager::getPlayer(pRef.sock));
Combat::genQItemRolls(playerRefs, qitemRolls);
for (int i = 0; i < players.size(); i++) { for (int i = 0; i < players.size(); i++) {
CNSocket* sockTo = players[i].sock; CNSocket* sockTo = players[i].sock;
Player* otherPlr = PlayerManager::getPlayer(sockTo); Player* otherPlr = PlayerManager::getPlayer(sockTo);
@@ -829,7 +828,14 @@ void MobAI::onDeath(CombatNPC* npc, EntityRef src) {
if (dist > 5000) if (dist > 5000)
continue; continue;
Items::giveMobDrop(sockTo, self, rolled, eventRolled); playersInRange.push_back(players[i]);
}
for (EntityRef pRef : playersInRange) playerRefs.push_back(PlayerManager::getPlayer(pRef.sock));
Combat::genQItemRolls(playerRefs, qitemRolls);
for (int i = 0; i < playersInRange.size(); i++) {
CNSocket* sockTo = playersInRange[i].sock;
Items::giveMobDrop(sockTo, self, rolled, eventRolled, playersInRange.size());
Missions::mobKilled(sockTo, self->type, qitemRolls); Missions::mobKilled(sockTo, self->type, qitemRolls);
} }
} }

View File

@@ -33,8 +33,10 @@ void Nanos::addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm)
*/ */
plr->level = level; plr->level = level;
if (spendfm) if (spendfm) {
Missions::updateFusionMatter(sock, -(int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]); plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]);
Missions::updateFusionMatter(sock);
}
#endif #endif
// Send to client // Send to client
@@ -143,7 +145,7 @@ static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
return; return;
#endif #endif
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"]; plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"]);
int reqItemCount = NanoTunings[skill->iTuneID].reqItemCount; int reqItemCount = NanoTunings[skill->iTuneID].reqItemCount;
int reqItemID = NanoTunings[skill->iTuneID].reqItems; int reqItemID = NanoTunings[skill->iTuneID].reqItems;
@@ -355,7 +357,7 @@ static void nanoPotionHandler(CNSocket* sock, CNPacketData* data) {
sock->sendPacket(response, P_FE2CL_REP_CHARGE_NANO_STAMINA); sock->sendPacket(response, P_FE2CL_REP_CHARGE_NANO_STAMINA);
// now update serverside // now update serverside
player->batteryN -= difference; player->subtractCapped(CappedValueType::BATTERY_N, difference);
player->Nanos[nano.iID].iStamina += difference; player->Nanos[nano.iID].iStamina += difference;
} }

View File

@@ -15,6 +15,14 @@ struct BuffStack;
#define PC_MAXHEALTH(level) (925 + 75 * (level)) #define PC_MAXHEALTH(level) (925 + 75 * (level))
enum class CappedValueType {
TAROS,
FUSIONMATTER,
BATTERY_W,
BATTERY_N,
TAROS_IN_TRADE,
};
struct Player : public Entity, public ICombatant { struct Player : public Entity, public ICombatant {
int accountId = 0; int accountId = 0;
int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums) int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums)
@@ -65,8 +73,6 @@ struct Player : public Entity, public ICombatant {
sItemBase QInven[AQINVEN_COUNT] = {}; sItemBase QInven[AQINVEN_COUNT] = {};
int32_t CurrentMissionID = 0; int32_t CurrentMissionID = 0;
sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {};
Group* group = nullptr; Group* group = nullptr;
bool notify = false; bool notify = false;
@@ -84,7 +90,14 @@ struct Player : public Entity, public ICombatant {
time_t lastShot = 0; time_t lastShot = 0;
std::vector<sItemBase> buyback = {}; std::vector<sItemBase> buyback = {};
Player() { kind = EntityKind::PLAYER; } double rateF[5] = { 1.0, 1.0, 1.0, 1.0, 1.0 };
double rateT[5] = { 1.0, 1.0, 1.0, 1.0, 1.0 };
Player() {
kind = EntityKind::PLAYER;
std::fill_n(rateF, 5, (double)settings::FUSIONMATTERRATE / 100.0);
std::fill_n(rateT, 5, (double)settings::TARORATE / 100.0);
}
virtual void enterIntoViewOf(CNSocket *sock) override; virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override;
@@ -111,4 +124,11 @@ struct Player : public Entity, public ICombatant {
sNano* getActiveNano(); sNano* getActiveNano();
sPCAppearanceData getAppearanceData(); sPCAppearanceData getAppearanceData();
bool hasQuestBoost() const;
bool hasHunterBoost() const;
bool hasRacerBoost() const;
bool hasSuperBoost() const;
void addCapped(CappedValueType type, int32_t diff);
void subtractCapped(CappedValueType type, int32_t diff);
void setCapped(CappedValueType type, int32_t value);
}; };

View File

@@ -243,9 +243,19 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
// client doesnt read this, it gets it from charinfo // client doesnt read this, it gets it from charinfo
// response.PCLoadData2CL.PCStyle2 = plr->PCStyle2; // response.PCLoadData2CL.PCStyle2 = plr->PCStyle2;
// inventory
for (int i = 0; i < AEQUIP_COUNT; i++) // equipment (except nanocom boosters)
for (int i = 0; i < AEQUIP_COUNT_MINUS_BOOSTERS; i++)
response.PCLoadData2CL.aEquip[i] = plr->Equip[i]; response.PCLoadData2CL.aEquip[i] = plr->Equip[i];
// equipment (nanocom boosters, loop only runs if boosters are available)
int32_t serverTime = getTime() / 1000UL;
int32_t timestamp = getTimestamp();
for (int i = AEQUIP_COUNT_MINUS_BOOSTERS; 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++) for (int i = 0; i < AINVEN_COUNT; i++)
response.PCLoadData2CL.aInven[i] = plr->Inven[i]; response.PCLoadData2CL.aInven[i] = plr->Inven[i];
// quest inventory // quest inventory
@@ -384,7 +394,7 @@ static void loadPlayer(CNSocket* sock, CNPacketData* data) {
Chat::sendServerMessage(sock, settings::MOTDSTRING); // MOTD Chat::sendServerMessage(sock, settings::MOTDSTRING); // MOTD
Missions::failInstancedMissions(sock); // auto-fail missions Missions::failInstancedMissions(sock); // auto-fail missions
Buddies::sendBuddyList(sock); // buddy list Buddies::sendBuddyList(sock); // buddy list
Items::checkItemExpire(sock, plr); // vehicle expiration Items::checkAndRemoveExpiredItems(sock, plr); // vehicle and booster expiration
plr->initialLoadDone = true; plr->initialLoadDone = true;
} }
@@ -495,7 +505,6 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
resp2.PCRegenDataForOtherPC.iAngle = plr->angle; resp2.PCRegenDataForOtherPC.iAngle = plr->angle;
if (plr->group != nullptr) { if (plr->group != nullptr) {
resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->getCompositeCondition(); resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->getCompositeCondition();
resp2.PCRegenDataForOtherPC.iPCState = plr->iPCState; resp2.PCRegenDataForOtherPC.iPCState = plr->iPCState;
resp2.PCRegenDataForOtherPC.iSpecialState = plr->iSpecialState; resp2.PCRegenDataForOtherPC.iSpecialState = plr->iSpecialState;
@@ -517,9 +526,7 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
if (plr->instanceID != 0) if (plr->instanceID != 0)
return; return;
bool expired = plr->Equip[8].iTimeLimit < getTimestamp() && plr->Equip[8].iTimeLimit != 0; if (plr->Equip[EQUIP_SLOT_VEHICLE].iID > 0) {
if (plr->Equip[8].iID > 0 && !expired) {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_SUCC, response); INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_SUCC, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_SUCC); sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_SUCC);
@@ -533,30 +540,6 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
} else { } else {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_FAIL, response); INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_FAIL, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_FAIL); 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);
} }
} }
@@ -585,7 +568,7 @@ static void changePlayerGuide(CNSocket *sock, CNPacketData *data) {
} }
// start Blossom nano mission if applicable // start Blossom nano mission if applicable
Missions::updateFusionMatter(sock, 0); Missions::updateFusionMatter(sock);
} }
// save it on player // save it on player
plr->mentor = pkt->iMentor; plr->mentor = pkt->iMentor;
@@ -607,6 +590,23 @@ static void setFirstUseFlag(CNSocket* sock, CNPacketData* data) {
} }
#pragma region Helper methods #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) { Player *PlayerManager::getPlayer(CNSocket* key) {
if (players.find(key) != players.end()) if (players.find(key) != players.end())
return players[key]; return players[key];

View File

@@ -14,6 +14,8 @@ namespace PlayerManager {
extern std::map<CNSocket*, Player*> players; extern std::map<CNSocket*, Player*> players;
void init(); void init();
void exitPlayerVehicle(CNSocket* sock, CNPacketData* data);
void removePlayer(CNSocket* key); void removePlayer(CNSocket* key);
void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, uint64_t I, int angle); void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, uint64_t I, int angle);

View File

@@ -7,6 +7,7 @@
#include "PlayerManager.hpp" #include "PlayerManager.hpp"
#include "Missions.hpp" #include "Missions.hpp"
#include "Items.hpp" #include "Items.hpp"
#include "Nanos.hpp"
using namespace Racing; using namespace Racing;
@@ -14,6 +15,15 @@ std::map<int32_t, EPInfo> Racing::EPData;
std::map<CNSocket*, EPRace> Racing::EPRaces; std::map<CNSocket*, EPRace> Racing::EPRaces;
std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> Racing::EPRewards; std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> Racing::EPRewards;
static int32_t calculateFMReward(Player* plr, int32_t baseAmount) {
double scavenge = plr->hasBuff(ECSB_REWARD_BLOB) ? (Nanos::getNanoBoost(plr) ? 1.23 : 1.2) : 1.0;
double raceBoost = plr->hasRacerBoost() ? (plr->hasHunterBoost() && plr->hasQuestBoost() ? 1.75 : 1.5) : 1.0;
int32_t reward = baseAmount * plr->rateF[RATE_SLOT_RACING] * scavenge;
reward *= raceBoost;
return reward;
}
static void racingStart(CNSocket* sock, CNPacketData* data) { static void racingStart(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_EP_RACE_START*)data->buf; auto req = (sP_CL2FE_REQ_EP_RACE_START*)data->buf;
@@ -155,7 +165,9 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
resp.iEPRaceMode = EPRaces[sock].mode; resp.iEPRaceMode = EPRaces[sock].mode;
resp.iEPRewardFM = fm; resp.iEPRewardFM = fm;
Missions::updateFusionMatter(sock, resp.iEPRewardFM); int32_t fmReward = calculateFMReward(plr, resp.iEPRewardFM);
plr->addCapped(CappedValueType::FUSIONMATTER, fmReward);
Missions::updateFusionMatter(sock);
resp.iFusionMatter = plr->fusionmatter; resp.iFusionMatter = plr->fusionmatter;
resp.iFatigue = 50; resp.iFatigue = 50;

View File

@@ -273,14 +273,16 @@ static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
resp2.iID_Request = pacdat->iID_Request; resp2.iID_Request = pacdat->iID_Request;
resp2.iID_From = pacdat->iID_From; resp2.iID_From = pacdat->iID_From;
resp2.iID_To = pacdat->iID_To; resp2.iID_To = pacdat->iID_To;
plr->money = plr->money + plr2->moneyInTrade - plr->moneyInTrade; plr->subtractCapped(CappedValueType::TAROS, plr->moneyInTrade);
plr->addCapped(CappedValueType::TAROS, plr2->moneyInTrade);
resp2.iCandy = plr->money; resp2.iCandy = plr->money;
memcpy(resp2.Item, plr2->Trade, sizeof(plr2->Trade)); memcpy(resp2.Item, plr2->Trade, sizeof(plr2->Trade));
memcpy(resp2.ItemStay, plr->Trade, sizeof(plr->Trade)); memcpy(resp2.ItemStay, plr->Trade, sizeof(plr->Trade));
sock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC)); sock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC));
plr2->money = plr2->money + plr->moneyInTrade - plr2->moneyInTrade; plr2->subtractCapped(CappedValueType::TAROS, plr2->moneyInTrade);
plr2->addCapped(CappedValueType::TAROS, plr->moneyInTrade);
resp2.iCandy = plr2->money; resp2.iCandy = plr2->money;
memcpy(resp2.Item, plr->Trade, sizeof(plr->Trade)); memcpy(resp2.Item, plr->Trade, sizeof(plr->Trade));
memcpy(resp2.ItemStay, plr2->Trade, sizeof(plr2->Trade)); memcpy(resp2.ItemStay, plr2->Trade, sizeof(plr2->Trade));
@@ -451,7 +453,7 @@ static void tradeRegisterCash(CNSocket* sock, CNPacketData* data) {
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC)); sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC));
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC)); otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC));
plr->moneyInTrade = pacdat->iCandy; plr->setCapped(CappedValueType::TAROS_IN_TRADE, pacdat->iCandy);
plr->isTradeConfirm = false; plr->isTradeConfirm = false;
} }

View File

@@ -117,7 +117,7 @@ static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
} }
TransportRoute route = Routes[req->iTransporationID]; TransportRoute route = Routes[req->iTransporationID];
plr->money -= route.cost; plr->subtractCapped(CappedValueType::TAROS, route.cost);
TransportLocation* target = nullptr; TransportLocation* target = nullptr;
switch (route.type) { switch (route.type) {
@@ -143,7 +143,7 @@ static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
} }
// refund and send alert packet // refund and send alert packet
plr->money += route.cost; plr->addCapped(CappedValueType::TAROS, route.cost);
INITSTRUCT(sP_FE2CL_ANNOUNCE_MSG, alert); INITSTRUCT(sP_FE2CL_ANNOUNCE_MSG, alert);
alert.iAnnounceType = 0; // don't think this lets us make a confirm dialog alert.iAnnounceType = 0; // don't think this lets us make a confirm dialog
alert.iDuringTime = 3; alert.iDuringTime = 3;

View File

@@ -72,7 +72,7 @@ static void vendorBuy(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC, resp);
plr->money = plr->money - itemCost; plr->subtractCapped(CappedValueType::TAROS, itemCost);
plr->Inven[slot] = req->Item; plr->Inven[slot] = req->Item;
resp.iCandy = plr->money; resp.iCandy = plr->money;
@@ -117,7 +117,7 @@ static void vendorSell(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC, resp);
// increment taros // increment taros
plr->money += itemData->sellPrice * req->iItemCnt; plr->addCapped(CappedValueType::TAROS, itemData->sellPrice * req->iItemCnt);
// modify item // modify item
if (item->iOpt - req->iItemCnt > 0) { // selling part of a stack if (item->iOpt - req->iItemCnt > 0) { // selling part of a stack
@@ -209,7 +209,7 @@ static void vendorBuyback(CNSocket* sock, CNPacketData* data) {
std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl; std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl;
} }
plr->money = plr->money - itemCost; plr->subtractCapped(CappedValueType::TAROS, itemCost);
plr->Inven[slot] = item; plr->Inven[slot] = item;
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, resp);
@@ -273,7 +273,7 @@ static void vendorBuyBattery(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock); Player* plr = PlayerManager::getPlayer(sock);
int cost = req->Item.iOpt * 100; 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 if ((req->Item.iID == 3 ? (plr->batteryW >= PC_BATTERY_MAX) : (plr->batteryN >= PC_BATTERY_MAX)) || plr->money < cost || req->Item.iOpt < 0) { // sanity check
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp);
failResp.iErrorCode = 0; failResp.iErrorCode = 0;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL); sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL);
@@ -281,17 +281,11 @@ static void vendorBuyBattery(CNSocket* sock, CNPacketData* data) {
} }
cost = plr->batteryW + plr->batteryN; cost = plr->batteryW + plr->batteryN;
plr->batteryW += req->Item.iID == 3 ? req->Item.iOpt * 100 : 0; plr->addCapped(CappedValueType::BATTERY_W, req->Item.iID == 3 ? req->Item.iOpt * 100 : 0);
plr->batteryN += req->Item.iID == 4 ? req->Item.iOpt * 100 : 0; plr->addCapped(CappedValueType::BATTERY_N, 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; cost = plr->batteryW + plr->batteryN - cost;
plr->money -= cost; plr->subtractCapped(CappedValueType::TAROS, cost);
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, resp);
@@ -364,7 +358,7 @@ static void vendorCombineItems(CNSocket* sock, CNPacketData* data) {
float rolled = Rand::randFloat(100.0f); // success chance out of 100 float rolled = Rand::randFloat(100.0f); // success chance out of 100
//std::cout << rolled << " vs " << successChance << std::endl; //std::cout << rolled << " vs " << successChance << std::endl;
plr->money -= cost; plr->subtractCapped(CappedValueType::TAROS, cost);
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_SUCC, resp); INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_SUCC, resp);

View File

@@ -67,4 +67,7 @@ void terminate(int);
#error Invalid PROTOCOL_VERSION #error Invalid PROTOCOL_VERSION
#endif #endif
#define AEQUIP_COUNT_MINUS_BOOSTERS 9
#define AEQUIP_COUNT_WITH_BOOSTERS 12
sSYSTEMTIME timeStampToStruct(uint64_t time); sSYSTEMTIME timeStampToStruct(uint64_t time);

View File

@@ -225,6 +225,15 @@ enum {
SIZEOF_NANO_TUNE_NEED_ITEM_SLOT = 10, SIZEOF_NANO_TUNE_NEED_ITEM_SLOT = 10,
VALUE_ATTACK_MISS = 1, VALUE_ATTACK_MISS = 1,
REWARD_TYPE_TAROS = 0,
REWARD_TYPE_FUSIONMATTER = 1,
RATE_SLOT_ALL = 0,
RATE_SLOT_COMBAT = 1,
RATE_SLOT_MISSION = 2,
RATE_SLOT_EGG = 3,
RATE_SLOT_RACING = 4,
MSG_ONLINE = 1, MSG_ONLINE = 1,
MSG_BUSY = 2, MSG_BUSY = 2,
MSG_OFFLINE = 0, MSG_OFFLINE = 0,

View File

@@ -2,40 +2,6 @@
// Loading and saving players to/from the DB // 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<sItemBase*> 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) { void Database::getPlayer(Player* plr, int id) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);
@@ -93,13 +59,13 @@ void Database::getPlayer(Player* plr, int id) {
plr->angle = sqlite3_column_int(stmt, 15); plr->angle = sqlite3_column_int(stmt, 15);
plr->HP = sqlite3_column_int(stmt, 16); plr->HP = sqlite3_column_int(stmt, 16);
plr->accountLevel = sqlite3_column_int(stmt, 17); plr->accountLevel = sqlite3_column_int(stmt, 17);
plr->fusionmatter = sqlite3_column_int(stmt, 18); plr->setCapped(CappedValueType::FUSIONMATTER, sqlite3_column_int(stmt, 18));
plr->money = sqlite3_column_int(stmt, 19); plr->setCapped(CappedValueType::TAROS, sqlite3_column_int(stmt, 19));
memcpy(plr->aQuestFlag, sqlite3_column_blob(stmt, 20), sizeof(plr->aQuestFlag)); memcpy(plr->aQuestFlag, sqlite3_column_blob(stmt, 20), sizeof(plr->aQuestFlag));
plr->batteryW = sqlite3_column_int(stmt, 21); plr->setCapped(CappedValueType::BATTERY_W, sqlite3_column_int(stmt, 21));
plr->batteryN = sqlite3_column_int(stmt, 22); plr->setCapped(CappedValueType::BATTERY_N, sqlite3_column_int(stmt, 22));
plr->mentor = sqlite3_column_int(stmt, 23); plr->mentor = sqlite3_column_int(stmt, 23);
plr->iWarpLocationFlag = sqlite3_column_int(stmt, 24); plr->iWarpLocationFlag = sqlite3_column_int(stmt, 24);
@@ -160,8 +126,6 @@ void Database::getPlayer(Player* plr, int id) {
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
removeExpiredVehicles(plr);
// get quest inventory // get quest inventory
sql = R"( sql = R"(
SELECT Slot, ID, Opt SELECT Slot, ID, Opt

View File

@@ -9,6 +9,7 @@
#include "MobAI.hpp" #include "MobAI.hpp"
#include "settings.hpp" #include "settings.hpp"
#include "TableData.hpp" // for flush() #include "TableData.hpp" // for flush()
#include "Items.hpp" // for checkAndRemoveExpiredItems()
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
@@ -23,6 +24,7 @@ CNShardServer::CNShardServer(uint16_t p) {
pHandler = &CNShardServer::handlePacket; pHandler = &CNShardServer::handlePacket;
REGISTER_SHARD_TIMER(keepAliveTimer, 4000); REGISTER_SHARD_TIMER(keepAliveTimer, 4000);
REGISTER_SHARD_TIMER(periodicSaveTimer, settings::DBSAVEINTERVAL*1000); REGISTER_SHARD_TIMER(periodicSaveTimer, settings::DBSAVEINTERVAL*1000);
REGISTER_SHARD_TIMER(periodicItemExpireTimer, 60000);
init(); init();
if (settings::MONITORENABLED) if (settings::MONITORENABLED)
@@ -88,6 +90,22 @@ void CNShardServer::periodicSaveTimer(CNServer* serv, time_t currTime) {
std::cout << "[INFO] Done." << std::endl; 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) { bool CNShardServer::checkExtraSockets(int i) {
return Monitor::acceptConnection(fds[i].fd, fds[i].revents); return Monitor::acceptConnection(fds[i].fd, fds[i].revents);
} }

View File

@@ -16,6 +16,7 @@ private:
static void keepAliveTimer(CNServer*, time_t); static void keepAliveTimer(CNServer*, time_t);
static void periodicSaveTimer(CNServer* serv, time_t currTime); static void periodicSaveTimer(CNServer* serv, time_t currTime);
static void periodicItemExpireTimer(CNServer* serv, time_t currTime);
public: public:
static std::map<uint32_t, PacketHandler> ShardPackets; static std::map<uint32_t, PacketHandler> ShardPackets;

View File

@@ -77,6 +77,16 @@ bool settings::IZRACESCORECAPPED = true;
// drop fixes enabled // drop fixes enabled
bool settings::DROPFIXESENABLED = false; bool settings::DROPFIXESENABLED = false;
// less taro / fm while in a group
bool settings::LESSTAROFMINGROUPDISABLED = false;
// general reward percentages
int settings::TARORATE = 100;
int settings::FUSIONMATTERRATE = 100;
// should expired items in the bank disappear automatically?
bool settings::REMOVEEXPIREDITEMSFROMBANK = false;
void settings::init() { void settings::init() {
INIReader reader("config.ini"); INIReader reader("config.ini");
@@ -121,11 +131,15 @@ void settings::init() {
PATCHDIR = reader.Get("shard", "patchdir", PATCHDIR); PATCHDIR = reader.Get("shard", "patchdir", PATCHDIR);
ENABLEDPATCHES = reader.Get("shard", "enabledpatches", ENABLEDPATCHES); ENABLEDPATCHES = reader.Get("shard", "enabledpatches", ENABLEDPATCHES);
DROPFIXESENABLED = reader.GetBoolean("shard", "dropfixesenabled", DROPFIXESENABLED); DROPFIXESENABLED = reader.GetBoolean("shard", "dropfixesenabled", DROPFIXESENABLED);
LESSTAROFMINGROUPDISABLED = reader.GetBoolean("shard", "lesstarofmingroupdisabled", LESSTAROFMINGROUPDISABLED);
TARORATE = reader.GetInteger("shard", "tarorate", TARORATE);
FUSIONMATTERRATE = reader.GetInteger("shard", "fusionmatterrate", FUSIONMATTERRATE);
ACCLEVEL = reader.GetInteger("shard", "accountlevel", ACCLEVEL); ACCLEVEL = reader.GetInteger("shard", "accountlevel", ACCLEVEL);
EVENTMODE = reader.GetInteger("shard", "eventmode", EVENTMODE); EVENTMODE = reader.GetInteger("shard", "eventmode", EVENTMODE);
DISABLEFIRSTUSEFLAG = reader.GetBoolean("shard", "disablefirstuseflag", DISABLEFIRSTUSEFLAG); DISABLEFIRSTUSEFLAG = reader.GetBoolean("shard", "disablefirstuseflag", DISABLEFIRSTUSEFLAG);
ANTICHEAT = reader.GetBoolean("shard", "anticheat", ANTICHEAT); ANTICHEAT = reader.GetBoolean("shard", "anticheat", ANTICHEAT);
IZRACESCORECAPPED = reader.GetBoolean("shard", "izracescorecapped", IZRACESCORECAPPED); IZRACESCORECAPPED = reader.GetBoolean("shard", "izracescorecapped", IZRACESCORECAPPED);
REMOVEEXPIREDITEMSFROMBANK = reader.GetBoolean("shard", "removeexpireditemsfrombank", REMOVEEXPIREDITEMSFROMBANK);
MONITORENABLED = reader.GetBoolean("monitor", "enabled", MONITORENABLED); MONITORENABLED = reader.GetBoolean("monitor", "enabled", MONITORENABLED);
MONITORPORT = reader.GetInteger("monitor", "port", MONITORPORT); MONITORPORT = reader.GetInteger("monitor", "port", MONITORPORT);
MONITORLISTENIP = reader.Get("monitor", "listenip", MONITORLISTENIP); MONITORLISTENIP = reader.Get("monitor", "listenip", MONITORLISTENIP);

View File

@@ -46,6 +46,10 @@ namespace settings {
extern bool DISABLEFIRSTUSEFLAG; extern bool DISABLEFIRSTUSEFLAG;
extern bool IZRACESCORECAPPED; extern bool IZRACESCORECAPPED;
extern bool DROPFIXESENABLED; extern bool DROPFIXESENABLED;
extern bool LESSTAROFMINGROUPDISABLED;
extern int TARORATE;
extern int FUSIONMATTERRATE;
extern bool REMOVEEXPIREDITEMSFROMBANK;
void init(); void init();
} }