From 7ccd4eaf8ebc9553147dc6f14967227e4d735408 Mon Sep 17 00:00:00 2001 From: CakeLancelot Date: Tue, 4 May 2021 10:58:06 -0500 Subject: [PATCH 1/7] 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 --- src/Items.cpp | 91 +++++++++++++++++++++++++++++-------------- src/Player.hpp | 2 +- src/PlayerManager.cpp | 4 +- src/db/player.cpp | 30 +++++++------- 4 files changed, 81 insertions(+), 46 deletions(-) diff --git a/src/Items.cpp b/src/Items.cpp index b11c08628..d3a36da68 100644 --- a/src/Items.cpp +++ b/src/Items.cpp @@ -328,8 +328,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 +430,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 +461,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 +501,54 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) { player->Inven[resp->iSlotNum] = resp->RemainItem; } +static void useNanocomBooster(CNSocket* sock, CNPacketData* data) { + +} + +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); @@ -633,14 +667,14 @@ Item* Items::getItemData(int32_t id, int32_t type) { } void Items::checkItemExpire(CNSocket* sock, Player* player) { - if (player->toRemoveVehicle.eIL == 0 && player->toRemoveVehicle.iSlotNum == 0) + if (player->expiringItem.eIL == 0 && player->expiringItem.iSlotNum == 0) return; /* 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 + * therefore, we delete all but 1 expired item while loading player * to delete the last one here so player gets a notification */ @@ -653,18 +687,18 @@ void Items::checkItemExpire(CNSocket* sock, Player* player) { memset(respbuf, 0, resplen); packet->iItemListCount = 1; - itemData->eIL = player->toRemoveVehicle.eIL; - itemData->iSlotNum = player->toRemoveVehicle.iSlotNum; + itemData->eIL = player->expiringItem.eIL; + itemData->iSlotNum = player->expiringItem.iSlotNum; 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)); + if (player->expiringItem.eIL == 0) + memset(&player->Equip[player->expiringItem.iSlotNum], 0, sizeof(sItemBase)); else - memset(&player->Inven[player->toRemoveVehicle.iSlotNum], 0, sizeof(sItemBase)); + memset(&player->Inven[player->expiringItem.iSlotNum], 0, sizeof(sItemBase)); - player->toRemoveVehicle.eIL = 0; - player->toRemoveVehicle.iSlotNum = 0; + player->expiringItem.eIL = 0; + player->expiringItem.iSlotNum = 0; } void Items::setItemStats(Player* plr) { @@ -857,7 +891,6 @@ void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const 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/Player.hpp b/src/Player.hpp index edf974880..a160ad9f0 100644 --- a/src/Player.hpp +++ b/src/Player.hpp @@ -65,7 +65,7 @@ struct Player : public Entity, public ICombatant { sItemBase QInven[AQINVEN_COUNT] = {}; int32_t CurrentMissionID = 0; - sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {}; + sTimeLimitItemDeleteInfo2CL expiringItem = {}; Group* group = nullptr; diff --git a/src/PlayerManager.cpp b/src/PlayerManager.cpp index 7f06ed4d0..635d40cb7 100644 --- a/src/PlayerManager.cpp +++ b/src/PlayerManager.cpp @@ -536,8 +536,8 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) { // check if vehicle didn't expire if (expired) { - plr->toRemoveVehicle.eIL = 0; - plr->toRemoveVehicle.iSlotNum = 8; + plr->expiringItem.eIL = 0; + plr->expiringItem.iSlotNum = 8; Items::checkItemExpire(sock, plr); } } diff --git a/src/db/player.cpp b/src/db/player.cpp index 8396e1a37..c4afbeb87 100644 --- a/src/db/player.cpp +++ b/src/db/player.cpp @@ -2,35 +2,37 @@ // Loading and saving players to/from the DB -static void removeExpiredVehicles(Player* player) { +static void removeExpiredItems(Player* player) { int32_t currentTime = getTimestamp(); - // if there are expired vehicles in bank just remove them silently + // if there are expired items 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) { + if (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 + // we want to leave only 1 expired item 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; + // 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]); + player->expiringItem.eIL = 0; + player->expiringItem.iSlotNum = i; + } } // 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) { + if (player->Inven[i].iTimeLimit < currentTime && player->Inven[i].iTimeLimit != 0) { toRemove.push_back(&player->Inven[i]); - player->toRemoveVehicle.eIL = 1; - player->toRemoveVehicle.iSlotNum = i; + player->expiringItem.eIL = 1; + player->expiringItem.iSlotNum = i; } } - // delete all but one vehicles, leave last one for ceremonial deletion + // delete all but one item, leave last one for ceremonial deletion for (int i = 0; i < (int)toRemove.size()-1; i++) { memset(toRemove[i], 0, sizeof(sItemBase)); } @@ -160,7 +162,7 @@ void Database::getPlayer(Player* plr, int id) { sqlite3_finalize(stmt); - removeExpiredVehicles(plr); + removeExpiredItems(plr); // get quest inventory sql = R"( From 67c6620fb07edb6d01f13ef3e1f3f0934539de27 Mon Sep 17 00:00:00 2001 From: FinnHornhoover Date: Sat, 7 Feb 2026 02:31:55 +0300 Subject: [PATCH 2/7] implement nanocom booster helpers, save and expiry --- src/Entities.cpp | 19 ++++ src/Items.cpp | 175 +++++++++++++++++++++++++++++----- src/Items.hpp | 2 +- src/Player.hpp | 6 +- src/PlayerManager.cpp | 62 ++++++------ src/PlayerManager.hpp | 2 + src/db/player.cpp | 38 -------- src/servers/CNShardServer.cpp | 18 ++++ src/servers/CNShardServer.hpp | 1 + 9 files changed, 226 insertions(+), 97 deletions(-) diff --git a/src/Entities.cpp b/src/Entities.cpp index c3532ded1..a9621d719 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 d3a36da68..5bc578781 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 @@ -502,7 +505,81 @@ static void useGumball(CNSocket* sock, CNPacketData* data) { } 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) { @@ -666,39 +743,87 @@ Item* Items::getItemData(int32_t id, int32_t type) { return nullptr; } -void Items::checkItemExpire(CNSocket* sock, Player* player) { - if (player->expiringItem.eIL == 0 && player->expiringItem.iSlotNum == 0) - return; +size_t Items::checkAndRemoveExpiredItems(CNSocket* sock, Player* player) { + int32_t currentTime = getTimestamp(); + + // 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; - /* 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 item while loading player - * to delete the last one here so player gets a notification - */ + // 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(); - const size_t resplen = sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL); + // 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->expiringItem.eIL; - itemData->iSlotNum = player->expiringItem.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->expiringItem.eIL == 0) - memset(&player->Equip[player->expiringItem.iSlotNum], 0, sizeof(sItemBase)); - else - memset(&player->Inven[player->expiringItem.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->expiringItem.eIL = 0; - player->expiringItem.iSlotNum = 0; + return itemData.size(); } void Items::setItemStats(Player* plr) { diff --git a/src/Items.hpp b/src/Items.hpp index 2156b92e1..3ed2f16a9 100644 --- a/src/Items.hpp +++ b/src/Items.hpp @@ -117,7 +117,7 @@ namespace Items { 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/Player.hpp b/src/Player.hpp index a160ad9f0..59deaef57 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 expiringItem = {}; - 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 635d40cb7..07bb13ab9 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->expiringItem.eIL = 0; - plr->expiringItem.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 ad9c76712..1fe782142 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 c4afbeb87..ed01c67f6 100644 --- a/src/db/player.cpp +++ b/src/db/player.cpp @@ -2,42 +2,6 @@ // Loading and saving players to/from the DB -static void removeExpiredItems(Player* player) { - int32_t currentTime = getTimestamp(); - - // 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)); - } - } - - // we want to leave only 1 expired item on player to delete it with the client packet - std::vector toRemove; - - // 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]); - player->expiringItem.eIL = 0; - player->expiringItem.iSlotNum = 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]); - player->expiringItem.eIL = 1; - player->expiringItem.iSlotNum = i; - } - } - - // delete all but one item, 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); @@ -162,8 +126,6 @@ void Database::getPlayer(Player* plr, int id) { sqlite3_finalize(stmt); - removeExpiredItems(plr); - // get quest inventory sql = R"( SELECT Slot, ID, Opt diff --git a/src/servers/CNShardServer.cpp b/src/servers/CNShardServer.cpp index c6d93b3e7..de7263238 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 7ed4e3f99..3f0f2b12a 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; From e29a931e43161db9d39869adf9124c7ead4a7d2d Mon Sep 17 00:00:00 2001 From: FinnHornhoover Date: Sat, 7 Feb 2026 02:33:42 +0300 Subject: [PATCH 3/7] implement authentic taro and fm modfication --- src/Items.cpp | 92 +++++++++++++++++++++++++++++++++++++-------------- src/Items.hpp | 2 +- src/MobAI.cpp | 4 +-- 3 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/Items.cpp b/src/Items.cpp index 5bc578781..f39cd7b92 100644 --- a/src/Items.cpp +++ b/src/Items.cpp @@ -870,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); @@ -922,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); } @@ -989,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; @@ -998,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 @@ -1009,7 +1051,7 @@ 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); } } diff --git a/src/Items.hpp b/src/Items.hpp index 3ed2f16a9..9a73a79ff 100644 --- a/src/Items.hpp +++ b/src/Items.hpp @@ -113,7 +113,7 @@ 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); diff --git a/src/MobAI.cpp b/src/MobAI.cpp index 50c87422e..be3678837 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); } } From 1896861ba4d27e1592623c56b815dda2bc5cdfab Mon Sep 17 00:00:00 2001 From: FinnHornhoover Date: Thu, 19 Feb 2026 05:48:19 +0300 Subject: [PATCH 4/7] magic number and code refactor --- src/Items.cpp | 144 ++++++++++++++++++++++------------------- src/PlayerManager.cpp | 8 +-- src/core/CNStructs.hpp | 6 +- 3 files changed, 86 insertions(+), 72 deletions(-) diff --git a/src/Items.cpp b/src/Items.cpp index f39cd7b92..cce371e95 100644 --- a/src/Items.cpp +++ b/src/Items.cpp @@ -36,6 +36,16 @@ std::map 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 std::map Items::NanoCapsules; // crate id -> nano id @@ -325,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 ((SlotType)itemmove->eTo == SlotType::EQUIP) { - if (fromItem->iType == 10 && itemmove->iToSlotNum != 8) + if (fromItem->iType == 10 && itemmove->iToSlotNum != AEQUIP_VEHICLE_IDX) return; // vehicle in wrong slot else if (fromItem->iType != 10 && !(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 > 8) - return; // any slot higher than 8 is for a booster, and they can't be equipped via move packet + else if (itemmove->iToSlotNum >= AEQUIP_COUNT_MINUS_BOOSTERS) + return; // boosters can't be equipped via move packet } // save items to response @@ -389,7 +399,7 @@ static void itemMoveHandler(CNSocket* sock, CNPacketData* data) { } // unequip vehicle if equip slot 8 is 0 - if (plr->Equip[8].iID == 0 && plr->iPCState & 8) { + if (plr->Equip[AEQUIP_VEHICLE_IDX].iID == 0 && plr->iPCState & 8) { INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response); sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC); @@ -443,9 +453,9 @@ static void useGumball(CNSocket* sock, CNPacketData* data) { // sanity check, check if gumball type matches nano style int nanoStyle = Nanos::nanoStyle(nano.iID); - if (!((gumball.iID == 119 && nanoStyle == 0) || - ( gumball.iID == 120 && nanoStyle == 1) || - ( gumball.iID == 121 && nanoStyle == 2))) { + if (!((gumball.iID == GENERALITEM_GUMBALL_ADAPTIUM && nanoStyle == 0) || + ( gumball.iID == GENERALITEM_GUMBALL_BLASTONS && nanoStyle == 1) || + ( gumball.iID == GENERALITEM_GUMBALL_COSMIX && nanoStyle == 2))) { std::cout << "[WARN] Gumball type doesn't match nano type" << std::endl; INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response); sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL); @@ -507,7 +517,7 @@ static void useGumball(CNSocket* sock, CNPacketData* data) { 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) { + 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); @@ -518,26 +528,26 @@ static void useNanocomBooster(CNSocket* sock, CNPacketData* data) { 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: + case GENERALITEM_FUSION_HUNTER_BOOSTER: + case GENERALITEM_IZ_RACER_BOOSTER: + case GENERALITEM_QUESTER_BOOSTER: boosterIDs.push_back(item.iID); break; - case 156: - boosterIDs.push_back(153); - boosterIDs.push_back(154); - boosterIDs.push_back(155); + 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 @@ -548,8 +558,8 @@ static void useNanocomBooster(CNSocket* sock, CNPacketData* data) { for (int16_t itemID : boosterIDs) { sItemBase boosterItem = { 7, itemID, 1, timeLimitDisplayed }; - // 155 -> 9, 153 -> 10, 154 -> 11 - int slot = 9 + ((itemID - 152) % 3); + // 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); @@ -607,15 +617,15 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) { */ switch(item.iID) { - case 119: - case 120: - case 121: + case GENERALITEM_GUMBALL_ADAPTIUM: + case GENERALITEM_GUMBALL_BLASTONS: + case GENERALITEM_GUMBALL_COSMIX: useGumball(sock, data); break; - case 153: - case 154: - case 155: - case 156: + case GENERALITEM_FUSION_HUNTER_BOOSTER: + case GENERALITEM_IZ_RACER_BOOSTER: + case GENERALITEM_QUESTER_BOOSTER: + case GENERALITEM_SUPER_BOOSTER_DX: useNanocomBooster(sock, data); break; default: @@ -709,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 (failing) { item->sItem.iType = 7; - item->sItem.iID = 119 + Rand::rand(3); + item->sItem.iID = GENERALITEM_GUMBALL_ADAPTIUM + Rand::rand(3); item->sItem.iOpt = 1; std::cout << "[WARN] Crate open failed, giving a Gumball..." << std::endl; @@ -820,7 +830,7 @@ size_t Items::checkAndRemoveExpiredItems(CNSocket* sock, Player* player) { } // exit vehicle if player no longer has one equipped (function checks pcstyle) - if (player->Equip[8].iID == 0) + if (player->Equip[AEQUIP_VEHICLE_IDX].iID == 0) PlayerManager::exitPlayerVehicle(sock, nullptr); return itemData.size(); @@ -881,53 +891,53 @@ 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; + // if player is within 1 level of the mob, FM is untouched double levelEffect = 1.0; - if (levelDiff >= 6) { + // 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; - } else if (levelDiff <= -3) { + 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; - } 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; - } + break; } + // if no group, FM is untouched double groupEffect = 1.0; + // otherwise, follow the table below 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; + 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; + int amount = baseAmount * bonus * levelEffect * groupEffect; + amount *= boosterEffect; return amount; } diff --git a/src/PlayerManager.cpp b/src/PlayerManager.cpp index 07bb13ab9..fe7a5495f 100644 --- a/src/PlayerManager.cpp +++ b/src/PlayerManager.cpp @@ -245,12 +245,12 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) { // response.PCLoadData2CL.PCStyle2 = plr->PCStyle2; // equipment (except nanocom boosters) - for (int i = 0; i < 9; i++) + for (int i = 0; i < AEQUIP_COUNT_MINUS_BOOSTERS; i++) response.PCLoadData2CL.aEquip[i] = plr->Equip[i]; - // equipment (nanocom boosters) + // equipment (nanocom boosters, loop only runs if boosters are available) int32_t serverTime = getTime() / 1000UL; int32_t timestamp = getTimestamp(); - for (int i = 9; i < AEQUIP_COUNT; i++) { + 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); @@ -526,7 +526,7 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) { if (plr->instanceID != 0) return; - if (plr->Equip[8].iID > 0) { + if (plr->Equip[AEQUIP_VEHICLE_IDX].iID > 0) { INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_SUCC, response); sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_SUCC); diff --git a/src/core/CNStructs.hpp b/src/core/CNStructs.hpp index 74afc824f..f84fca273 100644 --- a/src/core/CNStructs.hpp +++ b/src/core/CNStructs.hpp @@ -41,7 +41,7 @@ // wrapper for U16toU8 #define ARRLEN(x) (sizeof(x)/sizeof(*x)) #define AUTOU8(x) std::string((char*)x, ARRLEN(x)) -#define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x)) +#define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x)) // TODO: rewrite U16toU8 & U8toU16 to not use codecvt @@ -67,4 +67,8 @@ void terminate(int); #error Invalid PROTOCOL_VERSION #endif +#define AEQUIP_VEHICLE_IDX 8 +#define AEQUIP_COUNT_MINUS_BOOSTERS 9 +#define AEQUIP_COUNT_WITH_BOOSTERS 12 + sSYSTEMTIME timeStampToStruct(uint64_t time); From 31aac116dc6dd4c69419731adbfd6ecaadfc0481 Mon Sep 17 00:00:00 2001 From: FinnHornhoover Date: Thu, 19 Feb 2026 06:12:29 +0300 Subject: [PATCH 5/7] make sure only close by group members are counted --- src/MobAI.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/MobAI.cpp b/src/MobAI.cpp index be3678837..3da6c9a3d 100644 --- a/src/MobAI.cpp +++ b/src/MobAI.cpp @@ -808,6 +808,7 @@ void MobAI::onDeath(CombatNPC* npc, EntityRef src) { Items::DropRoll rolled; Items::DropRoll eventRolled; std::map qitemRolls; + std::vector playersInRange; std::vector playerRefs; if (plr->group == nullptr) { @@ -818,8 +819,6 @@ void MobAI::onDeath(CombatNPC* npc, EntityRef src) { } else { 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++) { CNSocket* sockTo = players[i].sock; Player* otherPlr = PlayerManager::getPlayer(sockTo); @@ -829,7 +828,14 @@ void MobAI::onDeath(CombatNPC* npc, EntityRef src) { if (dist > 5000) continue; - Items::giveMobDrop(sockTo, self, rolled, eventRolled, players.size()); + 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); } } From 80a30efb7910a8bb6d1da3ca3746360c940fd92b Mon Sep 17 00:00:00 2001 From: FinnHornhoover Date: Sun, 22 Feb 2026 06:24:04 +0300 Subject: [PATCH 6/7] add safe taro fm handling, rate command, race and mission booster logic --- src/Abilities.cpp | 8 ++-- src/BuiltinCommands.cpp | 64 +++++++++++++++++++++++------- src/Combat.cpp | 15 ++----- src/Email.cpp | 6 +-- src/Entities.cpp | 87 +++++++++++++++++++++++++++++++++++++++++ src/Items.cpp | 33 +++++++--------- src/Missions.cpp | 43 ++++++++++---------- src/Missions.hpp | 2 +- src/Nanos.cpp | 12 +++--- src/Player.hpp | 14 +++++++ src/PlayerManager.cpp | 4 +- src/Racing.cpp | 14 ++++++- src/Trading.cpp | 14 ++++--- src/Transport.cpp | 4 +- src/Vendors.cpp | 22 ++++------- src/core/CNStructs.hpp | 1 - src/core/Defines.hpp | 9 +++++ src/db/player.cpp | 8 ++-- 18 files changed, 251 insertions(+), 109 deletions(-) diff --git a/src/Abilities.cpp b/src/Abilities.cpp index fc5e61575..3b49f522f 100644 --- a/src/Abilities.cpp +++ b/src/Abilities.cpp @@ -170,11 +170,11 @@ static SkillResult handleSkillBatteryDrain(SkillData* skill, int power, ICombata if(!blocked) { boostDrain = (int)(skill->values[0][power] * scalingFactor); if(boostDrain > plr->batteryW) boostDrain = plr->batteryW; - plr->batteryW -= boostDrain; + plr->subtractCapped(CappedValueType::BATTERY_W, boostDrain); potionDrain = (int)(skill->values[1][power] * scalingFactor); if(potionDrain > plr->batteryN) potionDrain = plr->batteryN; - plr->batteryN -= potionDrain; + plr->subtractCapped(CappedValueType::BATTERY_N, potionDrain); } sSkillResult_BatteryDrain result{}; @@ -364,7 +364,7 @@ void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector ICombatant* src = nullptr; if(npc.kind == EntityKind::COMBAT_NPC || npc.kind == EntityKind::MOB) src = dynamic_cast(entity); - + SkillData* skill = &SkillTable[skillID]; std::vector results = handleSkill(skill, 0, src, affected); @@ -443,7 +443,7 @@ std::vector Abilities::matchTargets(ICombatant* src, SkillData* ski } } - return targets; + return targets; } /* ripped from client (enums emplaced) */ diff --git a/src/BuiltinCommands.cpp b/src/BuiltinCommands.cpp index 6ea442737..2e931b3d2 100644 --- a/src/BuiltinCommands.cpp +++ b/src/BuiltinCommands.cpp @@ -75,30 +75,22 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) { case CN_GM_SET_VALUE_TYPE__HP: response.iSetValue = plr->HP = setData->iSetValue; break; - case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY : - plr->batteryW = setData->iSetValue; - - // caps - if (plr->batteryW > 9999) - plr->batteryW = 9999; - + case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY: + plr->setCapped(CappedValueType::BATTERY_W, setData->iSetValue); response.iSetValue = plr->batteryW; break; case CN_GM_SET_VALUE_TYPE__NANO_BATTERY: - plr->batteryN = setData->iSetValue; - - // caps - if (plr->batteryN > 9999) - plr->batteryN = 9999; - + plr->setCapped(CappedValueType::BATTERY_N, setData->iSetValue); response.iSetValue = plr->batteryN; break; 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; break; case CN_GM_SET_VALUE_TYPE__CANDY: - response.iSetValue = plr->money = setData->iSetValue; + plr->setCapped(CappedValueType::TAROS, setData->iSetValue); + response.iSetValue = plr->money; break; case CN_GM_SET_VALUE_TYPE__SPEED: case CN_GM_SET_VALUE_TYPE__JUMP: @@ -148,6 +140,47 @@ 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 } +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 = plr->rateT; + + switch (req->iRewardType) { + case REWARD_TYPE_TAROS: + rate = plr->rateT; + break; + case REWARD_TYPE_FUSIONMATTER: + rate = plr->rateF; + break; + } + + switch (req->iRewardRateIndex) { + case RATE_SLOT_ALL: + for (int i = 0; i < 5; i++) + rate[i] = req->iSetRateValue; + break; + case RATE_SLOT_COMBAT: + case RATE_SLOT_MISSION: + case RATE_SLOT_EGG: + case RATE_SLOT_RACING: + rate[req->iRewardRateIndex] = req->iSetRateValue; + break; + } + } + + INITSTRUCT(sP_FE2CL_GM_REP_REWARD_RATE_SUCC, resp); + memcpy(resp.afRewardRate_Taros, plr->rateT, sizeof(resp.afRewardRate_Taros)); + memcpy(resp.afRewardRate_FusionMatter, plr->rateF, sizeof(resp.afRewardRate_FusionMatter)); + sock->sendPacket(resp, P_FE2CL_GM_REP_REWARD_RATE_SUCC); +} + static void locatePlayer(CNSocket *sock, CNPacketData *data) { Player *plr = PlayerManager::getPlayer(sock); @@ -371,6 +404,7 @@ void BuiltinCommands::init() { 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_REWARD_RATE, setGMRewardRate); REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_LOCATION, locatePlayer); REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_KICK_PLAYER, kickPlayer); diff --git a/src/Combat.cpp b/src/Combat.cpp index 2820e8c82..ec011d061 100644 --- a/src/Combat.cpp +++ b/src/Combat.cpp @@ -300,7 +300,7 @@ EntityRef CombatNPC::getRef() { } void CombatNPC::step(time_t currTime) { - + if(stateHandlers.find(state) != stateHandlers.end()) stateHandlers[state](this, currTime); else { @@ -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), Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty); - if (plr->batteryW >= 6 + difficulty) - plr->batteryW -= 6 + difficulty; - else - plr->batteryW = 0; - + plr->subtractCapped(CappedValueType::BATTERY_W, 6 + difficulty); damage.first = mob->takeDamage(sock, damage.first); 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); } - if (plr->batteryW >= 6 + plr->level) - plr->batteryW -= 6 + plr->level; - else - plr->batteryW = 0; + plr->subtractCapped(CappedValueType::BATTERY_W, 6 + plr->level); damage.first = target->takeDamage(sock, damage.first); @@ -742,7 +735,7 @@ static int8_t addBullet(Player* plr, bool isGrenade) { toAdd.weaponBoost = plr->batteryW > 0; if (toAdd.weaponBoost) { 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; diff --git a/src/Email.cpp b/src/Email.cpp index a7fffadea..66314f066 100644 --- a/src/Email.cpp +++ b/src/Email.cpp @@ -75,7 +75,7 @@ static void emailReceiveTaros(CNSocket* sock, CNPacketData* data) { Database::EmailData email = Database::getEmail(plr->iID, pkt->iEmailIndex); // money transfer - plr->money += email.Taros; + plr->addCapped(CappedValueType::TAROS, email.Taros); email.Taros = 0; // update Taros in 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 - plr->money -= cost; + plr->subtractCapped(CappedValueType::TAROS, cost); Database::EmailData email = { (int)pkt->iTo_PCUID, // PlayerId Database::getNextEmailIndex(pkt->iTo_PCUID), // MsgIndex @@ -291,7 +291,7 @@ static void emailSend(CNSocket* sock, CNPacketData* data) { }; if (!Database::sendEmail(&email, attachments, plr)) { - plr->money += cost; // give money back + plr->addCapped(CappedValueType::TAROS, cost); // give money back // give items back while (!attachments.empty()) { sItemBase attachment = attachments.back(); diff --git a/src/Entities.cpp b/src/Entities.cpp index a9621d719..740c7578a 100644 --- a/src/Entities.cpp +++ b/src/Entities.cpp @@ -136,6 +136,93 @@ 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() void Player::enterIntoViewOf(CNSocket *sock) { INITSTRUCT(sP_FE2CL_PC_NEW, pkt); diff --git a/src/Items.cpp b/src/Items.cpp index cce371e95..923cf0a3b 100644 --- a/src/Items.cpp +++ b/src/Items.cpp @@ -335,7 +335,7 @@ static void itemMoveHandler(CNSocket* sock, CNPacketData* data) { // if equipping an item, validate that it's of the correct type for the slot if ((SlotType)itemmove->eTo == SlotType::EQUIP) { - if (fromItem->iType == 10 && itemmove->iToSlotNum != AEQUIP_VEHICLE_IDX) + if (fromItem->iType == 10 && itemmove->iToSlotNum != EQUIP_SLOT_VEHICLE) return; // vehicle in wrong slot else if (fromItem->iType != 10 && !(fromItem->iType == 0 && itemmove->iToSlotNum == 7) @@ -399,7 +399,7 @@ static void itemMoveHandler(CNSocket* sock, CNPacketData* data) { } // unequip vehicle if equip slot 8 is 0 - if (plr->Equip[AEQUIP_VEHICLE_IDX].iID == 0 && plr->iPCState & 8) { + if (plr->Equip[EQUIP_SLOT_VEHICLE].iID == 0 && plr->iPCState & 8) { INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response); sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC); @@ -830,7 +830,7 @@ size_t Items::checkAndRemoveExpiredItems(CNSocket* sock, Player* player) { } // exit vehicle if player no longer has one equipped (function checks pcstyle) - if (player->Equip[AEQUIP_VEHICLE_IDX].iID == 0) + if (player->Equip[EQUIP_SLOT_VEHICLE].iID == 0) PlayerManager::exitPlayerVehicle(sock, nullptr); return itemData.size(); @@ -880,14 +880,13 @@ static void getMobDrop(sItemBase* reward, const std::vector& weights, const reward->iID = crateIds[chosenIndex]; } -static int getTaroDrop(Player* plr, int baseAmount, int groupSize) { +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 = 1.0 / groupSize; - int amount = baseAmount * bonus * groupEffect; - return amount; + return baseAmount * plr->rateT[RATE_SLOT_COMBAT] * bonus * groupEffect; } -static int getFMDrop(Player* plr, int baseAmount, int levelDiff, int groupSize) { +static int32_t calculateFMReward(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; @@ -936,7 +935,7 @@ static int getFMDrop(Player* plr, int baseAmount, int levelDiff, int groupSize) break; } - int amount = baseAmount * bonus * levelEffect * groupEffect; + int32_t amount = baseAmount * plr->rateF[RATE_SLOT_COMBAT] * bonus * levelEffect * groupEffect; amount *= boosterEffect; return amount; } @@ -993,24 +992,20 @@ 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 += getTaroDrop(plr, miscDropType.taroAmount, groupSize); + int32_t taros = calculateTaroReward(plr, miscDropType.taroAmount, groupSize); + plr->addCapped(CappedValueType::TAROS, taros); } if (rolled.fm % miscDropChance.fmDropChanceTotal < miscDropChance.fmDropChance) { int levelDifference = plr->level - mob->level; - int fm = getFMDrop(plr, miscDropType.fmAmount, levelDifference, groupSize); - Missions::updateFusionMatter(sock, fm); + int32_t fm = calculateFMReward(plr, miscDropType.fmAmount, levelDifference, groupSize); + plr->addCapped(CappedValueType::FUSIONMATTER, fm); + Missions::updateFusionMatter(sock); } 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) - plr->batteryW += miscDropType.boostAmount; - - // caps - if (plr->batteryW > 9999) - plr->batteryW = 9999; - if (plr->batteryN > 9999) - plr->batteryN = 9999; + plr->addCapped(CappedValueType::BATTERY_W, miscDropType.boostAmount); // simple rewards reward->m_iCandy = plr->money; diff --git a/src/Missions.cpp b/src/Missions.cpp index 67c1161d4..f8526237c 100644 --- a/src/Missions.cpp +++ b/src/Missions.cpp @@ -12,6 +12,20 @@ std::map Missions::Rewards; std::map Missions::Tasks; 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) { // sanity check missionID so we don't get exceptions if (missionId < 0 || missionId > 1023) { @@ -148,7 +162,7 @@ static int giveMissionReward(CNSocket *sock, int task, int choice=0) { } return -1; } - + plr->Inven[slots[i]] = { 999, 999, 999, 0 }; // temp item; overwritten later } @@ -162,21 +176,12 @@ static int giveMissionReward(CNSocket *sock, int task, int choice=0) { memset(respbuf, 0, CN_PACKET_BODY_SIZE); // update player - plr->money += reward->money; - if (plr->hasBuff(ECSB_REWARD_CASH)) { // nano boost for taros - int boost = 0; - if (Nanos::getNanoBoost(plr)) // for gumballs - boost = 1; - plr->money += reward->money * (5 + boost) / 25; - } + int32_t money = calculateTaroReward(plr, reward->money); + plr->addCapped(CappedValueType::TAROS, money); - if (plr->hasBuff(ECSB_REWARD_BLOB)) { // nano boost for fm - int boost = 0; - if (Nanos::getNanoBoost(plr)) // for gumballs - boost = 1; - updateFusionMatter(sock, reward->fusionmatter * (30 + boost) / 25); - } else - updateFusionMatter(sock, reward->fusionmatter); + int32_t fusionMatter = calculateFMReward(plr, reward->fusionmatter); + plr->addCapped(CappedValueType::FUSIONMATTER, fusionMatter); + Missions::updateFusionMatter(sock); // simple rewards 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)); } -void Missions::updateFusionMatter(CNSocket* sock, int fusion) { +void Missions::updateFusionMatter(CNSocket* sock) { Player *plr = PlayerManager::getPlayer(sock); - plr->fusionmatter += fusion; - // there's a much lower FM cap in the Future int fmCap = AvatarGrowth[plr->level]["m_iFMLimit"]; if (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 if (plr->level >= 36) @@ -551,7 +552,7 @@ void Missions::updateFusionMatter(CNSocket* sock, int fusion) { 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)); #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++; INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, response); diff --git a/src/Missions.hpp b/src/Missions.hpp index 46eaa4c19..231f88011 100644 --- a/src/Missions.hpp +++ b/src/Missions.hpp @@ -47,7 +47,7 @@ namespace Missions { bool startTask(Player* plr, int TaskID); // 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& rolls); diff --git a/src/Nanos.cpp b/src/Nanos.cpp index 73082be8e..bb2497489 100644 --- a/src/Nanos.cpp +++ b/src/Nanos.cpp @@ -33,8 +33,10 @@ void Nanos::addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm) */ plr->level = level; - if (spendfm) - Missions::updateFusionMatter(sock, -(int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]); + if (spendfm) { + plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]); + Missions::updateFusionMatter(sock); + } #endif // Send to client @@ -143,7 +145,7 @@ static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) { return; #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 reqItemID = NanoTunings[skill->iTuneID].reqItems; @@ -190,7 +192,7 @@ int Nanos::nanoStyle(int nanoID) { } bool Nanos::getNanoBoost(Player* plr) { - for (int i = 0; i < 3; i++) + for (int i = 0; i < 3; i++) if (plr->equippedNanos[i] == plr->activeNano) if (plr->hasBuff(ECSB_STIMPAKSLOT1 + i)) return true; @@ -355,7 +357,7 @@ static void nanoPotionHandler(CNSocket* sock, CNPacketData* data) { sock->sendPacket(response, P_FE2CL_REP_CHARGE_NANO_STAMINA); // now update serverside - player->batteryN -= difference; + player->subtractCapped(CappedValueType::BATTERY_N, difference); player->Nanos[nano.iID].iStamina += difference; } diff --git a/src/Player.hpp b/src/Player.hpp index 59deaef57..7962ac5a5 100644 --- a/src/Player.hpp +++ b/src/Player.hpp @@ -15,6 +15,14 @@ struct BuffStack; #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 { int accountId = 0; int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums) @@ -82,6 +90,9 @@ struct Player : public Entity, public ICombatant { time_t lastShot = 0; std::vector buyback = {}; + 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; } virtual void enterIntoViewOf(CNSocket *sock) override; @@ -113,4 +124,7 @@ struct Player : public Entity, public ICombatant { 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); }; diff --git a/src/PlayerManager.cpp b/src/PlayerManager.cpp index fe7a5495f..32b0e449e 100644 --- a/src/PlayerManager.cpp +++ b/src/PlayerManager.cpp @@ -526,7 +526,7 @@ static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) { if (plr->instanceID != 0) return; - if (plr->Equip[AEQUIP_VEHICLE_IDX].iID > 0) { + if (plr->Equip[EQUIP_SLOT_VEHICLE].iID > 0) { INITSTRUCT(sP_FE2CL_PC_VEHICLE_ON_SUCC, response); sock->sendPacket(response, P_FE2CL_PC_VEHICLE_ON_SUCC); @@ -568,7 +568,7 @@ static void changePlayerGuide(CNSocket *sock, CNPacketData *data) { } // start Blossom nano mission if applicable - Missions::updateFusionMatter(sock, 0); + Missions::updateFusionMatter(sock); } // save it on player plr->mentor = pkt->iMentor; diff --git a/src/Racing.cpp b/src/Racing.cpp index 844038bc3..d7ad95478 100644 --- a/src/Racing.cpp +++ b/src/Racing.cpp @@ -7,6 +7,7 @@ #include "PlayerManager.hpp" #include "Missions.hpp" #include "Items.hpp" +#include "Nanos.hpp" using namespace Racing; @@ -14,6 +15,15 @@ std::map Racing::EPData; std::map Racing::EPRaces; std::map, std::vector>> 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) { 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.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.iFatigue = 50; diff --git a/src/Trading.cpp b/src/Trading.cpp index ab8ce7bbd..e0f54177c 100644 --- a/src/Trading.cpp +++ b/src/Trading.cpp @@ -230,7 +230,7 @@ static void tradeConfirm(CNSocket* sock, CNPacketData* data) { Player* plr = PlayerManager::getPlayer(sock); Player* plr2 = PlayerManager::getPlayer(otherSock); - + if (!(plr->isTrading && plr2->isTrading)) { // both players must be trading INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, resp); resp.iID_Request = plr2->iID; @@ -273,14 +273,16 @@ static void tradeConfirm(CNSocket* sock, CNPacketData* data) { resp2.iID_Request = pacdat->iID_Request; resp2.iID_From = pacdat->iID_From; 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; memcpy(resp2.Item, plr2->Trade, sizeof(plr2->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)); - plr2->money = plr2->money + plr->moneyInTrade - plr2->moneyInTrade; + plr2->subtractCapped(CappedValueType::TAROS, plr2->moneyInTrade); + plr2->addCapped(CappedValueType::TAROS, plr->moneyInTrade); resp2.iCandy = plr2->money; memcpy(resp2.Item, plr->Trade, sizeof(plr->Trade)); memcpy(resp2.ItemStay, plr2->Trade, sizeof(plr2->Trade)); @@ -358,7 +360,7 @@ static void tradeRegisterItem(CNSocket* sock, CNPacketData* data) { // since you can spread items like gumballs over multiple slots, we need to count them all // to make sure the inventory shows the right value during trade. - int count = 0; + int count = 0; for (int i = 0; i < 5; i++) { if (plr->Trade[i].iInvenNum == pacdat->Item.iInvenNum) count += plr->Trade[i].iOpt; @@ -410,7 +412,7 @@ static void tradeUnregisterItem(CNSocket* sock, CNPacketData* data) { // since you can spread items like gumballs over multiple slots, we need to count them all // to make sure the inventory shows the right value during trade. - int count = 0; + int count = 0; for (int i = 0; i < 5; i++) { if (plr->Trade[i].iInvenNum == resp.InvenItem.iInvenNum) count += plr->Trade[i].iOpt; @@ -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)); 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->addCapped(CappedValueType::TAROS_IN_TRADE, pacdat->iCandy); plr->isTradeConfirm = false; } diff --git a/src/Transport.cpp b/src/Transport.cpp index db249c813..810949779 100644 --- a/src/Transport.cpp +++ b/src/Transport.cpp @@ -117,7 +117,7 @@ static void transportWarpHandler(CNSocket* sock, CNPacketData* data) { } TransportRoute route = Routes[req->iTransporationID]; - plr->money -= route.cost; + plr->subtractCapped(CappedValueType::TAROS, route.cost); TransportLocation* target = nullptr; switch (route.type) { @@ -143,7 +143,7 @@ static void transportWarpHandler(CNSocket* sock, CNPacketData* data) { } // refund and send alert packet - plr->money += route.cost; + plr->addCapped(CappedValueType::TAROS, route.cost); INITSTRUCT(sP_FE2CL_ANNOUNCE_MSG, alert); alert.iAnnounceType = 0; // don't think this lets us make a confirm dialog alert.iDuringTime = 3; diff --git a/src/Vendors.cpp b/src/Vendors.cpp index 5e2a7a338..042f98ee0 100644 --- a/src/Vendors.cpp +++ b/src/Vendors.cpp @@ -72,7 +72,7 @@ static void vendorBuy(CNSocket* sock, CNPacketData* data) { 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; 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); // increment taros - plr->money += itemData->sellPrice * req->iItemCnt; + plr->addCapped(CappedValueType::TAROS, itemData->sellPrice * req->iItemCnt); // modify item 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; } - plr->money = plr->money - itemCost; + plr->subtractCapped(CappedValueType::TAROS, itemCost); plr->Inven[slot] = item; 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); 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); failResp.iErrorCode = 0; 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; - plr->batteryW += req->Item.iID == 3 ? req->Item.iOpt * 100 : 0; - plr->batteryN += req->Item.iID == 4 ? req->Item.iOpt * 100 : 0; - - // caps - if (plr->batteryW > 9999) - plr->batteryW = 9999; - if (plr->batteryN > 9999) - plr->batteryN = 9999; + plr->addCapped(CappedValueType::BATTERY_W, req->Item.iID == 3 ? req->Item.iOpt * 100 : 0); + plr->addCapped(CappedValueType::BATTERY_N, req->Item.iID == 4 ? req->Item.iOpt * 100 : 0); cost = plr->batteryW + plr->batteryN - cost; - plr->money -= cost; + plr->subtractCapped(CappedValueType::TAROS, cost); 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 //std::cout << rolled << " vs " << successChance << std::endl; - plr->money -= cost; + plr->subtractCapped(CappedValueType::TAROS, cost); INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_SUCC, resp); diff --git a/src/core/CNStructs.hpp b/src/core/CNStructs.hpp index f84fca273..bcb7dfc5c 100644 --- a/src/core/CNStructs.hpp +++ b/src/core/CNStructs.hpp @@ -67,7 +67,6 @@ void terminate(int); #error Invalid PROTOCOL_VERSION #endif -#define AEQUIP_VEHICLE_IDX 8 #define AEQUIP_COUNT_MINUS_BOOSTERS 9 #define AEQUIP_COUNT_WITH_BOOSTERS 12 diff --git a/src/core/Defines.hpp b/src/core/Defines.hpp index b26825227..45b71a658 100644 --- a/src/core/Defines.hpp +++ b/src/core/Defines.hpp @@ -225,6 +225,15 @@ enum { SIZEOF_NANO_TUNE_NEED_ITEM_SLOT = 10, 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_BUSY = 2, MSG_OFFLINE = 0, diff --git a/src/db/player.cpp b/src/db/player.cpp index ed01c67f6..2d5584f93 100644 --- a/src/db/player.cpp +++ b/src/db/player.cpp @@ -59,13 +59,13 @@ void Database::getPlayer(Player* plr, int id) { plr->angle = sqlite3_column_int(stmt, 15); plr->HP = sqlite3_column_int(stmt, 16); plr->accountLevel = sqlite3_column_int(stmt, 17); - plr->fusionmatter = sqlite3_column_int(stmt, 18); - plr->money = sqlite3_column_int(stmt, 19); + plr->setCapped(CappedValueType::FUSIONMATTER, sqlite3_column_int(stmt, 18)); + plr->setCapped(CappedValueType::TAROS, sqlite3_column_int(stmt, 19)); memcpy(plr->aQuestFlag, sqlite3_column_blob(stmt, 20), sizeof(plr->aQuestFlag)); - plr->batteryW = sqlite3_column_int(stmt, 21); - plr->batteryN = sqlite3_column_int(stmt, 22); + plr->setCapped(CappedValueType::BATTERY_W, sqlite3_column_int(stmt, 21)); + plr->setCapped(CappedValueType::BATTERY_N, sqlite3_column_int(stmt, 22)); plr->mentor = sqlite3_column_int(stmt, 23); plr->iWarpLocationFlag = sqlite3_column_int(stmt, 24); From ffcc6e5a53215af0f52acc85623bc7daf0e1b148 Mon Sep 17 00:00:00 2001 From: FinnHornhoover Date: Sun, 22 Feb 2026 11:48:40 +0300 Subject: [PATCH 7/7] add config option to disable authentic group scaling --- config.ini | 5 +++++ src/Items.cpp | 26 ++++++++++++++------------ src/settings.cpp | 4 ++++ src/settings.hpp | 1 + 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/config.ini b/config.ini index 6ea0a843f..ffc84971d 100644 --- a/config.ini +++ b/config.ini @@ -54,6 +54,11 @@ motd=Welcome to OpenFusion! # This is a polish option that is slightly inauthentic to the original game. #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 + # location of the tabledata folder #tdatadir=tdata/ # location of the patch folder diff --git a/src/Items.cpp b/src/Items.cpp index 923cf0a3b..8b9f12394 100644 --- a/src/Items.cpp +++ b/src/Items.cpp @@ -882,7 +882,7 @@ static void getMobDrop(sItemBase* reward, const std::vector& weights, const 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 = 1.0 / groupSize; + double groupEffect = settings::LESSTAROFMINGROUPDISABLED ? 1.0 : 1.0 / groupSize; return baseAmount * plr->rateT[RATE_SLOT_COMBAT] * bonus * groupEffect; } @@ -922,17 +922,19 @@ static int32_t calculateFMReward(Player* plr, int baseAmount, int levelDiff, int // if no group, FM is untouched double groupEffect = 1.0; // otherwise, follow the table below - 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; + 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] * bonus * levelEffect * groupEffect; diff --git a/src/settings.cpp b/src/settings.cpp index 1c505a327..e31ce6e78 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -77,6 +77,9 @@ bool settings::IZRACESCORECAPPED = true; // drop fixes enabled bool settings::DROPFIXESENABLED = false; +// less taro / fm while in a group +bool settings::LESSTAROFMINGROUPDISABLED = false; + void settings::init() { INIReader reader("config.ini"); @@ -121,6 +124,7 @@ void settings::init() { PATCHDIR = reader.Get("shard", "patchdir", PATCHDIR); ENABLEDPATCHES = reader.Get("shard", "enabledpatches", ENABLEDPATCHES); DROPFIXESENABLED = reader.GetBoolean("shard", "dropfixesenabled", DROPFIXESENABLED); + LESSTAROFMINGROUPDISABLED = reader.GetBoolean("shard", "lesstarofmingroupdisabled", LESSTAROFMINGROUPDISABLED); ACCLEVEL = reader.GetInteger("shard", "accountlevel", ACCLEVEL); EVENTMODE = reader.GetInteger("shard", "eventmode", EVENTMODE); DISABLEFIRSTUSEFLAG = reader.GetBoolean("shard", "disablefirstuseflag", DISABLEFIRSTUSEFLAG); diff --git a/src/settings.hpp b/src/settings.hpp index d74c54df7..bf48b1cfe 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -46,6 +46,7 @@ namespace settings { extern bool DISABLEFIRSTUSEFLAG; extern bool IZRACESCORECAPPED; extern bool DROPFIXESENABLED; + extern bool LESSTAROFMINGROUPDISABLED; void init(); }