diff --git a/Makefile b/Makefile index 843352d..c88e00c 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ CXXSRC=\ src/db/email.cpp\ src/sandbox/seccomp.cpp\ src/sandbox/openbsd.cpp\ + src/Buffs.cpp\ src/Chat.cpp\ src/CustomCommands.cpp\ src/Entities.cpp\ @@ -96,6 +97,7 @@ CXXHDR=\ vendor/JSON.hpp\ vendor/INIReader.hpp\ vendor/JSON.hpp\ + src/Buffs.hpp\ src/Chat.hpp\ src/CustomCommands.hpp\ src/Entities.hpp\ diff --git a/src/Abilities.cpp b/src/Abilities.cpp index 691f486..9bb14b0 100644 --- a/src/Abilities.cpp +++ b/src/Abilities.cpp @@ -2,17 +2,260 @@ #include "NPCManager.hpp" #include "PlayerManager.hpp" +#include "Buffs.hpp" +#include "Nanos.hpp" + +#include + +using namespace Abilities; std::map Abilities::SkillTable; -/* -// New email notification -static void emailUpdateCheck(CNSocket* sock, CNPacketData* data) { - INITSTRUCT(sP_FE2CL_REP_PC_NEW_EMAIL, resp); - resp.iNewEmailCnt = Database::getUnreadEmailCount(PlayerManager::getPlayer(sock)->iID); - sock->sendPacket(resp, P_FE2CL_REP_PC_NEW_EMAIL); +#pragma region Skill handlers +static SkillResult handleSkillDamage(SkillData* skill, int power, ICombatant* source, ICombatant* target) { + EntityRef sourceRef = source->getRef(); + double scalingFactor = 1; + if(sourceRef.kind == EntityKind::PLAYER) + scalingFactor = std::max(source->getMaxHP(), target->getMaxHP()) / 1000.0; + else + scalingFactor = source->getMaxHP() / 1500.0; + + int damage = (int)(skill->values[0][power] * scalingFactor); + int dealt = target->takeDamage(sourceRef, damage); + + sSkillResult_Damage result{}; + result.eCT = target->getCharType(); + result.iID = target->getID(); + result.bProtected = dealt <= 0; + result.iDamage = dealt; + result.iHP = target->getCurrentHP(); + return SkillResult(sizeof(sSkillResult_Damage), &result); +} + +static SkillResult handleSkillHealHP(SkillData* skill, int power, ICombatant* source, ICombatant* target) { + EntityRef sourceRef = source->getRef(); + int heal = skill->values[0][power]; + int healed = target->heal(sourceRef, heal); + + sSkillResult_Heal_HP result{}; + result.eCT = target->getCharType(); + result.iID = target->getID(); + result.iHealHP = healed; + result.iHP = target->getCurrentHP(); + return SkillResult(sizeof(sSkillResult_Heal_HP), &result); +} + +static SkillResult handleSkillDamageNDebuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) { + // TODO abilities + sSkillResult_Damage_N_Debuff result{}; + result.eCT = target->getCharType(); + result.iID = target->getID(); + result.bProtected = false; + result.iConditionBitFlag = target->getCompositeCondition(); + return SkillResult(sizeof(sSkillResult_Damage_N_Debuff), &result); +} + +static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) { + sSkillResult_Buff result{}; + result.eCT = target->getCharType(); + result.iID = target->getID(); + result.bProtected = false; + result.iConditionBitFlag = target->getCompositeCondition(); + return SkillResult(sizeof(sSkillResult_Buff), &result); +} + +static SkillResult handleSkillBatteryDrain(SkillData* skill, int power, ICombatant* source, ICombatant* target) { + if(target->getCharType() != 1) + return SkillResult(); // only Players are valid targets for battery drain + Player* plr = dynamic_cast(target); + + const double scalingFactor = (18 + source->getLevel()) / 36.0; + + int boostDrain = (int)(skill->values[0][power] * scalingFactor); + if(boostDrain > plr->batteryW) boostDrain = plr->batteryW; + plr->batteryW -= boostDrain; + + int potionDrain = (int)(skill->values[1][power] * scalingFactor); + if(potionDrain > plr->batteryN) potionDrain = plr->batteryN; + plr->batteryN -= potionDrain; + + sSkillResult_BatteryDrain result{}; + result.eCT = target->getCharType(); + result.iID = target->getID(); + result.bProtected = target->hasBuff(ECSB_PROTECT_BATTERY); + result.iDrainW = boostDrain; + result.iBatteryW = plr->batteryW; + result.iDrainN = potionDrain; + result.iBatteryN = plr->batteryN; + result.iStamina = plr->getActiveNano()->iStamina; + result.bNanoDeactive = plr->getActiveNano()->iStamina <= 0; + result.iConditionBitFlag = target->getCompositeCondition(); + return SkillResult(sizeof(sSkillResult_BatteryDrain), &result); +} + +static SkillResult handleSkillMove(SkillData* skill, int power, ICombatant* source, ICombatant* target) { + if(source->getCharType() != 1) + return SkillResult(); // only Players are valid sources for recall + Player* plr = dynamic_cast(source); + + sSkillResult_Move result{}; + result.eCT = target->getCharType(); + result.iID = target->getID(); + result.iMapNum = plr->recallInstance; + result.iMoveX = plr->recallX; + result.iMoveY = plr->recallY; + result.iMoveZ = plr->recallZ; + return SkillResult(sizeof(sSkillResult_Move), &result); +} + +static SkillResult handleSkillResurrect(SkillData* skill, int power, ICombatant* source, ICombatant* target) { + sSkillResult_Resurrect result{}; + result.eCT = target->getCharType(); + result.iID = target->getID(); + result.iRegenHP = target->getCurrentHP(); + return SkillResult(sizeof(sSkillResult_Resurrect), &result); +} +#pragma endregion + +static std::vector handleSkill(SkillData* skill, int power, ICombatant* src, std::vector targets) { + size_t resultSize = 0; + SkillResult (*skillHandler)(SkillData*, int, ICombatant*, ICombatant*) = nullptr; + std::vector results; + + switch(skill->skillType) + { + case EST_DAMAGE: + resultSize = sizeof(sSkillResult_Damage); + skillHandler = handleSkillDamage; + break; + case EST_HEAL_HP: + case EST_RETURNHOMEHEAL: + resultSize = sizeof(sSkillResult_Heal_HP); + skillHandler = handleSkillHealHP; + break; + case EST_JUMP: + case EST_RUN: + case EST_FREEDOM: + case EST_PHOENIX: + case EST_INVULNERABLE: + case EST_MINIMAPENEMY: + case EST_MINIMAPTRESURE: + case EST_NANOSTIMPAK: + case EST_PROTECTBATTERY: + case EST_PROTECTINFECTION: + case EST_REWARDBLOB: + case EST_REWARDCASH: + case EST_STAMINA_SELF: + case EST_STEALTH: + resultSize = sizeof(sSkillResult_Buff); + skillHandler = handleSkillBuff; + break; + case EST_BATTERYDRAIN: + resultSize = sizeof(sSkillResult_BatteryDrain); + skillHandler = handleSkillBatteryDrain; + break; + case EST_RECALL: + case EST_RECALL_GROUP: + resultSize = sizeof(sSkillResult_Move); + skillHandler = handleSkillMove; + break; + case EST_PHOENIX_GROUP: + resultSize = sizeof(sSkillResult_Resurrect); + skillHandler = handleSkillResurrect; + break; + default: + std::cout << "[WARN] Unhandled skill type " << skill->skillType << std::endl; + return results; + } + assert(skillHandler != nullptr); + + for(ICombatant* target : targets) { + assert(target != nullptr); + SkillResult result = skillHandler(skill, power, src != nullptr ? src : target, target); + if(result.size == 0) continue; // skill not applicable + if(result.size != resultSize) { + std::cout << "[WARN] bad skill result size for " << skill->skillType << " from " << (void*)handleSkillBuff << std::endl; + continue; + } + results.push_back(result); + } + return results; +} + +static void attachSkillResults(std::vector results, size_t resultSize, uint8_t* pivot) { + for(SkillResult& result : results) { + memcpy(pivot, result.payload, resultSize); + pivot += resultSize; + } +} + +void Abilities::useNanoSkill(CNSocket* sock, sNano& nano, std::vector affected) { + if(SkillTable.count(nano.iSkillID) == 0) + return; + + SkillData* skill = &SkillTable[nano.iSkillID]; + Player* plr = PlayerManager::getPlayer(sock); + + std::vector results = handleSkill(skill, Nanos::getNanoBoost(plr), plr, affected); + size_t resultSize = results.back().size; // guaranteed to be the same for every item + + if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), results.size(), resultSize)) { + std::cout << "[WARN] bad sP_FE2CL_NANO_SKILL_USE_SUCC packet size\n"; + return; + } + + // initialize response struct + size_t resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) + results.size() * resultSize; + uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; + memset(respbuf, 0, resplen); + + sP_FE2CL_NANO_SKILL_USE_SUCC* pkt = (sP_FE2CL_NANO_SKILL_USE_SUCC*)respbuf; + pkt->iPC_ID = plr->iID; + pkt->iNanoID = nano.iID; + pkt->iSkillID = nano.iSkillID; + pkt->iNanoStamina = nano.iStamina; + pkt->bNanoDeactive = nano.iStamina <= 0; + pkt->eST = skill->skillType; + pkt->iTargetCnt = (int32_t)results.size(); + + attachSkillResults(results, resultSize, (uint8_t*)(pkt + 1)); + sock->sendPacket(pkt, P_FE2CL_NANO_SKILL_USE_SUCC, resplen); + PlayerManager::sendToViewable(sock, pkt, P_FE2CL_NANO_SKILL_USE_SUCC, resplen); +} + +void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector affected) { + if(SkillTable.count(skillID) == 0) + return; + + Entity* entity = npc.getEntity(); + 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); + size_t resultSize = results.back().size; // guaranteed to be the same for every item + + if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), results.size(), resultSize)) { + std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_HIT packet size\n"; + return; + } + + // initialize response struct + size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + results.size() * resultSize; + uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; + memset(respbuf, 0, resplen); + + sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf; + pkt->iNPC_ID = npc.id; + pkt->iSkillID = skillID; + pkt->eST = skill->skillType; + pkt->iTargetCnt = (int32_t)results.size(); + + attachSkillResults(results, resultSize, (uint8_t*)(pkt + 1)); + NPCManager::sendToViewable(entity, pkt, P_FE2CL_NPC_SKILL_HIT, resplen); } -*/ std::vector Abilities::matchTargets(SkillData* skill, int count, int32_t *ids) { @@ -35,16 +278,65 @@ std::vector Abilities::matchTargets(SkillData* skill, int count, int3 return targets; } -void Abilities::applyAbility(SkillData* skill, EntityRef src, std::vector targets) { - for (EntityRef target : targets) { - Entity* entity = target.getEntity(); - if (entity->kind != PLAYER && entity->kind != COMBAT_NPC && entity->kind != MOB) - continue; // not a combatant - - +/* ripped from client (enums emplaced) */ +int Abilities::getCSTBFromST(int eSkillType) { + int result = 0; + switch (eSkillType) + { + case EST_RUN: + result = ECSB_UP_MOVE_SPEED; + break; + case EST_JUMP: + result = ECSB_UP_JUMP_HEIGHT; + break; + case EST_STEALTH: + result = ECSB_UP_STEALTH; + break; + case EST_PHOENIX: + result = ECSB_PHOENIX; + break; + case EST_PROTECTBATTERY: + result = ECSB_PROTECT_BATTERY; + break; + case EST_PROTECTINFECTION: + result = ECSB_PROTECT_INFECTION; + break; + case EST_SNARE: + result = ECSB_DN_MOVE_SPEED; + break; + case EST_SLEEP: + result = ECSB_MEZ; + break; + case EST_MINIMAPENEMY: + result = ECSB_MINIMAP_ENEMY; + break; + case EST_MINIMAPTRESURE: + result = ECSB_MINIMAP_TRESURE; + break; + case EST_REWARDBLOB: + result = ECSB_REWARD_BLOB; + break; + case EST_REWARDCASH: + result = ECSB_REWARD_CASH; + break; + case EST_INFECTIONDAMAGE: + result = ECSB_INFECTION; + break; + case EST_FREEDOM: + result = ECSB_FREEDOM; + break; + case EST_BOUNDINGBALL: + result = ECSB_BOUNDINGBALL; + break; + case EST_INVULNERABLE: + result = ECSB_INVULNERABLE; + break; + case EST_BUFFHEAL: + result = ECSB_HEAL; + break; + case EST_NANOSTIMPAK: + result = ECSB_STIMPAKSLOT1; + break; } -} - -void Abilities::init() { - //REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_EMAIL_UPDATE_CHECK, emailUpdateCheck); + return result; } diff --git a/src/Abilities.hpp b/src/Abilities.hpp index 834c605..275c2c5 100644 --- a/src/Abilities.hpp +++ b/src/Abilities.hpp @@ -1,10 +1,15 @@ #pragma once +#include "core/Core.hpp" + #include "Entities.hpp" +#include "Player.hpp" #include #include +constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain); + enum class SkillEffectTarget { POINT = 1, SELF = 2, @@ -25,8 +30,20 @@ enum class SkillDrainType { PASSIVE = 2 }; +struct SkillResult { + size_t size; + uint8_t payload[MAX_SKILLRESULT_SIZE]; + SkillResult(size_t len, void* dat) { + size = len; + memcpy(payload, dat, len); + } + SkillResult() { + size = 0; + } +}; + struct SkillData { - int skillType; + int skillType; // eST SkillEffectTarget effectTarget; int effectType; // always 1? SkillTargetType targetType; @@ -43,8 +60,9 @@ struct SkillData { namespace Abilities { extern std::map SkillTable; - std::vector matchTargets(SkillData*, int, int32_t*); - void applyAbility(SkillData*, EntityRef, std::vector); + void useNanoSkill(CNSocket*, sNano&, std::vector); + void useNPCSkill(EntityRef, int skillID, std::vector); - void init(); + std::vector matchTargets(SkillData*, int, int32_t*); + int getCSTBFromST(int eSkillType); } diff --git a/src/Buffs.cpp b/src/Buffs.cpp new file mode 100644 index 0000000..072001f --- /dev/null +++ b/src/Buffs.cpp @@ -0,0 +1,151 @@ +#include "Buffs.hpp" + +#include "PlayerManager.hpp" +#include "NPCManager.hpp" + +using namespace Buffs; + +void Buff::tick(time_t currTime) { + auto it = stacks.begin(); + while(it != stacks.end()) { + BuffStack& stack = *it; + //if(onTick) onTick(self, this, currTime); + + if(stack.durationTicks == 0) { + BuffStack deadStack = stack; + it = stacks.erase(it); + if(onUpdate) onUpdate(self, this, ETBU_DEL, &deadStack); + } else { + if(stack.durationTicks > 0) stack.durationTicks--; + it++; + } + } +} + +void Buff::combatTick(time_t currTime) { + if(onCombatTick) onCombatTick(self, this, currTime); +} + +void Buff::clear() { + while(!stacks.empty()) { + BuffStack stack = stacks.back(); + stacks.pop_back(); + if(onUpdate) onUpdate(self, this, ETBU_DEL, &stack); + } +} + +void Buff::clear(BuffClass buffClass) { + auto it = stacks.begin(); + while(it != stacks.end()) { + BuffStack& stack = *it; + if(stack.buffStackClass == buffClass) { + BuffStack deadStack = stack; + it = stacks.erase(it); + if(onUpdate) onUpdate(self, this, ETBU_DEL, &deadStack); + } else it++; + } +} + +void Buff::addStack(BuffStack* stack) { + stacks.push_back(*stack); + if(onUpdate) onUpdate(self, this, ETBU_ADD, &stacks.back()); +} + +bool Buff::hasClass(BuffClass buffClass) { + for(BuffStack& stack : stacks) { + if(stack.buffStackClass == buffClass) + return true; + } + return false; +} + +BuffClass Buff::maxClass() { + BuffClass buffClass = BuffClass::NONE; + for(BuffStack& stack : stacks) { + if(stack.buffStackClass > buffClass) + buffClass = stack.buffStackClass; + } + return buffClass; +} + +int Buff::getValue(BuffValueSelector selector) { + if(isStale()) return 0; + + int value = selector == BuffValueSelector::NET_TOTAL ? 0 : stacks.front().value; + for(BuffStack& stack : stacks) { + switch(selector) + { + case BuffValueSelector::NET_TOTAL: + value += stack.value; + break; + case BuffValueSelector::MIN_VALUE: + if(stack.value < value) value = stack.value; + break; + case BuffValueSelector::MAX_VALUE: + if(stack.value > value) value = stack.value; + break; + case BuffValueSelector::MIN_MAGNITUDE: + if(abs(stack.value) < abs(value)) value = stack.value; + break; + case BuffValueSelector::MAX_MAGNITUDE: + default: + if(abs(stack.value) > abs(value)) value = stack.value; + } + } + return value; +} + +bool Buff::isStale() { + return stacks.empty(); +} + +/* This will practically never do anything important, but it's here just in case */ +void Buff::updateCallbacks(BuffCallback fOnUpdate, BuffCallback fOnCombatTick) { + if(!onUpdate) onUpdate = fOnUpdate; + if(!onCombatTick) onCombatTick = fOnCombatTick; +} + +#pragma region Handlers +void Buffs::timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack) { + + if(self.kind != EntityKind::PLAYER) + return; // not implemented + + Player* plr = (Player*)self.getEntity(); + if(plr == nullptr) + return; // sanity check + + if(status == ETBU_DEL && !buff->isStale()) + return; // no premature effect deletion + + int cbf = plr->getCompositeCondition(); + sTimeBuff payload{}; + if(status == ETBU_ADD) { + payload.iValue = buff->getValue(BuffValueSelector::MAX_MAGNITUDE); + // we need to explicitly add the ECSB for this buff, + // in case this is the first stack in and the entry + // in the buff map doesn't yet exist + if(buff->id > 0) cbf |= CSB_FROM_ECSB(buff->id); + } + + INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt); + pkt.eCSTB = buff->id; // eCharStatusTimeBuffID + pkt.eTBU = status; // eTimeBuffUpdate + pkt.eTBT = (int)stack->buffStackClass; + pkt.iConditionBitFlag = cbf; + pkt.TimeBuff = payload; + self.sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE)); +} + +void Buffs::timeBuffTimeout(EntityRef self) { + if(self.kind != EntityKind::PLAYER && self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB) + return; // not a combatant + Entity* entity = self.getEntity(); + ICombatant* combatant = dynamic_cast(entity); + INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt); // send a buff timeout to other players + pkt.eCT = combatant->getCharType(); + pkt.iID = combatant->getID(); + pkt.iConditionBitFlag = combatant->getCompositeCondition(); + NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); +} +#pragma endregion diff --git a/src/Buffs.hpp b/src/Buffs.hpp new file mode 100644 index 0000000..3247143 --- /dev/null +++ b/src/Buffs.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include "core/Core.hpp" + +#include "EntityRef.hpp" + +#include +#include + +/* forward declaration(s) */ +class Buff; +template +using BuffCallback = std::function; + +#define CSB_FROM_ECSB(x) (1 << (x - 1)) + +enum class BuffClass { + NONE = ETBT_NONE, + NANO = ETBT_NANO, + GROUP_NANO = ETBT_GROUPNANO, + EGG = ETBT_SHINY, + ENVIRONMENT = ETBT_LANDEFFECT, + ITEM = ETBT_ITEM, + CASH_ITEM = ETBT_CASHITEM +}; + +enum class BuffValueSelector { + MAX_VALUE, + MIN_VALUE, + MAX_MAGNITUDE, + MIN_MAGNITUDE, + NET_TOTAL +}; + +struct BuffStack { + int durationTicks; + int value; + EntityRef source; + BuffClass buffStackClass; +}; + +class Buff { +private: + EntityRef self; + std::vector stacks; + +public: + int id; + /* called just after a stack is added or removed */ + BuffCallback onUpdate; + /* called when the buff is combat-ticked */ + BuffCallback onCombatTick; + + void tick(time_t); + void combatTick(time_t); + void clear(); + void clear(BuffClass buffClass); + void addStack(BuffStack* stack); + + /* + * Sometimes we need to determine if a buff + * is covered by a certain class, ex: nano + * vs. coco egg in the case of infection protection + */ + bool hasClass(BuffClass buffClass); + BuffClass maxClass(); + + int getValue(BuffValueSelector selector); + + /* + * In general, a Buff object won't exist + * unless it has stacks. However, when + * popping stacks during iteration (onExpire), + * stacks will be empty for a brief moment + * when the last stack is popped. + */ + bool isStale(); + + void updateCallbacks(BuffCallback fOnUpdate, BuffCallback fonTick); + + Buff(int iid, EntityRef pSelf, BuffCallback fOnUpdate, BuffCallback fOnCombatTick, BuffStack* firstStack) + : self(pSelf), id(iid), onUpdate(fOnUpdate), onCombatTick(fOnCombatTick) { + addStack(firstStack); + } +}; + +namespace Buffs { + void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack); + void timeBuffTimeout(EntityRef self); +} diff --git a/src/Combat.cpp b/src/Combat.cpp index 94e3c86..4220b1e 100644 --- a/src/Combat.cpp +++ b/src/Combat.cpp @@ -8,21 +8,84 @@ #include "NPCManager.hpp" #include "Nanos.hpp" #include "Abilities.hpp" +#include "Buffs.hpp" #include +#include +#include using namespace Combat; /// Player Id -> Bullet Id -> Bullet std::map> Combat::Bullets; -int Player::takeDamage(EntityRef src, int amt) { - HP -= amt; - return amt; +#pragma region Player +bool Player::addBuff(int buffId, BuffCallback onUpdate, BuffCallback onTick, BuffStack* stack) { + EntityRef self = PlayerManager::getSockFromID(iID); + + if(!hasBuff(buffId)) { + buffs[buffId] = new Buff(buffId, self, onUpdate, onTick, stack); + return true; + } + + buffs[buffId]->updateCallbacks(onUpdate, onTick); + buffs[buffId]->addStack(stack); + return false; } -void Player::heal(EntityRef src, int amt) { - // stubbed +Buff* Player::getBuff(int buffId) { + if(hasBuff(buffId)) { + return buffs[buffId]; + } + return nullptr; +} + +void Player::removeBuff(int buffId) { + if(hasBuff(buffId)) { + buffs[buffId]->clear(); + delete buffs[buffId]; + buffs.erase(buffId); + } +} + +void Player::removeBuff(int buffId, int buffClass) { + if(hasBuff(buffId)) { + buffs[buffId]->clear((BuffClass)buffClass); + if(buffs[buffId]->isStale()) { + delete buffs[buffId]; + buffs.erase(buffId); + } + } +} + +bool Player::hasBuff(int buffId) { + auto buff = buffs.find(buffId); + return buff != buffs.end() && !buff->second->isStale(); +} + +int Player::getCompositeCondition() { + int conditionBitFlag = 0; + for(auto buff : buffs) { + if(!buff.second->isStale() && buff.second->id > 0) + conditionBitFlag |= CSB_FROM_ECSB(buff.first); + } + return conditionBitFlag; +} + +int Player::takeDamage(EntityRef src, int amt) { + int dmg = amt; + if(HP - dmg < 0) dmg = HP; + HP -= dmg; + + return dmg; +} + +int Player::heal(EntityRef src, int amt) { + int heal = amt; + if(HP + heal > getMaxHP()) heal = getMaxHP() - HP; + HP += heal; + + return heal; } bool Player::isAlive() { @@ -33,25 +96,107 @@ int Player::getCurrentHP() { return HP; } +int Player::getMaxHP() { + return PC_MAXHEALTH(level); +} + +int Player::getLevel() { + return level; +} + +std::vector Player::getGroupMembers() { + std::vector members; + if(group != nullptr) + members = group->members; + else + members.push_back(PlayerManager::getSockFromID(iID)); + return members; +} + +int32_t Player::getCharType() { + return 1; // eCharType (eCT_PC) +} + int32_t Player::getID() { return iID; } +EntityRef Player::getRef() { + return EntityRef(PlayerManager::getSockFromID(iID)); +} + void Player::step(time_t currTime) { - // no-op + CNSocket* sock = getRef().sock; + + // nanos + for (int i = 0; i < 3; i++) { + if (activeNano != 0 && equippedNanos[i] == activeNano) { // tick active nano + sNano& nano = Nanos[activeNano]; + int drainRate = 0; + + if (Abilities::SkillTable.find(nano.iSkillID) != Abilities::SkillTable.end()) { + // nano has skill data + SkillData* skill = &Abilities::SkillTable[nano.iSkillID]; + int boost = Nanos::getNanoBoost(this); + if (skill->drainType == SkillDrainType::PASSIVE) + drainRate = skill->batteryUse[boost * 3]; + } + + nano.iStamina -= 1 + drainRate / 5; + if (nano.iStamina <= 0) + Nanos::summonNano(sock, -1, true); // unsummon nano silently + + } else if (Nanos[equippedNanos[i]].iStamina < 150) { // tick resting nano + sNano& nano = Nanos[equippedNanos[i]]; + if (nano.iStamina < 150) + nano.iStamina += 1; + } + } + + // buffs + for(auto buffEntry : buffs) { + buffEntry.second->combatTick(currTime); + } +} +#pragma endregion + +#pragma region CombatNPC +bool CombatNPC::addBuff(int buffId, BuffCallback onUpdate, BuffCallback onTick, BuffStack* stack) { /* stubbed */ + return false; +} + +Buff* CombatNPC::getBuff(int buffId) { /* stubbed */ + return nullptr; +} + +void CombatNPC::removeBuff(int buffId) { /* stubbed */ } + +void CombatNPC::removeBuff(int buffId, int buffClass) { /* stubbed */ } + +bool CombatNPC::hasBuff(int buffId) { /* stubbed */ + return false; +} + +int CombatNPC::getCompositeCondition() { /* stubbed */ + return 0; } int CombatNPC::takeDamage(EntityRef src, int amt) { + int dmg = amt; + if(hp - dmg < 0) dmg = hp; + hp -= dmg; - hp -= amt; - if (hp <= 0) - transition(AIState::DEAD, src); + if(hp <= 0) transition(AIState::DEAD, src); - return amt; + return dmg; } -void CombatNPC::heal(EntityRef src, int amt) { - // stubbed +int CombatNPC::heal(EntityRef src, int amt) { + int heal = amt; + if(hp + heal > getMaxHP()) heal = getMaxHP() - hp; + hp += heal; + + return heal; } bool CombatNPC::isAlive() { @@ -62,10 +207,37 @@ int CombatNPC::getCurrentHP() { return hp; } +int CombatNPC::getMaxHP() { + return maxHealth; +} + +int CombatNPC::getLevel() { + return level; +} + +std::vector CombatNPC::getGroupMembers() { + std::vector members; + if(group != nullptr) + members = group->members; + else + members.push_back(id); + return members; +} + +int32_t CombatNPC::getCharType() { + if(kind == EntityKind::MOB) + return 4; // eCharType (eCT_MOB) + return 2; // eCharType (eCT_NPC) +} + int32_t CombatNPC::getID() { return id; } +EntityRef CombatNPC::getRef() { + return EntityRef(id); +} + void CombatNPC::step(time_t currTime) { if(stateHandlers.find(state) != stateHandlers.end()) @@ -91,6 +263,7 @@ void CombatNPC::transition(AIState newState, EntityRef src) { event.handler(src, this); */ } +#pragma endregion static std::pair getDamage(int attackPower, int defensePower, bool shouldCrit, bool batteryBoost, int attackerStyle, @@ -299,40 +472,27 @@ static void combatEnd(CNSocket *sock, CNPacketData *data) { plr->healCooldown = 4000; } -static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) { - sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf; +static void dealGooDamage(CNSocket *sock) { Player *plr = PlayerManager::getPlayer(sock); + if(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE) + return; // ignore completely - if ((plr->iConditionBitFlag & CSB_BIT_INFECTION) != (bool)pkt->iFlag) - plr->iConditionBitFlag ^= CSB_BIT_INFECTION; - - INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt1); - - pkt1.eCSTB = ECSB_INFECTION; // eCharStatusTimeBuffID - pkt1.eTBU = 1; // eTimeBuffUpdate - pkt1.eTBT = 0; // eTimeBuffType 1 means nano - pkt1.iConditionBitFlag = plr->iConditionBitFlag; - - sock->sendPacket((void*)&pkt1, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE)); -} - -static void dealGooDamage(CNSocket *sock, int amount) { size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_DotDamage); assert(resplen < CN_PACKET_BUFFER_SIZE - 8); uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; - Player *plr = PlayerManager::getPlayer(sock); - memset(respbuf, 0, resplen); sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf; sSkillResult_DotDamage *dmg = (sSkillResult_DotDamage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK)); - if (plr->iConditionBitFlag & CSB_BIT_PROTECT_INFECTION) { + int amount = PC_MAXHEALTH(plr->level) * 3 / 20; + Buff* protectionBuff = plr->getBuff(ECSB_PROTECT_INFECTION); + if (protectionBuff != nullptr) { amount = -2; // -2 is the magic number for "Protected" to appear as the damage number dmg->bProtected = 1; // eggs allow protection without nanos - if (plr->activeNano != -1 && (plr->iSelfConditionBitFlag & CSB_BIT_PROTECT_INFECTION)) + if (protectionBuff->maxClass() <= BuffClass::NANO && plr->activeNano != -1) plr->Nanos[plr->activeNano].iStamina -= 3; } else { plr->HP -= amount; @@ -356,12 +516,39 @@ static void dealGooDamage(CNSocket *sock, int amount) { dmg->iID = plr->iID; dmg->iDamage = amount; dmg->iHP = plr->HP; - dmg->iConditionBitFlag = plr->iConditionBitFlag; + dmg->iConditionBitFlag = plr->getCompositeCondition(); sock->sendPacket((void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen); PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen); } +static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) { + sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf; + Player *plr = PlayerManager::getPlayer(sock); + + // infection debuff toggles as the client asks it to, + // so we add and remove a permanent debuff + if (pkt->iFlag && !plr->hasBuff(ECSB_INFECTION)) { + BuffStack infection = { + -1, // infinite + 0, // no value + sock, // self-inflicted + BuffClass::ENVIRONMENT + }; + plr->addBuff(ECSB_INFECTION, + [](EntityRef self, Buff* buff, int status, BuffStack* stack) { + Buffs::timeBuffUpdate(self, buff, status, stack); + }, + [](EntityRef self, Buff* buff, time_t currTime) { + if(self.kind == EntityKind::PLAYER) + dealGooDamage(self.sock); + }, + &infection); + } else if(!pkt->iFlag && plr->hasBuff(ECSB_INFECTION)) { + plr->removeBuff(ECSB_INFECTION); + } +} + static void pcAttackChars(CNSocket *sock, CNPacketData *data) { sP_CL2FE_REQ_PC_ATTACK_CHARs* pkt = (sP_CL2FE_REQ_PC_ATTACK_CHARs*)data->buf; Player *plr = PlayerManager::getPlayer(sock); @@ -649,6 +836,7 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) { static void playerTick(CNServer *serv, time_t currTime) { static time_t lastHealTime = 0; + static time_t lastCombatTIme = 0; for (auto& pair : PlayerManager::players) { CNSocket *sock = pair.first; @@ -657,17 +845,12 @@ static void playerTick(CNServer *serv, time_t currTime) { // group ticks if (plr->group != nullptr) - Groups::groupTickInfo(plr); + Groups::groupTickInfo(sock); // do not tick dead players if (plr->HP <= 0) continue; - // fm patch/lake damage - if ((plr->iConditionBitFlag & CSB_BIT_INFECTION) - && !(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) - dealGooDamage(sock, PC_MAXHEALTH(plr->level) * 3 / 20); - // heal if (currTime - lastHealTime >= 4000 && !plr->inCombat && plr->HP < PC_MAXHEALTH(plr->level)) { if (currTime - lastHealTime - plr->healCooldown >= 4000) { @@ -679,44 +862,21 @@ static void playerTick(CNServer *serv, time_t currTime) { plr->healCooldown -= 4000; } + // combat tick + if(currTime - lastCombatTIme >= 2000) { + plr->step(currTime); + transmit = true; + } + // nanos - for (int i = 0; i < 3; i++) { - if (plr->activeNano != 0 && plr->equippedNanos[i] == plr->activeNano) { // tick active nano - sNano& nano = plr->Nanos[plr->activeNano]; - int drainRate = 0; - - if (Abilities::SkillTable.find(nano.iSkillID) != Abilities::SkillTable.end()) { - // nano has skill data - SkillData* skill = &Abilities::SkillTable[nano.iSkillID]; - int boost = Nanos::getNanoBoost(plr); - drainRate = skill->batteryUse[boost * 3]; - - if (skill->drainType == SkillDrainType::PASSIVE) { - // passive buff - std::vector targets; - if (skill->targetType == SkillTargetType::GROUP && plr->group != nullptr) - targets = plr->group->members; // group - else if(skill->targetType == SkillTargetType::SELF) - targets.push_back(sock); // self - - std::cout << "[SKILL] id " << nano.iSkillID << ", type " << skill->skillType << ", target " << (int)skill->targetType << std::endl; - } - } - - nano.iStamina -= 1 + drainRate / 5; - - if (nano.iStamina <= 0) - Nanos::summonNano(sock, -1, true); // unsummon nano silently - - transmit = true; - } else if (plr->Nanos[plr->equippedNanos[i]].iStamina < 150) { // tick resting nano - sNano& nano = plr->Nanos[plr->equippedNanos[i]]; - nano.iStamina += 1; - - if (nano.iStamina > 150) - nano.iStamina = 150; - - transmit = true; + if (plr->activeNano != 0) { // tick active nano + sNano* nano = plr->getActiveNano(); + if (Abilities::SkillTable.find(nano->iSkillID) != Abilities::SkillTable.end()) { + // nano has skill data + SkillData* skill = &Abilities::SkillTable[nano->iSkillID]; + if (skill->drainType == SkillDrainType::PASSIVE) + Nanos::applyNanoBuff(skill, plr); + // ^ composite condition calculation is separate from combat for responsiveness } } @@ -732,6 +892,19 @@ static void playerTick(CNServer *serv, time_t currTime) { PlayerManager::sendToViewable(sock, (void*)&dead, P_FE2CL_PC_SUDDEN_DEAD, sizeof(sP_FE2CL_PC_SUDDEN_DEAD)); } + // process buffsets + auto it = plr->buffs.begin(); + while(it != plr->buffs.end()) { + Buff* buff = (*it).second; + buff->tick(currTime); + if(buff->isStale()) { + // garbage collect + it = plr->buffs.erase(it); + delete buff; + } + else it++; + } + if (transmit) { INITSTRUCT(sP_FE2CL_REP_PC_TICK, pkt); @@ -746,13 +919,15 @@ static void playerTick(CNServer *serv, time_t currTime) { } } - // if this was a heal tick, update the counter outside of the loop + // if this was a heal/combat tick, update the counters outside of the loop if (currTime - lastHealTime >= 4000) lastHealTime = currTime; + if(currTime - lastCombatTIme >= 2000) + lastCombatTIme = currTime; } void Combat::init() { - REGISTER_SHARD_TIMER(playerTick, 2000); + REGISTER_SHARD_TIMER(playerTick, MS_PER_PLAYER_TICK); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, pcAttackNpcs); diff --git a/src/CustomCommands.cpp b/src/CustomCommands.cpp index 3b48907..217a33b 100644 --- a/src/CustomCommands.cpp +++ b/src/CustomCommands.cpp @@ -11,6 +11,7 @@ #include "Missions.hpp" #include "Eggs.hpp" #include "Items.hpp" +#include "Abilities.hpp" #include #include @@ -501,9 +502,12 @@ static void buffCommand(std::string full, std::vector& args, CNSock if (*tmp) return; - if (Eggs::eggBuffPlayer(sock, skillId, 0, duration)<0) + if (Abilities::SkillTable.count(skillId) == 0) { Chat::sendServerMessage(sock, "/buff: unknown skill Id"); - + return; + } + + Eggs::eggBuffPlayer(sock, skillId, 0, duration); } static void eggCommand(std::string full, std::vector& args, CNSocket* sock) { diff --git a/src/Eggs.cpp b/src/Eggs.cpp index cddf6b4..1aa9ef4 100644 --- a/src/Eggs.cpp +++ b/src/Eggs.cpp @@ -13,118 +13,52 @@ using namespace Eggs; -/// sock, CBFlag -> until -std::map, time_t> Eggs::EggBuffs; std::unordered_map Eggs::EggTypes; -int Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) { +void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) { Player* plr = PlayerManager::getPlayer(sock); - // TODO ABILITIES - //int bitFlag = plr->group->conditionBitFlag; - int CBFlag = 0;// Abilities::applyBuff(sock, skillId, 1, 3, bitFlag); - - size_t resplen; - - if (skillId == 183) { - resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Damage); - } else if (skillId == 150) { - resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Heal_HP); - } else { - resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Buff); - } - assert(resplen < CN_PACKET_BUFFER_SIZE - 8); - // we know it's only one trailing struct, so we can skip full validation - - uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; - auto skillUse = (sP_FE2CL_NPC_SKILL_HIT*)respbuf; - - if (skillId == 183) { // damage egg - auto skill = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT)); - memset(respbuf, 0, resplen); - skill->eCT = 1; - skill->iID = plr->iID; - skill->iDamage = PC_MAXHEALTH(plr->level) * Abilities::SkillTable[skillId].values[0][0] / 1000; - plr->HP -= skill->iDamage; - if (plr->HP < 0) - plr->HP = 0; - skill->iHP = plr->HP; - } else if (skillId == 150) { // heal egg - auto skill = (sSkillResult_Heal_HP*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT)); - memset(respbuf, 0, resplen); - skill->eCT = 1; - skill->iID = plr->iID; - skill->iHealHP = PC_MAXHEALTH(plr->level) * Abilities::SkillTable[skillId].values[0][0] / 1000; - plr->HP += skill->iHealHP; - if (plr->HP > PC_MAXHEALTH(plr->level)) - plr->HP = PC_MAXHEALTH(plr->level); - skill->iHP = plr->HP; - } else { // regular buff egg - auto skill = (sSkillResult_Buff*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT)); - memset(respbuf, 0, resplen); - skill->eCT = 1; - skill->iID = plr->iID; - skill->iConditionBitFlag = plr->iConditionBitFlag; + // eggId might be 0 if the buff is made by the /buff command + EntityRef src = eggId == 0 ? sock : EntityRef(eggId); + + if(Abilities::SkillTable.count(skillId) == 0) { + std::cout << "[WARN] egg " << eggId << " has skill ID " << skillId << " which doesn't exist" << std::endl; + return; } - skillUse->iNPC_ID = eggId; - skillUse->iSkillID = skillId; - skillUse->eST = Abilities::SkillTable[skillId].skillType; - skillUse->iTargetCnt = 1; + SkillData* skill = &Abilities::SkillTable[skillId]; + if(skill->drainType == SkillDrainType::PASSIVE) { + // apply buff + if(skill->targetType != SkillTargetType::SELF) { + std::cout << "[WARN] weird skill type for egg " << eggId << " with skill " << skillId << ", should be " << (int)skill->targetType << std::endl; + } - sock->sendPacket((void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen); - PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen); + int timeBuffId = Abilities::getCSTBFromST(skill->skillType); + int value = skill->values[0][0]; + BuffStack eggBuff = { + duration * 1000 / MS_PER_PLAYER_TICK, + value, + src, + BuffClass::EGG + }; + plr->addBuff(timeBuffId, + [](EntityRef self, Buff* buff, int status, BuffStack* stack) { + Buffs::timeBuffUpdate(self, buff, status, stack); + if(status == ETBU_DEL) Buffs::timeBuffTimeout(self); + }, + [](EntityRef self, Buff* buff, time_t currTime) { + // no-op + }, + &eggBuff); + } - if (CBFlag == 0) - return -1; - - std::pair key = std::make_pair(sock, CBFlag); - - // save the buff serverside; - // if you get the same buff again, new duration will override the previous one - time_t until = getTime() + (time_t)duration * 1000; - EggBuffs[key] = until; - - return 0; + // use skill + std::vector targets; + targets.push_back(dynamic_cast(plr)); + Abilities::useNPCSkill(src, skillId, targets); } static void eggStep(CNServer* serv, time_t currTime) { - // tick buffs - time_t timeStamp = currTime; - auto it = EggBuffs.begin(); - while (it != EggBuffs.end()) { - // check remaining time - if (it->second > timeStamp) { - it++; - } else { // if time reached 0 - CNSocket* sock = it->first.first; - int32_t CBFlag = it->first.second; - Player* plr = PlayerManager::getPlayer(sock); - - //int groupFlags = plr->group->conditionBitFlag; - // TODO ABILITIES - //for (auto& pwr : Abilities::Powers) { - // if (pwr.bitFlag == CBFlag) { // pick the power with the right flag and unbuff - // INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp); - // resp.eCSTB = pwr.timeBuffID; - // resp.eTBU = 2; - // resp.eTBT = 3; // for egg buffs - // plr->iConditionBitFlag &= ~CBFlag; - // resp.iConditionBitFlag = plr->iConditionBitFlag |= groupFlags | plr->iSelfConditionBitFlag; - // sock->sendPacket(resp, P_FE2CL_PC_BUFF_UPDATE); - - // INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, resp2); // send a buff timeout to other players - // resp2.eCT = 1; - // resp2.iID = plr->iID; - // resp2.iConditionBitFlag = plr->iConditionBitFlag; - // PlayerManager::sendToViewable(sock, resp2, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT); - // } - //} - // remove buff from the map - it = EggBuffs.erase(it); - } - } - // check dead eggs and eggs in inactive chunks for (auto npc : NPCManager::NPCs) { if (npc.second->kind != EntityKind::EGG) @@ -134,7 +68,7 @@ static void eggStep(CNServer* serv, time_t currTime) { if (!egg->dead || !Chunking::inPopulatedChunks(&egg->viewableChunks)) continue; - if (egg->deadUntil <= timeStamp) { + if (egg->deadUntil <= currTime) { // respawn it egg->dead = false; egg->deadUntil = 0; @@ -191,16 +125,13 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) { EggType* type = &EggTypes[typeId]; - // buff the player - if (type->effectId != 0) - eggBuffPlayer(sock, type->effectId, eggRef.id, type->duration); - /* * SHINY_PICKUP_SUCC is only causing a GUI effect in the client * (buff icon pops up in the bottom of the screen) * so we don't send it for non-effect */ if (type->effectId != 0) { + eggBuffPlayer(sock, type->effectId, eggRef.id, type->duration); INITSTRUCT(sP_FE2CL_REP_SHINY_PICKUP_SUCC, resp); resp.iSkillID = type->effectId; diff --git a/src/Eggs.hpp b/src/Eggs.hpp index eeea678..9c34ec9 100644 --- a/src/Eggs.hpp +++ b/src/Eggs.hpp @@ -10,12 +10,10 @@ struct EggType { }; namespace Eggs { - extern std::map, time_t> EggBuffs; extern std::unordered_map EggTypes; void init(); - /// returns -1 on fail - int eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration); + void eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration); void npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg); } diff --git a/src/Entities.cpp b/src/Entities.cpp index a8c53b1..77b5e3f 100644 --- a/src/Entities.cpp +++ b/src/Entities.cpp @@ -83,6 +83,10 @@ void Egg::enterIntoViewOf(CNSocket *sock) { sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER); } +sNano* Player::getActiveNano() { + return &Nanos[activeNano]; +} + sPCAppearanceData Player::getAppearanceData() { sPCAppearanceData data = {}; data.iID = iID; diff --git a/src/Entities.hpp b/src/Entities.hpp index 1b02f0b..9078d4f 100644 --- a/src/Entities.hpp +++ b/src/Entities.hpp @@ -3,18 +3,16 @@ #include "core/Core.hpp" #include "Chunking.hpp" +#include "EntityRef.hpp" +#include "Buffs.hpp" + #include #include +#include -enum EntityKind { - INVALID, - PLAYER, - SIMPLE_NPC, - COMBAT_NPC, - MOB, - EGG, - BUS -}; +/* forward declaration(s) */ +class Chunk; +struct Group; enum class AIState { INACTIVE, @@ -24,9 +22,6 @@ enum class AIState { DEAD }; -class Chunk; -struct Group; - struct Entity { EntityKind kind = EntityKind::INVALID; int x = 0, y = 0, z = 0; @@ -44,42 +39,6 @@ struct Entity { virtual void disappearFromViewOf(CNSocket *sock) = 0; }; -struct EntityRef { - EntityKind kind; - union { - CNSocket *sock; - int32_t id; - }; - - EntityRef(CNSocket *s); - EntityRef(int32_t i); - - bool isValid() const; - Entity *getEntity() const; - - bool operator==(const EntityRef& other) const { - if (kind != other.kind) - return false; - - if (kind == EntityKind::PLAYER) - return sock == other.sock; - - return id == other.id; - } - - // arbitrary ordering - bool operator<(const EntityRef& other) const { - if (kind == other.kind) { - if (kind == EntityKind::PLAYER) - return sock < other.sock; - else - return id < other.id; - } - - return kind < other.kind; - } -}; - /* * Interfaces */ @@ -88,11 +47,22 @@ public: ICombatant() {} virtual ~ICombatant() {} + virtual bool addBuff(int, BuffCallback, BuffCallback, BuffStack*) = 0; + virtual Buff* getBuff(int) = 0; + virtual void removeBuff(int) = 0; + virtual void removeBuff(int, int) = 0; + virtual bool hasBuff(int) = 0; + virtual int getCompositeCondition() = 0; virtual int takeDamage(EntityRef, int) = 0; - virtual void heal(EntityRef, int) = 0; + virtual int heal(EntityRef, int) = 0; virtual bool isAlive() = 0; virtual int getCurrentHP() = 0; + virtual int getMaxHP() = 0; + virtual int getLevel() = 0; + virtual std::vector getGroupMembers() = 0; + virtual int32_t getCharType() = 0; virtual int32_t getID() = 0; + virtual EntityRef getRef() = 0; virtual void step(time_t currTime) = 0; }; @@ -109,6 +79,7 @@ public: bool loopingPath = false; BaseNPC(int _A, uint64_t iID, int t, int _id) { + kind = EntityKind::SIMPLE_NPC; type = t; hp = 400; angle = _A; @@ -143,17 +114,30 @@ struct CombatNPC : public BaseNPC, public ICombatant { spawnY = y; spawnZ = z; + kind = EntityKind::COMBAT_NPC; + stateHandlers[AIState::INACTIVE] = {}; transitionHandlers[AIState::INACTIVE] = {}; } virtual bool isExtant() override { return hp > 0; } + virtual bool addBuff(int buffId, BuffCallback onUpdate, BuffCallback onTick, BuffStack* stack) override; + virtual Buff* getBuff(int buffId) override; + virtual void removeBuff(int buffId) override; + virtual void removeBuff(int buffId, int buffClass) override; + virtual bool hasBuff(int buffId) override; + virtual int getCompositeCondition() override; virtual int takeDamage(EntityRef src, int amt) override; - virtual void heal(EntityRef src, int amt) override; + virtual int heal(EntityRef src, int amt) override; virtual bool isAlive() override; virtual int getCurrentHP() override; + virtual int getMaxHP() override; + virtual int getLevel() override; + virtual std::vector getGroupMembers() override; + virtual int32_t getCharType() override; virtual int32_t getID() override; + virtual EntityRef getRef() override; virtual void step(time_t currTime) override; virtual void transition(AIState newState, EntityRef src); diff --git a/src/EntityRef.hpp b/src/EntityRef.hpp new file mode 100644 index 0000000..2d974fa --- /dev/null +++ b/src/EntityRef.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "core/Core.hpp" + +/* forward declaration(s) */ +struct Entity; + +enum EntityKind { + INVALID, + PLAYER, + SIMPLE_NPC, + COMBAT_NPC, + MOB, + EGG, + BUS +}; + +struct EntityRef { + EntityKind kind; + union { + CNSocket *sock; + int32_t id; + }; + + EntityRef(CNSocket *s); + EntityRef(int32_t i); + + bool isValid() const; + Entity *getEntity() const; + + bool operator==(const EntityRef& other) const { + if (kind != other.kind) + return false; + + if (kind == EntityKind::PLAYER) + return sock == other.sock; + + return id == other.id; + } + + bool operator!=(const EntityRef& other) const { + return !(*this == other); + } + + // arbitrary ordering + bool operator<(const EntityRef& other) const { + if (kind == other.kind) { + if (kind == EntityKind::PLAYER) + return sock < other.sock; + else + return id < other.id; + } + + return kind < other.kind; + } +}; \ No newline at end of file diff --git a/src/Groups.cpp b/src/Groups.cpp index 5655b54..2ba539b 100644 --- a/src/Groups.cpp +++ b/src/Groups.cpp @@ -15,7 +15,54 @@ using namespace Groups; -void Groups::addToGroup(EntityRef member, Group* group) { +Group::Group(EntityRef leader) { + addToGroup(this, leader); +} + +static void attachGroupData(std::vector& pcs, std::vector& npcs, uint8_t* pivot) { + for(EntityRef pcRef : pcs) { + sPCGroupMemberInfo* info = (sPCGroupMemberInfo*)pivot; + + Player* plr = PlayerManager::getPlayer(pcRef.sock); + info->iPC_ID = plr->iID; + info->iPCUID = plr->PCStyle.iPC_UID; + info->iNameCheck = plr->PCStyle.iNameCheck; + memcpy(info->szFirstName, plr->PCStyle.szFirstName, sizeof(plr->PCStyle.szFirstName)); + memcpy(info->szLastName, plr->PCStyle.szLastName, sizeof(plr->PCStyle.szLastName)); + info->iSpecialState = plr->iSpecialState; + info->iLv = plr->level; + info->iHP = plr->HP; + info->iMaxHP = PC_MAXHEALTH(plr->level); + // info->iMapType = 0; + // info->iMapNum = 0; + info->iX = plr->x; + info->iY = plr->y; + info->iZ = plr->z; + if(plr->activeNano > 0) { + info->Nano = *plr->getActiveNano(); + info->bNano = true; + } + + pivot = (uint8_t*)(info + 1); + } + for(EntityRef npcRef : npcs) { + sNPCGroupMemberInfo* info = (sNPCGroupMemberInfo*)pivot; + + // probably should not assume that the combatant is an + // entity, but it works for now + BaseNPC* npc = (BaseNPC*)npcRef.getEntity(); + info->iNPC_ID = npcRef.id; + info->iNPC_Type = npc->type; + info->iHP = npc->hp; + info->iX = npc->x; + info->iY = npc->y; + info->iZ = npc->z; + + pivot = (uint8_t*)(info + 1); + } +} + +void Groups::addToGroup(Group* group, EntityRef member) { if (member.kind == EntityKind::PLAYER) { Player* plr = PlayerManager::getPlayer(member.sock); plr->group = group; @@ -29,37 +76,102 @@ void Groups::addToGroup(EntityRef member, Group* group) { } group->members.push_back(member); + + if(member.kind == EntityKind::PLAYER) { + std::vector pcs = group->filter(EntityKind::PLAYER); + std::vector npcs = group->filter(EntityKind::COMBAT_NPC); + size_t pcCount = pcs.size(); + size_t npcCount = npcs.size(); + + uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; + memset(respbuf, 0, CN_PACKET_BUFFER_SIZE); + sP_FE2CL_PC_GROUP_JOIN* pkt = (sP_FE2CL_PC_GROUP_JOIN*)respbuf; + + pkt->iID_NewMember = PlayerManager::getPlayer(member.sock)->iID; + pkt->iMemberPCCnt = (int32_t)pcCount; + pkt->iMemberNPCCnt = (int32_t)npcCount; + + if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), pcCount, sizeof(sPCGroupMemberInfo)) + || !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) { + std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size" << std::endl; + } else { + uint8_t* pivot = (uint8_t*)(pkt + 1); + attachGroupData(pcs, npcs, pivot); + // PC_GROUP_JOIN_SUCC and PC_GROUP_JOIN carry identical payloads but have different IDs + // (and the client does care!) so we need to send one to the new member + // and the other to the rest + size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo); + member.sock->sendPacket(respbuf, P_FE2CL_PC_GROUP_JOIN_SUCC, resplen); + sendToGroup(group, member, respbuf, P_FE2CL_PC_GROUP_JOIN, resplen); + } + } } -void Groups::removeFromGroup(EntityRef member, Group* group) { +bool Groups::removeFromGroup(Group* group, EntityRef member) { if (member.kind == EntityKind::PLAYER) { Player* plr = PlayerManager::getPlayer(member.sock); plr->group = nullptr; // no dangling pointers here muahaahahah + + INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, leavePkt); + member.sock->sendPacket(leavePkt, P_FE2CL_PC_GROUP_LEAVE_SUCC); } else if (member.kind == EntityKind::COMBAT_NPC) { CombatNPC* npc = (CombatNPC*)member.getEntity(); npc->group = nullptr; } else { - std::cout << "[WARN] Adding a weird entity type to a group" << std::endl; + std::cout << "[WARN] Removing a weird entity type from a group" << std::endl; } auto it = std::find(group->members.begin(), group->members.end(), member); if (it == group->members.end()) { std::cout << "[WARN] Tried to remove a member that isn't in the group" << std::endl; - return; + } else { + group->members.erase(it); } - group->members.erase(it); + if(member.kind == EntityKind::PLAYER) { + std::vector pcs = group->filter(EntityKind::PLAYER); + std::vector npcs = group->filter(EntityKind::COMBAT_NPC); + size_t pcCount = pcs.size(); + size_t npcCount = npcs.size(); - if (group->members.empty()) delete group; // cleanup memory + uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; + memset(respbuf, 0, CN_PACKET_BUFFER_SIZE); + sP_FE2CL_PC_GROUP_LEAVE* pkt = (sP_FE2CL_PC_GROUP_LEAVE*)respbuf; + + pkt->iID_LeaveMember = PlayerManager::getPlayer(member.sock)->iID; + pkt->iMemberPCCnt = (int32_t)pcCount; + pkt->iMemberNPCCnt = (int32_t)npcCount; + + if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), pcCount, sizeof(sPCGroupMemberInfo)) + || !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) { + std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_LEAVE packet size" << std::endl; + } else { + uint8_t* pivot = (uint8_t*)(pkt + 1); + attachGroupData(pcs, npcs, pivot); + sendToGroup(group, respbuf, P_FE2CL_PC_GROUP_LEAVE, + sizeof(sP_FE2CL_PC_GROUP_LEAVE) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo)); + } + } + + if (group->members.size() == 1) { + return removeFromGroup(group, group->members.back()); + } + + if (group->members.empty()) { + delete group; // cleanup memory + return true; + } + return false; } void Groups::disbandGroup(Group* group) { // remove everyone from the group!! - std::vector members = group->members; - for (EntityRef member : members) { - removeFromGroup(member, group); + bool done = false; + while(!done) { + EntityRef back = group->members.back(); + done = removeFromGroup(group, back); } } @@ -114,7 +226,7 @@ static void joinGroup(CNSocket* sock, CNPacketData* data) { Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_From); if (otherPlr == nullptr) - return; + return; // disconnect or something int size = otherPlr->group == nullptr ? 1 : otherPlr->group->filter(EntityKind::PLAYER).size(); @@ -125,220 +237,69 @@ static void joinGroup(CNSocket* sock, CNPacketData* data) { return; } - if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), size + 1, sizeof(sPCGroupMemberInfo))) { - std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n"; - return; - } - if (otherPlr->group == nullptr) { // create group - otherPlr->group = new Group(); // spooky - addToGroup(PlayerManager::getSockFromID(recv->iID_From), otherPlr->group); + EntityRef otherPlrRef = PlayerManager::getSockFromID(recv->iID_From); + otherPlr->group = new Group(otherPlrRef); } - addToGroup(sock, otherPlr->group); - auto players = otherPlr->group->filter(EntityKind::PLAYER); - - size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + players.size() * sizeof(sPCGroupMemberInfo); - uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; - - memset(respbuf, 0, resplen); - - sP_FE2CL_PC_GROUP_JOIN *resp = (sP_FE2CL_PC_GROUP_JOIN*)respbuf; - sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_JOIN)); - - resp->iID_NewMember = plr->iID; - resp->iMemberPCCnt = players.size(); - - //int bitFlag = otherPlr->group->conditionBitFlag; - for (int i = 0; i < players.size(); i++) { - - Player* varPlr = PlayerManager::getPlayer(players[i].sock); - CNSocket* sockTo = players[i].sock; - - if (varPlr == nullptr || sockTo == nullptr) - continue; - - respdata[i].iPC_ID = varPlr->iID; - respdata[i].iPCUID = varPlr->PCStyle.iPC_UID; - respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck; - memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName)); - memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName)); - respdata[i].iSpecialState = varPlr->iSpecialState; - respdata[i].iLv = varPlr->level; - respdata[i].iHP = varPlr->HP; - respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level); - //respdata[i].iMapType = 0; - //respdata[i].iMapNum = 0; - respdata[i].iX = varPlr->x; - respdata[i].iY = varPlr->y; - respdata[i].iZ = varPlr->z; - // client doesnt read nano data here - - if (varPlr != plr) { // apply the new member's buffs to the group and the group's buffs to the new member - // TODO ABILITIES - /*if (Abilities::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3) - Abilities::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 1, 1, bitFlag); - if (Abilities::SkillTable[plr->Nanos[plr->activeNano].iSkillID].targetType == 3) - Abilities::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 1, 1, bitFlag);*/ - } - } - - Groups::sendToGroup(otherPlr->group, (void*)&respbuf, P_FE2CL_PC_GROUP_JOIN, resplen); + addToGroup(otherPlr->group, sock); } static void leaveGroup(CNSocket* sock, CNPacketData* data) { Player* plr = PlayerManager::getPlayer(sock); - groupKick(plr); + groupKick(plr->group, sock); } void Groups::sendToGroup(Group* group, void* buf, uint32_t type, size_t size) { auto players = group->filter(EntityKind::PLAYER); - for (int i = 0; i < players.size(); i++) { - CNSocket* sock = players[i].sock; - sock->sendPacket(buf, type, size); + for (EntityRef ref : players) { + ref.sock->sendPacket(buf, type, size); } } -void Groups::groupTickInfo(Player* plr) { - - auto players = plr->group->filter(EntityKind::PLAYER); - - if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), players.size(), sizeof(sPCGroupMemberInfo))) { - std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n"; - return; +void Groups::sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size) { + auto players = group->filter(EntityKind::PLAYER); + for (EntityRef ref : players) { + if(ref != excluded) ref.sock->sendPacket(buf, type, size); } +} + +void Groups::groupTickInfo(CNSocket* sock) { + Player* plr = PlayerManager::getPlayer(sock); + Group* group = plr->group; + std::vector pcs = group->filter(EntityKind::PLAYER); + std::vector npcs = group->filter(EntityKind::COMBAT_NPC); + size_t pcCount = pcs.size(); + size_t npcCount = npcs.size(); - size_t resplen = sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + players.size() * sizeof(sPCGroupMemberInfo); uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; + memset(respbuf, 0, CN_PACKET_BUFFER_SIZE); + sP_FE2CL_PC_GROUP_MEMBER_INFO* pkt = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf; - memset(respbuf, 0, resplen); + pkt->iID = plr->iID; + pkt->iMemberPCCnt = (int32_t)pcCount; + pkt->iMemberNPCCnt = (int32_t)npcCount; - sP_FE2CL_PC_GROUP_MEMBER_INFO *resp = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf; - sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO)); - - resp->iID = plr->iID; - resp->iMemberPCCnt = players.size(); - - for (int i = 0; i < players.size(); i++) { - EntityRef member = players[i]; - Player* varPlr = PlayerManager::getPlayer(member.sock); - - if (varPlr == nullptr) - continue; - - respdata[i].iPC_ID = varPlr->iID; - respdata[i].iPCUID = varPlr->PCStyle.iPC_UID; - respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck; - memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName)); - memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName)); - respdata[i].iSpecialState = varPlr->iSpecialState; - respdata[i].iLv = varPlr->level; - respdata[i].iHP = varPlr->HP; - respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level); - //respdata[i].iMapType = 0; - //respdata[i].iMapNum = 0; - respdata[i].iX = varPlr->x; - respdata[i].iY = varPlr->y; - respdata[i].iZ = varPlr->z; - if (varPlr->activeNano > 0) { - respdata[i].bNano = 1; - respdata[i].Nano = varPlr->Nanos[varPlr->activeNano]; - } - } - - sendToGroup(plr->group, (void*)&respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO, resplen); -} - -static void groupUnbuff(Player* plr) { - Group* group = plr->group; - for (int i = 0; i < group->members.size(); i++) { - for (int n = 0; n < group->members.size(); n++) { - if (i == n) - continue; - - EntityRef other = group->members[n]; - - // TODO ABILITIES - //Abilities::applyBuff(sock, otherPlr->Nanos[otherPlr->activeNano].iSkillID, 2, 1, 0); - } + if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), pcCount, sizeof(sPCGroupMemberInfo)) + || !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) { + std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_MEMBER_INFO packet size" << std::endl; + } else { + uint8_t* pivot = (uint8_t*)(pkt + 1); + attachGroupData(pcs, npcs, pivot); + sock->sendPacket(respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO, + sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo)); } } -void Groups::groupKick(Player* plr) { - Group* group = plr->group; +void Groups::groupKick(Group* group, EntityRef ref) { // if you are the group leader, destroy your own group and kick everybody - if (plr->group->members[0] == PlayerManager::getSockFromID(plr->iID)) { - groupUnbuff(plr); - INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1); - sendToGroup(plr->group, (void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC)); - disbandGroup(plr->group); + if (group->members[0] == ref) { + disbandGroup(group); return; } - auto players = group->filter(EntityKind::PLAYER); - - if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), players.size() - 1, sizeof(sPCGroupMemberInfo))) { - std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_LEAVE packet size\n"; - return; - } - - size_t resplen = sizeof(sP_FE2CL_PC_GROUP_LEAVE) + (players.size() - 1) * sizeof(sPCGroupMemberInfo); - uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; - - memset(respbuf, 0, resplen); - - sP_FE2CL_PC_GROUP_LEAVE *resp = (sP_FE2CL_PC_GROUP_LEAVE*)respbuf; - sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_LEAVE)); - - resp->iID_LeaveMember = plr->iID; - resp->iMemberPCCnt = players.size() - 1; - - int bitFlag = 0; // TODO ABILITIES getGroupFlags(otherPlr) & ~plr->iGroupConditionBitFlag; - - CNSocket* sock = PlayerManager::getSockFromID(plr->iID); - - if (sock == nullptr) - return; - - removeFromGroup(sock, group); - - players = group->filter(EntityKind::PLAYER); - for (int i = 0; i < players.size(); i++) { - CNSocket* sockTo = players[i].sock; - Player* varPlr = PlayerManager::getPlayer(sock); - - if (varPlr == nullptr || sockTo == nullptr) - continue; - - respdata[i].iPC_ID = varPlr->iID; - respdata[i].iPCUID = varPlr->PCStyle.iPC_UID; - respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck; - memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName)); - memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName)); - respdata[i].iSpecialState = varPlr->iSpecialState; - respdata[i].iLv = varPlr->level; - respdata[i].iHP = varPlr->HP; - respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level); - // respdata[i]].iMapType = 0; - // respdata[i]].iMapNum = 0; - respdata[i].iX = varPlr->x; - respdata[i].iY = varPlr->y; - respdata[i].iZ = varPlr->z; - // client doesnt read nano data here - - // remove the leaving member's buffs from the group and remove the group buffs from the leaving member. - // TODO ABILITIES - /*if (Abilities::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3) - Abilities::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 2, 1, 0); - if (Abilities::SkillTable[plr->Nanos[varPlr->activeNano].iSkillID].targetType == 3) - Abilities::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 2, 1, bitFlag);*/ - } - - sendToGroup(group, (void*)&respbuf, P_FE2CL_PC_GROUP_LEAVE, resplen); - - INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1); - sock->sendPacket((void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC)); + removeFromGroup(group, ref); } void Groups::init() { diff --git a/src/Groups.hpp b/src/Groups.hpp index 18cf6e5..d8ddd67 100644 --- a/src/Groups.hpp +++ b/src/Groups.hpp @@ -19,16 +19,19 @@ struct Group { }); return filtered; } + + Group(EntityRef leader); }; namespace Groups { void init(); void sendToGroup(Group* group, void* buf, uint32_t type, size_t size); - void groupTickInfo(Player* plr); - void groupKick(Player* plr); + void sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size); + void groupTickInfo(CNSocket* sock); - void addToGroup(EntityRef member, Group* group); - void removeFromGroup(EntityRef member, Group* group); + void groupKick(Group* group, EntityRef ref); + void addToGroup(Group* group, EntityRef member); + bool removeFromGroup(Group* group, EntityRef member); // true iff group deleted void disbandGroup(Group* group); } diff --git a/src/Items.cpp b/src/Items.cpp index fc993e2..2698c14 100644 --- a/src/Items.cpp +++ b/src/Items.cpp @@ -9,6 +9,7 @@ #include "Eggs.hpp" #include "MobAI.hpp" #include "Missions.hpp" +#include "Buffs.hpp" #include // for memset() #include @@ -484,27 +485,31 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) { resp->eST = EST_NANOSTIMPAK; resp->iSkillID = 144; - int value1 = CSB_BIT_STIMPAKSLOT1 << request->iNanoSlot; - int value2 = ECSB_STIMPAKSLOT1 + request->iNanoSlot; + int eCSB = ECSB_STIMPAKSLOT1 + request->iNanoSlot; respdata->eCT = 1; respdata->iID = player->iID; - respdata->iConditionBitFlag = value1; + respdata->iConditionBitFlag = CSB_FROM_ECSB(eCSB); - INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt); - pkt.eCSTB = value2; // eCharStatusTimeBuffID - pkt.eTBU = 1; // eTimeBuffUpdate - pkt.eTBT = 1; // eTimeBuffType 1 means nano - pkt.iConditionBitFlag = player->iConditionBitFlag |= value1; - sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE); + int durationMilliseconds = Abilities::SkillTable[144].durationTime[0] * 100; + BuffStack gumballBuff = { + durationMilliseconds / MS_PER_PLAYER_TICK, + 0, + sock, + BuffClass::CASH_ITEM // or BuffClass::ITEM? + }; + player->addBuff(eCSB, + [](EntityRef self, Buff* buff, int status, BuffStack* stack) { + Buffs::timeBuffUpdate(self, buff, status, stack); + }, + [](EntityRef self, Buff* buff, time_t currTime) { + // no-op + }, + &gumballBuff); sock->sendPacket((void*)&respbuf, P_FE2CL_REP_PC_ITEM_USE_SUCC, resplen); // update inventory serverside player->Inven[resp->iSlotNum] = resp->RemainItem; - - std::pair key = std::make_pair(sock, value1); - time_t until = getTime() + (time_t)Abilities::SkillTable[144].durationTime[0] * 100; - Eggs::EggBuffs[key] = until; } static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) { @@ -757,7 +762,7 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) { plr->money += miscDropType.taroAmount; // money nano boost - if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) { + if (plr->hasBuff(ECSB_REWARD_CASH)) { int boost = 0; if (Nanos::getNanoBoost(plr)) // for gumballs boost = 1; @@ -772,7 +777,7 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo if (levelDifference > 0) fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0; // scavenger nano boost - if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) { + if (plr->hasBuff(ECSB_REWARD_BLOB)) { int boost = 0; if (Nanos::getNanoBoost(plr)) // for gumballs boost = 1; diff --git a/src/Missions.cpp b/src/Missions.cpp index db0d17c..b13699a 100644 --- a/src/Missions.cpp +++ b/src/Missions.cpp @@ -163,14 +163,14 @@ static int giveMissionReward(CNSocket *sock, int task, int choice=0) { // update player plr->money += reward->money; - if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) { // nano boost for taros + 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; } - if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) { // nano boost for fm + if (plr->hasBuff(ECSB_REWARD_BLOB)) { // nano boost for fm int boost = 0; if (Nanos::getNanoBoost(plr)) // for gumballs boost = 1; diff --git a/src/MobAI.cpp b/src/MobAI.cpp index 7313b85..26552f1 100644 --- a/src/MobAI.cpp +++ b/src/MobAI.cpp @@ -187,7 +187,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) { int mobRange = mob->sightRange; - if (plr->iConditionBitFlag & CSB_BIT_UP_STEALTH + if (plr->hasBuff(ECSB_UP_STEALTH) || Racing::EPRaces.find(s) != Racing::EPRaces.end()) mobRange /= 3; @@ -315,7 +315,7 @@ static void dealCorruption(Mob *mob, std::vector targetData, int skillID, i plr->HP -= respdata[i].iDamage; respdata[i].iHP = plr->HP; - respdata[i].iConditionBitFlag = plr->iConditionBitFlag; + respdata[i].iConditionBitFlag = plr->getCompositeCondition(); if (plr->HP <= 0) { if (!MobAI::aggroCheck(mob, getTime())) diff --git a/src/NPCManager.cpp b/src/NPCManager.cpp index 5baddb6..f1a5e13 100644 --- a/src/NPCManager.cpp +++ b/src/NPCManager.cpp @@ -81,7 +81,7 @@ void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I, Chunking::updateEntityChunk({id}, oldChunk, newChunk); } -void NPCManager::sendToViewable(BaseNPC *npc, void *buf, uint32_t type, size_t size) { +void NPCManager::sendToViewable(Entity *npc, void *buf, uint32_t type, size_t size) { for (auto it = npc->viewableChunks.begin(); it != npc->viewableChunks.end(); it++) { Chunk* chunk = *it; for (const EntityRef& ref : chunk->entities) { diff --git a/src/NPCManager.hpp b/src/NPCManager.hpp index 3429d42..5009e3c 100644 --- a/src/NPCManager.hpp +++ b/src/NPCManager.hpp @@ -42,7 +42,7 @@ namespace NPCManager { void destroyNPC(int32_t); void updateNPCPosition(int32_t, int X, int Y, int Z, uint64_t I, int angle); - void sendToViewable(BaseNPC* npc, void* buf, uint32_t type, size_t size); + void sendToViewable(Entity* npc, void* buf, uint32_t type, size_t size); BaseNPC *summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn=false, bool baseInstance=false); diff --git a/src/Nanos.cpp b/src/Nanos.cpp index 088d9cb..f587b68 100644 --- a/src/Nanos.cpp +++ b/src/Nanos.cpp @@ -69,6 +69,52 @@ void Nanos::addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm) PlayerManager::sendToViewable(sock, resp2, P_FE2CL_REP_PC_CHANGE_LEVEL); } +std::vector Nanos::applyNanoBuff(SkillData* skill, Player* plr) { + assert(skill->drainType == SkillDrainType::PASSIVE); + + EntityRef self = PlayerManager::getSockFromID(plr->iID); + std::vector affected; + std::vector targets; + if (skill->targetType == SkillTargetType::GROUP) { + targets = plr->getGroupMembers(); // group + } + else if(skill->targetType == SkillTargetType::SELF) { + targets.push_back(self); // self + } else { + std::cout << "[WARN] Passive skill with type " << skill->skillType << " has target type MOB" << std::endl; + } + + int timeBuffId = Abilities::getCSTBFromST(skill->skillType); + int boost = Nanos::getNanoBoost(plr) ? 3 : 0; + int value = skill->values[0][boost]; + + BuffStack passiveBuff = { + 1, // passive nano buffs refreshed every tick + value, + self, + BuffClass::NONE, // overwritten per target + }; + + for (EntityRef target : targets) { + Entity* entity = target.getEntity(); + if (entity->kind != PLAYER && entity->kind != COMBAT_NPC && entity->kind != MOB) + continue; // not a combatant + + passiveBuff.buffStackClass = target == self ? BuffClass::NANO : BuffClass::GROUP_NANO; + ICombatant* combatant = dynamic_cast(entity); + if(combatant->addBuff(timeBuffId, + [](EntityRef self, Buff* buff, int status, BuffStack* stack) { + Buffs::timeBuffUpdate(self, buff, status, stack); + }, + [](EntityRef self, Buff* buff, time_t currTime) { + // no-op + }, + &passiveBuff)) affected.push_back(combatant); + } + + return affected; +} + void Nanos::summonNano(CNSocket *sock, int slot, bool silent) { INITSTRUCT(sP_FE2CL_REP_NANO_ACTIVE_SUCC, resp); resp.iActiveNanoSlotNum = slot; @@ -86,10 +132,16 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) { return; // sanity check plr->activeNano = nanoID; + sNano& nano = plr->Nanos[nanoID]; - int16_t skillID = plr->Nanos[nanoID].iSkillID; - if (Abilities::SkillTable.count(skillID) > 0 && Abilities::SkillTable[skillID].drainType == SkillDrainType::PASSIVE) - resp.eCSTB___Add = 1; // passive buff effect + SkillData* skill = Abilities::SkillTable.count(nano.iSkillID) > 0 + ? &Abilities::SkillTable[nano.iSkillID] : nullptr; + if (slot != -1 && skill != nullptr && skill->drainType == SkillDrainType::PASSIVE) { + // passive buff effect + resp.eCSTB___Add = 1; + std::vector affectedCombatants = applyNanoBuff(skill, plr); + if(!affectedCombatants.empty()) Abilities::useNanoSkill(sock, nano, affectedCombatants); + } if (!silent) // silent nano death but only for the summoning player sock->sendPacket(resp, P_FE2CL_REP_NANO_ACTIVE_SUCC); @@ -97,7 +149,7 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) { // Send to other players, these players can't handle silent nano deaths so this packet needs to be sent. INITSTRUCT(sP_FE2CL_NANO_ACTIVE, pkt1); pkt1.iPC_ID = plr->iID; - pkt1.Nano = plr->Nanos[nanoID]; + pkt1.Nano = nano; PlayerManager::sendToViewable(sock, pkt1, P_FE2CL_NANO_ACTIVE); } @@ -184,7 +236,7 @@ int Nanos::nanoStyle(int nanoID) { bool Nanos::getNanoBoost(Player* plr) { for (int i = 0; i < 3; i++) if (plr->equippedNanos[i] == plr->activeNano) - if (plr->iConditionBitFlag & (CSB_BIT_STIMPAKSLOT1 << i)) + if (plr->hasBuff(ECSB_STIMPAKSLOT1 + i)) return true; return false; } @@ -208,18 +260,6 @@ static void nanoEquipHandler(CNSocket* sock, CNPacketData* data) { // Update player plr->equippedNanos[nano->iNanoSlotNum] = nano->iNanoID; - // Unbuff gumballs - int value1 = CSB_BIT_STIMPAKSLOT1 << nano->iNanoSlotNum; - if (plr->iConditionBitFlag & value1) { - int value2 = ECSB_STIMPAKSLOT1 + nano->iNanoSlotNum; - INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt); - pkt.eCSTB = value2; // eCharStatusTimeBuffID - pkt.eTBU = 2; // eTimeBuffUpdate - pkt.eTBT = 1; // eTimeBuffType 1 means nano - pkt.iConditionBitFlag = plr->iConditionBitFlag &= ~value1; - sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE); - } - // unsummon nano if replaced if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum]) summonNano(sock, -1); diff --git a/src/Nanos.hpp b/src/Nanos.hpp index 2002330..b4a3955 100644 --- a/src/Nanos.hpp +++ b/src/Nanos.hpp @@ -3,6 +3,7 @@ #include "core/Core.hpp" #include "Player.hpp" +#include "Abilities.hpp" #include @@ -25,4 +26,5 @@ namespace Nanos { void summonNano(CNSocket* sock, int slot, bool silent = false); int nanoStyle(int nanoID); bool getNanoBoost(Player* plr); + std::vector applyNanoBuff(SkillData* skill, Player* plr); } diff --git a/src/Player.hpp b/src/Player.hpp index c58b7d6..224ea90 100644 --- a/src/Player.hpp +++ b/src/Player.hpp @@ -7,6 +7,10 @@ #include +/* forward declaration(s) */ +class Buff; +struct BuffStack; + #define ACTIVE_MISSION_COUNT 6 #define PC_MAXHEALTH(level) (925 + 75 * (level)) @@ -32,9 +36,8 @@ struct Player : public Entity, public ICombatant { int8_t iPCState = 0; int32_t iWarpLocationFlag = 0; int64_t aSkywayLocationFlag[2] = {}; - int32_t iConditionBitFlag = 0; - int32_t iSelfConditionBitFlag = 0; int8_t iSpecialState = 0; + std::unordered_map buffs = {}; int angle = 0; int lastX = 0, lastY = 0, lastZ = 0, lastAngle = 0; @@ -86,13 +89,25 @@ struct Player : public Entity, public ICombatant { virtual void enterIntoViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override; + virtual bool addBuff(int buffId, BuffCallback onUpdate, BuffCallback onTick, BuffStack* stack) override; + virtual Buff* getBuff(int buffId) override; + virtual void removeBuff(int buffId) override; + virtual void removeBuff(int buffId, int buffClass) override; + virtual bool hasBuff(int buffId) override; + virtual int getCompositeCondition() override; virtual int takeDamage(EntityRef src, int amt) override; - virtual void heal(EntityRef src, int amt) override; + virtual int heal(EntityRef src, int amt) override; virtual bool isAlive() override; virtual int getCurrentHP() override; + virtual int getMaxHP() override; + virtual int getLevel() override; + virtual std::vector getGroupMembers() override; + virtual int32_t getCharType() override; virtual int32_t getID() override; + virtual EntityRef getRef() override; virtual void step(time_t currTime) override; + sNano* getActiveNano(); sPCAppearanceData getAppearanceData(); }; diff --git a/src/PlayerManager.cpp b/src/PlayerManager.cpp index 91d4c69..a4c4865 100644 --- a/src/PlayerManager.cpp +++ b/src/PlayerManager.cpp @@ -36,8 +36,13 @@ void PlayerManager::removePlayer(CNSocket* key) { Player* plr = getPlayer(key); uint64_t fromInstance = plr->instanceID; + // free buff memory + for(auto buffEntry : plr->buffs) + delete buffEntry.second; + + // leave group if(plr->group != nullptr) - Groups::groupKick(plr); + Groups::groupKick(plr->group, key); // remove player's bullets Combat::Bullets.erase(plr->iID); @@ -61,16 +66,6 @@ void PlayerManager::removePlayer(CNSocket* key) { // if the player was in a lair, clean it up Chunking::destroyInstanceIfEmpty(fromInstance); - // remove player's buffs from the server - auto it = Eggs::EggBuffs.begin(); - while (it != Eggs::EggBuffs.end()) { - if (it->first.first == key) { - it = Eggs::EggBuffs.erase(it); - } - else - it++; - } - std::cout << players.size() << " players" << std::endl; } @@ -403,7 +398,7 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) { switch ((ePCRegenType)reviveData->iRegenType) { case ePCRegenType::HereByPhoenix: // nano revive - if (!(plr->iConditionBitFlag & CSB_BIT_PHOENIX)) + if (!(plr->hasBuff(ECSB_PHOENIX))) return; // sanity check plr->Nanos[plr->activeNano].iStamina = 0; // TODO ABILITIES @@ -470,10 +465,8 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) { resp2.PCRegenDataForOtherPC.iAngle = plr->angle; if (plr->group != nullptr) { - // TODO ABILITIES - //int bitFlag = plr->group->conditionBitFlag; - //resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->iConditionBitFlag = plr->iSelfConditionBitFlag | bitFlag; - + + resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->getCompositeCondition(); resp2.PCRegenDataForOtherPC.iPCState = plr->iPCState; resp2.PCRegenDataForOtherPC.iSpecialState = plr->iSpecialState; resp2.PCRegenDataForOtherPC.Nano = plr->Nanos[plr->activeNano]; diff --git a/src/core/CNProtocol.hpp b/src/core/CNProtocol.hpp index 782190f..9d1a6b2 100644 --- a/src/core/CNProtocol.hpp +++ b/src/core/CNProtocol.hpp @@ -93,7 +93,7 @@ inline constexpr bool isOutboundPacketID(uint32_t id) { // overflow-safe validation of variable-length packets // for outbound packets -inline constexpr bool validOutVarPacket(size_t base, int32_t npayloads, size_t plsize) { +inline constexpr bool validOutVarPacket(size_t base, size_t npayloads, size_t plsize) { // check for multiplication overflow if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize) return false; @@ -110,7 +110,7 @@ inline constexpr bool validOutVarPacket(size_t base, int32_t npayloads, size_t p } // for inbound packets -inline constexpr bool validInVarPacket(size_t base, int32_t npayloads, size_t plsize, size_t datasize) { +inline constexpr bool validInVarPacket(size_t base, size_t npayloads, size_t plsize, size_t datasize) { // check for multiplication overflow if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize) return false; diff --git a/src/core/Defines.hpp b/src/core/Defines.hpp index 185b5c5..4e20658 100644 --- a/src/core/Defines.hpp +++ b/src/core/Defines.hpp @@ -118,6 +118,27 @@ enum { ECSTB__END = 26, }; +enum { + ETBU_NONE = 0, + ETBU_ADD = 1, + ETBU_DEL = 2, + ETBU_CHANGE = 3, + ETBU__END = 4, +}; + +enum { + ETBT_NONE = 0, + ETBT_NANO = 1, + ETBT_GROUPNANO = 2, + ETBT_SHINY = 3, + ETBT_LANDEFFECT = 4, + ETBT_ITEM = 5, + ETBT_CASHITEM = 6, + ETBT__END = 7, + ETBT_SKILL = 1, + ETBT_GROUPSKILL = 2 +}; + enum { SUCC = 1, FAIL = 0, diff --git a/src/servers/CNShardServer.hpp b/src/servers/CNShardServer.hpp index 460d2a8..c585428 100644 --- a/src/servers/CNShardServer.hpp +++ b/src/servers/CNShardServer.hpp @@ -7,6 +7,7 @@ #define REGISTER_SHARD_PACKET(pactype, handlr) CNShardServer::ShardPackets[pactype] = handlr; #define REGISTER_SHARD_TIMER(handlr, delta) CNShardServer::Timers.push_back(TimerEvent(handlr, delta)); +#define MS_PER_PLAYER_TICK 500 class CNShardServer : public CNServer { private: