diff --git a/.gitignore b/.gitignore index 7ce449c..ab596ff 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ build/ version.h infer-out gmon.out +*.bak + diff --git a/CMakeLists.txt b/CMakeLists.txt index 2093a94..7aee17d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,8 @@ project(OpenFusion) set(CMAKE_CXX_STANDARD 17) -execute_process(COMMAND git describe --tags OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE) +execute_process(COMMAND git describe --tags OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) # OpenFusion supports multiple packet/struct versions # 104 is the default version to build which can be changed 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 baec351..a3b3f86 100644 --- a/src/Abilities.cpp +++ b/src/Abilities.cpp @@ -1,804 +1,494 @@ #include "Abilities.hpp" -#include "PlayerManager.hpp" -#include "Player.hpp" + +#include "servers/CNShardServer.hpp" + #include "NPCManager.hpp" +#include "PlayerManager.hpp" +#include "Buffs.hpp" #include "Nanos.hpp" -#include "Groups.hpp" -#include "Eggs.hpp" +#include "MobAI.hpp" -/* - * TODO: This file is in desperate need of deduplication and rewriting. - */ +using namespace Abilities; -std::map Nanos::SkillTable; +std::map Abilities::SkillTable; -/* - * targetData approach - * first integer is the count - * second to fifth integers are IDs, these can be either player iID or mob's iID - */ -std::vector Nanos::findTargets(Player* plr, int skillID, CNPacketData* data) { - std::vector tD(5); +#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; - if (SkillTable[skillID].targetType <= 2 && data != nullptr) { // client gives us the targets - sP_CL2FE_REQ_NANO_SKILL_USE* pkt = (sP_CL2FE_REQ_NANO_SKILL_USE*)data->buf; + int damage = (int)(skill->values[0][power] * scalingFactor); + int dealt = target->takeDamage(sourceRef, damage); - // validate request check - if (!validInVarPacket(sizeof(sP_CL2FE_REQ_NANO_SKILL_USE), pkt->iTargetCnt, sizeof(int32_t), data->size)) { - std::cout << "[WARN] bad sP_CL2FE_REQ_NANO_SKILL_USE packet size" << std::endl; - return tD; + 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]; + double scalingFactor = target->getMaxHP() / 1000.0; + int healed = target->heal(sourceRef, heal * scalingFactor); + + 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) { + // take aggro + target->takeDamage(source->getRef(), 0); + + int duration = 0; + int strength = 0; + bool blocked = target->hasBuff(ECSB_FREEDOM); + if(!blocked) { + duration = skill->durationTime[power]; + strength = skill->values[0][power]; + BuffStack debuff = { + (duration * 100) / MS_PER_COMBAT_TICK, // ticks + strength, // value + source->getRef(), // source + BuffClass::NANO, // buff class + }; + int timeBuffId = Abilities::getCSTBFromST(skill->skillType); + target->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) { + Buffs::timeBuffTick(self, buff); + }, + &debuff); + } + + sSkillResult_Damage_N_Debuff result{}; + result.iDamage = duration / 10; // we use the duration as the damage number (why?) + result.iHP = target->getCurrentHP(); + result.eCT = target->getCharType(); + result.iID = target->getID(); + result.bProtected = blocked; + result.iConditionBitFlag = target->getCompositeCondition(); + return SkillResult(sizeof(sSkillResult_Damage_N_Debuff), &result); +} + +static SkillResult handleSkillLeech(SkillData* skill, int power, ICombatant* source, ICombatant* target) { + EntityRef sourceRef = source->getRef(); + int heal = skill->values[0][power]; + int healed = source->heal(sourceRef, heal); + int damage = heal * 2; + int dealt = target->takeDamage(sourceRef, damage); + + sSkillResult_Leech result{}; + + result.Damage.eCT = target->getCharType(); + result.Damage.iID = target->getID(); + result.Damage.bProtected = dealt <= 0; + result.Damage.iDamage = dealt; + result.Damage.iHP = target->getCurrentHP(); + + result.Heal.eCT = result.Damage.eCT; + result.Heal.iID = result.Damage.iID; + result.Heal.iHealHP = healed; + result.Heal.iHP = source->getCurrentHP(); + + return SkillResult(sizeof(sSkillResult_Leech), &result); +} + +static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) { + int duration = skill->durationTime[power]; + int strength = skill->values[0][power]; + BuffStack passiveBuff = { + skill->drainType == SkillDrainType::PASSIVE ? 1 : (duration * 100) / MS_PER_COMBAT_TICK, // ticks + strength, // value + source->getRef(), // source + source == target ? BuffClass::NANO : BuffClass::GROUP_NANO, // buff class + }; + + int timeBuffId = Abilities::getCSTBFromST(skill->skillType); + SkillDrainType drainType = skill->drainType; + if(!target->addBuff(timeBuffId, + [drainType](EntityRef self, Buff* buff, int status, BuffStack* stack) { + if(buff->id == ECSB_BOUNDINGBALL) { + // drain + ICombatant* combatant = dynamic_cast(self.getEntity()); + combatant->takeDamage(buff->getLastSource(), 0); // aggro + } + if(drainType == SkillDrainType::ACTIVE && status == ETBU_DEL) + Buffs::timeBuffTimeout(self); + }, + [](EntityRef self, Buff* buff, time_t currTime) { + if(buff->id == ECSB_BOUNDINGBALL) + Buffs::tickDrain(self, buff); // drain + }, + &passiveBuff)) return SkillResult(); // no result if already buffed + + 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); + if(source == target) { + // no trailing struct for self + PlayerManager::sendPlayerTo(target->getRef().sock, plr->recallX, plr->recallY, plr->recallZ, plr->recallInstance); + return SkillResult(); + } + + 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) { + SkillResult (*skillHandler)(SkillData*, int, ICombatant*, ICombatant*) = nullptr; + std::vector results; + + switch(skill->skillType) + { + case SkillType::DAMAGE: + skillHandler = handleSkillDamage; + break; + case SkillType::HEAL_HP: + case SkillType::RETURNHOMEHEAL: + skillHandler = handleSkillHealHP; + break; + case SkillType::KNOCKDOWN: + case SkillType::SLEEP: + case SkillType::SNARE: + case SkillType::STUN: + skillHandler = handleSkillDamageNDebuff; + break; + case SkillType::JUMP: + case SkillType::RUN: + case SkillType::STEALTH: + case SkillType::MINIMAPENEMY: + case SkillType::MINIMAPTRESURE: + case SkillType::PHOENIX: + case SkillType::PROTECTBATTERY: + case SkillType::PROTECTINFECTION: + case SkillType::REWARDBLOB: + case SkillType::REWARDCASH: + // case SkillType::INFECTIONDAMAGE: + case SkillType::FREEDOM: + case SkillType::BOUNDINGBALL: + case SkillType::INVULNERABLE: + case SkillType::STAMINA_SELF: + case SkillType::NANOSTIMPAK: + case SkillType::BUFFHEAL: + skillHandler = handleSkillBuff; + break; + case SkillType::BLOODSUCKING: + skillHandler = handleSkillLeech; + break; + case SkillType::RETROROCKET_SELF: + // no-op + return results; + case SkillType::PHOENIX_GROUP: + skillHandler = handleSkillResurrect; + break; + case SkillType::RECALL: + case SkillType::RECALL_GROUP: + skillHandler = handleSkillMove; + break; + case SkillType::BATTERYDRAIN: + skillHandler = handleSkillBatteryDrain; + break; + default: + std::cout << "[WARN] Unhandled skill type " << (int)skill->skillType << std::endl; + return results; + } + + 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 > MAX_SKILLRESULT_SIZE) { + std::cout << "[WARN] bad skill result size for " << (int)skill->skillType << " from " << (void*)handleSkillBuff << std::endl; + continue; } + results.push_back(result); + } + return results; +} - int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_NANO_SKILL_USE)); - tD[0] = pkt->iTargetCnt; +static void attachSkillResults(std::vector results, uint8_t* pivot) { + for(SkillResult& result : results) { + size_t sz = result.size; + memcpy(pivot, result.payload, sz); + pivot += sz; + } +} - for (int i = 0; i < pkt->iTargetCnt; i++) - tD[i+1] = pktdata[i]; +void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std::vector affected) { - } else if (SkillTable[skillID].targetType == 2) { // self target only - tD[0] = 1; - tD[1] = plr->iID; + Player* plr = PlayerManager::getPlayer(sock); + ICombatant* combatant = dynamic_cast(plr); - } else if (SkillTable[skillID].targetType == 3) { // entire group as target - Player *otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup); + int boost = 0; + if (Nanos::getNanoBoost(plr)) + boost = 3; - if (otherPlr == nullptr) - return tD; + if(skill->drainType == SkillDrainType::ACTIVE) { + nano.iStamina -= skill->batteryUse[boost]; + if (nano.iStamina <= 0) + nano.iStamina = 0; + } - if (SkillTable[skillID].effectArea == 0) { // for buffs - tD[0] = otherPlr->groupCnt; - for (int i = 0; i < otherPlr->groupCnt; i++) - tD[i+1] = otherPlr->groupIDs[i]; - return tD; - } + std::vector results = handleSkill(skill, boost, combatant, affected); + if(results.empty()) return; // no effect; no need for confirmation packets - for (int i = 0; i < otherPlr->groupCnt; i++) { // group heals have an area limit - Player *otherPlr2 = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]); - if (otherPlr2 == nullptr) + // lazy validation since skill results might be different sizes + if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), results.size(), MAX_SKILLRESULT_SIZE)) { + 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); + for(SkillResult& sr : results) + resplen += sr.size; + 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 = (int32_t)skill->skillType; + pkt->iTargetCnt = (int32_t)results.size(); + + attachSkillResults(results, (uint8_t*)(pkt + 1)); + sock->sendPacket(pkt, P_FE2CL_NANO_SKILL_USE_SUCC, resplen); + + if(skill->skillType == SkillType::RECALL_GROUP) + // group recall packet is sent only to group members + PlayerManager::sendToGroup(sock, pkt, P_FE2CL_NANO_SKILL_USE, resplen); + else + PlayerManager::sendToViewable(sock, pkt, P_FE2CL_NANO_SKILL_USE, 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); + if(results.empty()) return; // no effect; no need for confirmation packets + + // lazy validation since skill results might be different sizes + if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), results.size(), MAX_SKILLRESULT_SIZE)) { + 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); + for(SkillResult& sr : results) + resplen += sr.size; + 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 = (int32_t)skill->skillType; + pkt->iTargetCnt = (int32_t)results.size(); + if(npc.kind == EntityKind::MOB) { + Mob* mob = dynamic_cast(entity); + pkt->iValue1 = mob->hitX; + pkt->iValue2 = mob->hitY; + pkt->iValue3 = mob->hitZ; + } + + attachSkillResults(results, (uint8_t*)(pkt + 1)); + NPCManager::sendToViewable(entity, pkt, P_FE2CL_NPC_SKILL_HIT, resplen); +} + +static std::vector entityRefsToCombatants(std::vector refs) { + std::vector combatants; + for(EntityRef ref : refs) { + if(ref.kind == EntityKind::PLAYER) + combatants.push_back(dynamic_cast(PlayerManager::getPlayer(ref.sock))); + else if(ref.kind == EntityKind::COMBAT_NPC || ref.kind == EntityKind::MOB) + combatants.push_back(dynamic_cast(ref.getEntity())); + } + return combatants; +} + +std::vector Abilities::matchTargets(ICombatant* src, SkillData* skill, int count, int32_t *ids) { + + if(skill->targetType == SkillTargetType::GROUP) + return entityRefsToCombatants(src->getGroupMembers()); + + // this check *has* to happen after the group check above due to cases like group recall that use both + if(skill->effectTarget == SkillEffectTarget::SELF) + return {src}; // client sends 0 targets for certain self-targeting skills (recall) + + // individuals + std::vector targets; + for (int i = 0; i < count; i++) { + int32_t id = ids[i]; + if (skill->targetType == SkillTargetType::MOBS) { + // mob + if (NPCManager::NPCs.find(id) != NPCManager::NPCs.end()) { + BaseNPC* npc = NPCManager::NPCs[id]; + if (npc->kind == EntityKind::COMBAT_NPC || npc->kind == EntityKind::MOB) { + targets.push_back(dynamic_cast(npc)); + continue; + } + } + std::cout << "[WARN] skill: invalid mob target (id " << id << ")\n"; + } else if(skill->targetType == SkillTargetType::PLAYERS) { + // player + Player* plr = PlayerManager::getPlayerFromID(id); + if (plr != nullptr) { + targets.push_back(dynamic_cast(plr)); continue; - if (true) {//hypot(otherPlr2->x - plr->x, otherPlr2->y - plr->y) < SkillTable[skillID].effectArea) { - tD[i+1] = otherPlr->groupIDs[i]; - tD[0] += 1; } - } - } - - return tD; -} - -void Nanos::nanoUnbuff(CNSocket* sock, std::vector targetData, int32_t bitFlag, int16_t timeBuffID, int16_t amount, bool groupPower) { - Player *plr = PlayerManager::getPlayer(sock); - - plr->iSelfConditionBitFlag &= ~bitFlag; - int groupFlags = 0; - - if (groupPower) { - plr->iGroupConditionBitFlag &= ~bitFlag; - Player *leader = PlayerManager::getPlayerFromID(plr->iIDGroup); - if (leader != nullptr) - groupFlags = Groups::getGroupFlags(leader); - } - - for (int i = 0; i < targetData[0]; i++) { - Player* varPlr = PlayerManager::getPlayerFromID(targetData[i+1]); - if (!((groupFlags | varPlr->iSelfConditionBitFlag) & bitFlag)) { - CNSocket* sockTo = PlayerManager::getSockFromID(targetData[i+1]); - if (sockTo == nullptr) - continue; // sanity check - - INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp); - resp.eCSTB = timeBuffID; // eCharStatusTimeBuffID - resp.eTBU = 2; // eTimeBuffUpdate - resp.eTBT = 1; // eTimeBuffType 1 means nano - varPlr->iConditionBitFlag &= ~bitFlag; - resp.iConditionBitFlag = varPlr->iConditionBitFlag |= groupFlags | varPlr->iSelfConditionBitFlag; - - if (amount > 0) - resp.TimeBuff.iValue = amount; - - sockTo->sendPacket((void*)&resp, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE)); - } - } -} - -int Nanos::applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int32_t groupFlags) { - if (SkillTable[skillID].drainType == 1) - return 0; - - int32_t bitFlag = 0; - - for (auto& pwr : NanoPowers) { - if (pwr.skillType == SkillTable[skillID].skillType) { - bitFlag = pwr.bitFlag; - Player *plr = PlayerManager::getPlayer(sock); - if (eTBU == 1 || !((groupFlags | plr->iSelfConditionBitFlag) & bitFlag)) { - INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp); - resp.eCSTB = pwr.timeBuffID; - resp.eTBU = eTBU; - resp.eTBT = eTBT; - - if (eTBU == 1) - plr->iConditionBitFlag |= bitFlag; - else - plr->iConditionBitFlag &= ~bitFlag; - - resp.iConditionBitFlag = plr->iConditionBitFlag |= groupFlags | plr->iSelfConditionBitFlag; - resp.TimeBuff.iValue = SkillTable[skillID].powerIntensity[0]; - sock->sendPacket((void*)&resp, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE)); - } - return bitFlag; + std::cout << "[WARN] skill: invalid player target (id " << id << ")\n"; } } - return 0; + return targets; } -#pragma region Nano Powers -namespace Nanos { - -bool doDebuff(CNSocket *sock, sSkillResult_Buff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) { - std::cout << "[WARN] doDebuff: NPC ID not found" << std::endl; - return false; +/* ripped from client (enums emplaced) */ +int Abilities::getCSTBFromST(SkillType skillType) { + int result = 0; + switch (skillType) + { + case SkillType::RUN: + result = ECSB_UP_MOVE_SPEED; + break; + case SkillType::JUMP: + result = ECSB_UP_JUMP_HEIGHT; + break; + case SkillType::STEALTH: + result = ECSB_UP_STEALTH; + break; + case SkillType::PHOENIX: + result = ECSB_PHOENIX; + break; + case SkillType::PROTECTBATTERY: + result = ECSB_PROTECT_BATTERY; + break; + case SkillType::PROTECTINFECTION: + result = ECSB_PROTECT_INFECTION; + break; + case SkillType::MINIMAPENEMY: + result = ECSB_MINIMAP_ENEMY; + break; + case SkillType::MINIMAPTRESURE: + result = ECSB_MINIMAP_TRESURE; + break; + case SkillType::REWARDBLOB: + result = ECSB_REWARD_BLOB; + break; + case SkillType::REWARDCASH: + result = ECSB_REWARD_CASH; + break; + case SkillType::FREEDOM: + result = ECSB_FREEDOM; + break; + case SkillType::INVULNERABLE: + result = ECSB_INVULNERABLE; + break; + case SkillType::BUFFHEAL: + result = ECSB_HEAL; + break; + case SkillType::NANOSTIMPAK: + result = ECSB_STIMPAKSLOT1; + // shift as necessary + break; + case SkillType::SNARE: + result = ECSB_DN_MOVE_SPEED; + break; + case SkillType::STUN: + result = ECSB_STUN; + break; + case SkillType::SLEEP: + result = ECSB_MEZ; + break; + case SkillType::INFECTIONDAMAGE: + result = ECSB_INFECTION; + break; + case SkillType::BOUNDINGBALL: + result = ECSB_BOUNDINGBALL; + break; + default: + break; } - - BaseNPC* npc = NPCManager::NPCs[targetID]; - if (npc->type != EntityType::MOB) { - std::cout << "[WARN] doDebuff: NPC is not a mob" << std::endl; - return false; - } - - Mob* mob = (Mob*)npc; - Combat::hitMob(sock, mob, 0); - - respdata[i].eCT = 4; - respdata[i].iID = mob->appearanceData.iNPC_ID; - respdata[i].bProtected = 1; - if (mob->skillStyle < 0 && mob->state != MobState::RETREAT - && !(mob->appearanceData.iConditionBitFlag & CSB_BIT_FREEDOM)) { // only debuff if the enemy is not retreating, casting corruption or in freedom - mob->appearanceData.iConditionBitFlag |= bitFlag; - mob->unbuffTimes[bitFlag] = getTime() + duration * 100; - respdata[i].bProtected = 0; - } - respdata[i].iConditionBitFlag = mob->appearanceData.iConditionBitFlag; - - return true; + return result; } - -bool doBuff(CNSocket *sock, sSkillResult_Buff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - Player *plr = nullptr; - CNSocket *sockTo = nullptr; - - for (auto& pair : PlayerManager::players) { - if (pair.second->iID == targetID) { - sockTo = pair.first; - plr = pair.second; - break; - } - } - - // player not found - if (sockTo == nullptr || plr == nullptr) { - std::cout << "[WARN] doBuff: player ID not found" << std::endl; - return false; - } - - respdata[i].eCT = 1; - respdata[i].iID = plr->iID; - respdata[i].iConditionBitFlag = 0; - - // only apply buffs if the player is actually alive - if (plr->HP > 0) { - respdata[i].iConditionBitFlag = bitFlag; - - INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt); - pkt.eCSTB = timeBuffID; // eCharStatusTimeBuffID - pkt.eTBU = 1; // eTimeBuffUpdate - pkt.eTBT = 1; // eTimeBuffType 1 means nano - pkt.iConditionBitFlag = plr->iConditionBitFlag |= bitFlag; - - if (amount > 0) - pkt.TimeBuff.iValue = amount; - - sockTo->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE)); - } - - return true; -} - -bool doDamageNDebuff(CNSocket *sock, sSkillResult_Damage_N_Debuff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) { - // not sure how to best handle this - std::cout << "[WARN] doDamageNDebuff: NPC ID not found" << std::endl; - return false; - } - - BaseNPC* npc = NPCManager::NPCs[targetID]; - if (npc->type != EntityType::MOB) { - std::cout << "[WARN] doDamageNDebuff: NPC is not a mob" << std::endl; - return false; - } - - Mob* mob = (Mob*)npc; - - Combat::hitMob(sock, mob, 0); // just to gain aggro - - respdata[i].eCT = 4; - respdata[i].iDamage = duration / 10; - respdata[i].iID = mob->appearanceData.iNPC_ID; - respdata[i].iHP = mob->appearanceData.iHP; - respdata[i].bProtected = 1; - if (mob->skillStyle < 0 && mob->state != MobState::RETREAT - && !(mob->appearanceData.iConditionBitFlag & CSB_BIT_FREEDOM)) { // only debuff if the enemy is not retreating, casting corruption or in freedom - mob->appearanceData.iConditionBitFlag |= bitFlag; - mob->unbuffTimes[bitFlag] = getTime() + duration * 100; - respdata[i].bProtected = 0; - } - respdata[i].iConditionBitFlag = mob->appearanceData.iConditionBitFlag; - - return true; -} - -bool doHeal(CNSocket *sock, sSkillResult_Heal_HP *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - Player *plr = nullptr; - - for (auto& pair : PlayerManager::players) { - if (pair.second->iID == targetID) { - plr = pair.second; - break; - } - } - - // player not found - if (plr == nullptr) { - std::cout << "[WARN] doHeal: player ID not found" << std::endl; - return false; - } - - int healedAmount = PC_MAXHEALTH(plr->level) * amount / 1000; - - // do not heal dead players - if (plr->HP <= 0) - healedAmount = 0; - - plr->HP += healedAmount; - - if (plr->HP > PC_MAXHEALTH(plr->level)) - plr->HP = PC_MAXHEALTH(plr->level); - - respdata[i].eCT = 1; - respdata[i].iID = plr->iID; - respdata[i].iHP = plr->HP; - respdata[i].iHealHP = healedAmount; - - return true; -} - -bool doDamage(CNSocket *sock, sSkillResult_Damage *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) { - // not sure how to best handle this - std::cout << "[WARN] doDamage: NPC ID not found" << std::endl; - return false; - } - - BaseNPC* npc = NPCManager::NPCs[targetID]; - if (npc->type != EntityType::MOB) { - std::cout << "[WARN] doDamage: NPC is not a mob" << std::endl; - return false; - } - - Mob* mob = (Mob*)npc; - Player *plr = PlayerManager::getPlayer(sock); - - int damage = Combat::hitMob(sock, mob, std::max(PC_MAXHEALTH(plr->level) * amount / 1000, mob->maxHealth * amount / 1000)); - - respdata[i].eCT = 4; - respdata[i].iDamage = damage; - respdata[i].iID = mob->appearanceData.iNPC_ID; - respdata[i].iHP = mob->appearanceData.iHP; - - return true; -} - -/* - * NOTE: Leech is specially encoded. - * - * It manages to fit inside the nanoPower<>() mold with only a slight hack, - * but it really is it's own thing. There is a hard assumption that players - * will only every leech a single mob, and the sanity check that enforces that - * assumption is critical. - */ - -bool doLeech(CNSocket *sock, sSkillResult_Heal_HP *healdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - // this sanity check is VERY important - if (i != 0) { - std::cout << "[WARN] Player attempted to leech more than one mob!" << std::endl; - return false; - } - - sSkillResult_Damage *damagedata = (sSkillResult_Damage*)(((uint8_t*)healdata) + sizeof(sSkillResult_Heal_HP)); - Player *plr = PlayerManager::getPlayer(sock); - - int healedAmount = amount; - - plr->HP += healedAmount; - - if (plr->HP > PC_MAXHEALTH(plr->level)) - plr->HP = PC_MAXHEALTH(plr->level); - - healdata->eCT = 1; - healdata->iID = plr->iID; - healdata->iHP = plr->HP; - healdata->iHealHP = healedAmount; - - if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) { - // not sure how to best handle this - std::cout << "[WARN] doLeech: NPC ID not found" << std::endl; - return false; - } - - BaseNPC* npc = NPCManager::NPCs[targetID]; - if (npc->type != EntityType::MOB) { - std::cout << "[WARN] doLeech: NPC is not a mob" << std::endl; - return false; - } - - Mob* mob = (Mob*)npc; - - int damage = Combat::hitMob(sock, mob, amount * 2); - - damagedata->eCT = 4; - damagedata->iDamage = damage; - damagedata->iID = mob->appearanceData.iNPC_ID; - damagedata->iHP = mob->appearanceData.iHP; - - return true; -} - -bool doResurrect(CNSocket *sock, sSkillResult_Resurrect *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - Player *plr = nullptr; - - for (auto& pair : PlayerManager::players) { - if (pair.second->iID == targetID) { - plr = pair.second; - break; - } - } - - // player not found - if (plr == nullptr) { - std::cout << "[WARN] doResurrect: player ID not found" << std::endl; - return false; - } - - respdata[i].eCT = 1; - respdata[i].iID = plr->iID; - respdata[i].iRegenHP = plr->HP; - - return true; -} - -bool doMove(CNSocket *sock, sSkillResult_Move *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - Player *plr = nullptr; - - for (auto& pair : PlayerManager::players) { - if (pair.second->iID == targetID) { - plr = pair.second; - break; - } - } - - // player not found - if (plr == nullptr) { - std::cout << "[WARN] doMove: player ID not found" << std::endl; - return false; - } - - Player *plr2 = PlayerManager::getPlayer(sock); - - respdata[i].eCT = 1; - respdata[i].iID = plr->iID; - respdata[i].iMapNum = plr2->recallInstance; - respdata[i].iMoveX = plr2->recallX; - respdata[i].iMoveY = plr2->recallY; - respdata[i].iMoveZ = plr2->recallZ; - - return true; -} - -template -void nanoPower(CNSocket *sock, std::vector targetData, - int16_t nanoID, int16_t skillID, int16_t duration, int16_t amount, - int16_t skillType, int32_t bitFlag, int16_t timeBuffID) { - Player *plr = PlayerManager::getPlayer(sock); - - if (skillType == EST_RETROROCKET_SELF || skillType == EST_RECALL) // rocket and self recall does not need any trailing structs - targetData[0] = 0; - - size_t resplen; - // special case since leech is atypically encoded - if (skillType == EST_BLOODSUCKING) - resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) + sizeof(sSkillResult_Heal_HP) + sizeof(sSkillResult_Damage); - else - resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) + targetData[0] * sizeof(sPAYLOAD); - - // validate response packet - if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), targetData[0], sizeof(sPAYLOAD))) { - std::cout << "[WARN] bad sP_FE2CL_NANO_SKILL_USE packet size" << std::endl; - return; - } - - uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; - memset(respbuf, 0, resplen); - - sP_FE2CL_NANO_SKILL_USE_SUCC *resp = (sP_FE2CL_NANO_SKILL_USE_SUCC*)respbuf; - sPAYLOAD *respdata = (sPAYLOAD*)(respbuf+sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC)); - - resp->iPC_ID = plr->iID; - resp->iSkillID = skillID; - resp->iNanoID = nanoID; - resp->iNanoStamina = plr->Nanos[plr->activeNano].iStamina; - resp->eST = skillType; - resp->iTargetCnt = targetData[0]; - - if (SkillTable[skillID].drainType == 2) { - if (SkillTable[skillID].targetType >= 2) - plr->iSelfConditionBitFlag |= bitFlag; - if (SkillTable[skillID].targetType == 3) - plr->iGroupConditionBitFlag |= bitFlag; - } - - for (int i = 0; i < targetData[0]; i++) - if (!work(sock, respdata, i, targetData[i+1], bitFlag, timeBuffID, duration, amount)) - return; - - sock->sendPacket((void*)&respbuf, P_FE2CL_NANO_SKILL_USE_SUCC, resplen); - assert(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) == sizeof(sP_FE2CL_NANO_SKILL_USE)); - if (skillType == EST_RECALL_GROUP) { // in the case of group recall, nobody but group members need the packet - for (int i = 0; i < targetData[0]; i++) { - CNSocket *sock2 = PlayerManager::getSockFromID(targetData[i+1]); - sock2->sendPacket((void*)&respbuf, P_FE2CL_NANO_SKILL_USE, resplen); - } - } else - PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_NANO_SKILL_USE, resplen); - - // Warping on recall - if (skillType == EST_RECALL || skillType == EST_RECALL_GROUP) { - if ((int32_t)plr->instanceID == plr->recallInstance) - PlayerManager::sendPlayerTo(sock, plr->recallX, plr->recallY, plr->recallZ, plr->recallInstance); - else { - INITSTRUCT(sP_FE2CL_REP_WARP_USE_RECALL_FAIL, response) - sock->sendPacket((void*)&response, P_FE2CL_REP_WARP_USE_RECALL_FAIL, sizeof(sP_FE2CL_REP_WARP_USE_RECALL_FAIL)); - } - } -} - -// nano power dispatch table -std::vector NanoPowers = { - NanoPower(EST_STUN, CSB_BIT_STUN, ECSB_STUN, nanoPower), - NanoPower(EST_HEAL_HP, CSB_BIT_NONE, ECSB_NONE, nanoPower), - NanoPower(EST_BOUNDINGBALL, CSB_BIT_BOUNDINGBALL, ECSB_BOUNDINGBALL, nanoPower), - NanoPower(EST_SNARE, CSB_BIT_DN_MOVE_SPEED, ECSB_DN_MOVE_SPEED, nanoPower), - NanoPower(EST_DAMAGE, CSB_BIT_NONE, ECSB_NONE, nanoPower), - NanoPower(EST_BLOODSUCKING, CSB_BIT_NONE, ECSB_NONE, nanoPower), - NanoPower(EST_SLEEP, CSB_BIT_MEZ, ECSB_MEZ, nanoPower), - NanoPower(EST_REWARDBLOB, CSB_BIT_REWARD_BLOB, ECSB_REWARD_BLOB, nanoPower), - NanoPower(EST_RUN, CSB_BIT_UP_MOVE_SPEED, ECSB_UP_MOVE_SPEED, nanoPower), - NanoPower(EST_REWARDCASH, CSB_BIT_REWARD_CASH, ECSB_REWARD_CASH, nanoPower), - NanoPower(EST_PROTECTBATTERY, CSB_BIT_PROTECT_BATTERY, ECSB_PROTECT_BATTERY, nanoPower), - NanoPower(EST_MINIMAPENEMY, CSB_BIT_MINIMAP_ENEMY, ECSB_MINIMAP_ENEMY, nanoPower), - NanoPower(EST_PROTECTINFECTION, CSB_BIT_PROTECT_INFECTION, ECSB_PROTECT_INFECTION, nanoPower), - NanoPower(EST_JUMP, CSB_BIT_UP_JUMP_HEIGHT, ECSB_UP_JUMP_HEIGHT, nanoPower), - NanoPower(EST_FREEDOM, CSB_BIT_FREEDOM, ECSB_FREEDOM, nanoPower), - NanoPower(EST_PHOENIX, CSB_BIT_PHOENIX, ECSB_PHOENIX, nanoPower), - NanoPower(EST_STEALTH, CSB_BIT_UP_STEALTH, ECSB_UP_STEALTH, nanoPower), - NanoPower(EST_MINIMAPTRESURE, CSB_BIT_MINIMAP_TRESURE, ECSB_MINIMAP_TRESURE, nanoPower), - NanoPower(EST_RECALL, CSB_BIT_NONE, ECSB_NONE, nanoPower), - NanoPower(EST_RECALL_GROUP, CSB_BIT_NONE, ECSB_NONE, nanoPower), - NanoPower(EST_RETROROCKET_SELF, CSB_BIT_NONE, ECSB_NONE, nanoPower), - NanoPower(EST_PHOENIX_GROUP, CSB_BIT_NONE, ECSB_NONE, nanoPower), - NanoPower(EST_NANOSTIMPAK, CSB_BIT_STIMPAKSLOT1, ECSB_STIMPAKSLOT1, nanoPower), - NanoPower(EST_NANOSTIMPAK, CSB_BIT_STIMPAKSLOT2, ECSB_STIMPAKSLOT2, nanoPower), - NanoPower(EST_NANOSTIMPAK, CSB_BIT_STIMPAKSLOT3, ECSB_STIMPAKSLOT3, nanoPower) -}; - -}; // namespace -#pragma endregion - -#pragma region Mob Powers -namespace Combat { -bool doDamageNDebuff(Mob *mob, sSkillResult_Damage_N_Debuff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - CNSocket *sock = nullptr; - Player *plr = nullptr; - - for (auto& pair : PlayerManager::players) { - if (pair.second->iID == targetID) { - sock = pair.first; - plr = pair.second; - break; - } - } - - // player not found - if (plr == nullptr) { - std::cout << "[WARN] doDamageNDebuff: player ID not found" << std::endl; - return false; - } - - respdata[i].eCT = 1; - respdata[i].iDamage = duration / 10; - respdata[i].iID = plr->iID; - respdata[i].iHP = plr->HP; - respdata[i].iStamina = plr->Nanos[plr->activeNano].iStamina; - if (plr->iConditionBitFlag & CSB_BIT_FREEDOM) - respdata[i].bProtected = 1; - else { - if (!(plr->iConditionBitFlag & bitFlag)) { - INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt); - pkt.eCSTB = timeBuffID; // eCharStatusTimeBuffID - pkt.eTBU = 1; // eTimeBuffUpdate - pkt.eTBT = 2; - pkt.iConditionBitFlag = plr->iConditionBitFlag |= bitFlag; - pkt.TimeBuff.iValue = amount * 5; - sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE)); - } - - respdata[i].bProtected = 0; - std::pair key = std::make_pair(sock, bitFlag); - time_t until = getTime() + (time_t)duration * 100; - Eggs::EggBuffs[key] = until; - } - respdata[i].iConditionBitFlag = plr->iConditionBitFlag; - - if (plr->HP <= 0) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!MobAI::aggroCheck(mob, getTime())) { - MobAI::clearDebuff(mob); - if (mob->groupLeader != 0) - MobAI::groupRetreat(mob); - } - } - - return true; -} - -bool doHeal(Mob *mob, sSkillResult_Heal_HP *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) { - std::cout << "[WARN] doHeal: NPC ID not found" << std::endl; - return false; - } - - BaseNPC* npc = NPCManager::NPCs[targetID]; - if (npc->type != EntityType::MOB) { - std::cout << "[WARN] doHeal: NPC is not a mob" << std::endl; - return false; - } - - Mob* targetMob = (Mob*)npc; - - int healedAmount = amount * targetMob->maxHealth / 1000; - targetMob->appearanceData.iHP += healedAmount; - if (targetMob->appearanceData.iHP > targetMob->maxHealth) - targetMob->appearanceData.iHP = targetMob->maxHealth; - - respdata[i].eCT = 4; - respdata[i].iID = targetMob->appearanceData.iNPC_ID; - respdata[i].iHP = targetMob->appearanceData.iHP; - respdata[i].iHealHP = healedAmount; - - return true; -} - -bool doReturnHeal(Mob *mob, sSkillResult_Heal_HP *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - int healedAmount = amount * mob->maxHealth / 1000; - mob->appearanceData.iHP += healedAmount; - if (mob->appearanceData.iHP > mob->maxHealth) - mob->appearanceData.iHP = mob->maxHealth; - - respdata[i].eCT = 4; - respdata[i].iID = mob->appearanceData.iNPC_ID; - respdata[i].iHP = mob->appearanceData.iHP; - respdata[i].iHealHP = healedAmount; - - return true; -} - -bool doDamage(Mob *mob, sSkillResult_Damage *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - Player *plr = nullptr; - - for (auto& pair : PlayerManager::players) { - if (pair.second->iID == targetID) { - plr = pair.second; - break; - } - } - - // player not found - if (plr == nullptr) { - std::cout << "[WARN] doDamage: player ID not found" << std::endl; - return false; - } - - int damage = amount * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500; - - if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE) - damage = 0; - - respdata[i].eCT = 1; - respdata[i].iDamage = damage; - respdata[i].iID = plr->iID; - respdata[i].iHP = plr->HP -= damage; - - if (plr->HP <= 0) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!MobAI::aggroCheck(mob, getTime())) { - MobAI::clearDebuff(mob); - if (mob->groupLeader != 0) - MobAI::groupRetreat(mob); - } - } - - return true; -} - -bool doLeech(Mob *mob, sSkillResult_Heal_HP *healdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - // this sanity check is VERY important - if (i != 0) { - std::cout << "[WARN] Mob attempted to leech more than one player!" << std::endl; - return false; - } - - Player *plr = nullptr; - - for (auto& pair : PlayerManager::players) { - if (pair.second->iID == targetID) { - plr = pair.second; - break; - } - } - - // player not found - if (plr == nullptr) { - std::cout << "[WARN] doLeech: player ID not found" << std::endl; - return false; - } - - sSkillResult_Damage *damagedata = (sSkillResult_Damage*)(((uint8_t*)healdata) + sizeof(sSkillResult_Heal_HP)); - - int healedAmount = amount * PC_MAXHEALTH(plr->level) / 1000; - - mob->appearanceData.iHP += healedAmount; - if (mob->appearanceData.iHP > mob->maxHealth) - mob->appearanceData.iHP = mob->maxHealth; - - healdata->eCT = 4; - healdata->iID = mob->appearanceData.iNPC_ID; - healdata->iHP = mob->appearanceData.iHP; - healdata->iHealHP = healedAmount; - - int damage = healedAmount; - - if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE) - damage = 0; - - damagedata->eCT = 1; - damagedata->iDamage = damage; - damagedata->iID = plr->iID; - damagedata->iHP = plr->HP -= damage; - - if (plr->HP <= 0) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!MobAI::aggroCheck(mob, getTime())) { - MobAI::clearDebuff(mob); - if (mob->groupLeader != 0) - MobAI::groupRetreat(mob); - } - } - - return true; -} - -bool doBatteryDrain(Mob *mob, sSkillResult_BatteryDrain *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - Player *plr = nullptr; - - for (auto& pair : PlayerManager::players) { - if (pair.second->iID == targetID) { - plr = pair.second; - break; - } - } - - // player not found - if (plr == nullptr) { - std::cout << "[WARN] doBatteryDrain: player ID not found" << std::endl; - return false; - } - - respdata[i].eCT = 1; - respdata[i].iID = plr->iID; - - if (plr->iConditionBitFlag & CSB_BIT_PROTECT_BATTERY) { - respdata[i].bProtected = 1; - respdata[i].iDrainW = 0; - respdata[i].iDrainN = 0; - } else { - respdata[i].bProtected = 0; - respdata[i].iDrainW = amount * (18 + (int)mob->data["m_iNpcLevel"]) / 36; - respdata[i].iDrainN = amount * (18 + (int)mob->data["m_iNpcLevel"]) / 36; - } - - respdata[i].iBatteryW = plr->batteryW -= (respdata[i].iDrainW < plr->batteryW) ? respdata[i].iDrainW : plr->batteryW; - respdata[i].iBatteryN = plr->batteryN -= (respdata[i].iDrainN < plr->batteryN) ? respdata[i].iDrainN : plr->batteryN; - respdata[i].iStamina = plr->Nanos[plr->activeNano].iStamina; - respdata[i].iConditionBitFlag = plr->iConditionBitFlag; - - return true; -} - -bool doBuff(Mob *mob, sSkillResult_Buff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - respdata[i].eCT = 4; - respdata[i].iID = mob->appearanceData.iNPC_ID; - mob->appearanceData.iConditionBitFlag |= bitFlag; - respdata[i].iConditionBitFlag = mob->appearanceData.iConditionBitFlag; - - return true; -} - -template -void mobPower(Mob *mob, std::vector targetData, - int16_t skillID, int16_t duration, int16_t amount, - int16_t skillType, int32_t bitFlag, int16_t timeBuffID) { - size_t resplen; - // special case since leech is atypically encoded - if (skillType == EST_BLOODSUCKING) - resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Heal_HP) + sizeof(sSkillResult_Damage); - else - resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + targetData[0] * sizeof(sPAYLOAD); - - // validate response packet - if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), targetData[0], sizeof(sPAYLOAD))) { - std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_HIT packet size" << std::endl; - return; - } - - uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; - memset(respbuf, 0, resplen); - - sP_FE2CL_NPC_SKILL_HIT *resp = (sP_FE2CL_NPC_SKILL_HIT*)respbuf; - sPAYLOAD *respdata = (sPAYLOAD*)(respbuf+sizeof(sP_FE2CL_NPC_SKILL_HIT)); - - resp->iNPC_ID = mob->appearanceData.iNPC_ID; - resp->iSkillID = skillID; - resp->iValue1 = mob->hitX; - resp->iValue2 = mob->hitY; - resp->iValue3 = mob->hitZ; - resp->eST = skillType; - resp->iTargetCnt = targetData[0]; - - for (int i = 0; i < targetData[0]; i++) - if (!work(mob, respdata, i, targetData[i+1], bitFlag, timeBuffID, duration, amount)) - return; - - NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen); -} - -// nano power dispatch table -std::vector MobPowers = { - MobPower(EST_STUN, CSB_BIT_STUN, ECSB_STUN, mobPower), - MobPower(EST_HEAL_HP, CSB_BIT_NONE, ECSB_NONE, mobPower), - MobPower(EST_RETURNHOMEHEAL, CSB_BIT_NONE, ECSB_NONE, mobPower), - MobPower(EST_SNARE, CSB_BIT_DN_MOVE_SPEED, ECSB_DN_MOVE_SPEED, mobPower), - MobPower(EST_DAMAGE, CSB_BIT_NONE, ECSB_NONE, mobPower), - MobPower(EST_BATTERYDRAIN, CSB_BIT_NONE, ECSB_NONE, mobPower), - MobPower(EST_SLEEP, CSB_BIT_MEZ, ECSB_MEZ, mobPower), - MobPower(EST_BLOODSUCKING, CSB_BIT_NONE, ECSB_NONE, mobPower), - MobPower(EST_FREEDOM, CSB_BIT_FREEDOM, ECSB_FREEDOM, mobPower) -}; - -}; // namespace -#pragma endregion diff --git a/src/Abilities.hpp b/src/Abilities.hpp index fc3ea76..ef703b1 100644 --- a/src/Abilities.hpp +++ b/src/Abilities.hpp @@ -1,64 +1,111 @@ #pragma once #include "core/Core.hpp" -#include "Combat.hpp" -typedef void (*PowerHandler)(CNSocket*, std::vector, int16_t, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t); +#include "Entities.hpp" +#include "Player.hpp" -struct NanoPower { - int16_t skillType; - int32_t bitFlag; - int16_t timeBuffID; - PowerHandler handler; +#include +#include +#include - NanoPower(int16_t s, int32_t b, int16_t t, PowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {} +constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain); - void handle(CNSocket *sock, std::vector targetData, int16_t nanoID, int16_t skillID, int16_t duration, int16_t amount) { - if (handler == nullptr) - return; - - handler(sock, targetData, nanoID, skillID, duration, amount, skillType, bitFlag, timeBuffID); - } +enum class SkillType { + DAMAGE = 1, + HEAL_HP = 2, + KNOCKDOWN = 3, // dnd + SLEEP = 4, // dnd + SNARE = 5, // dnd + HEAL_STAMINA = 6, + STAMINA_SELF = 7, + STUN = 8, // dnd + WEAPONSLOW = 9, + JUMP = 10, + RUN = 11, + STEALTH = 12, + SWIM = 13, + MINIMAPENEMY = 14, + MINIMAPTRESURE = 15, + PHOENIX = 16, + PROTECTBATTERY = 17, + PROTECTINFECTION = 18, + REWARDBLOB = 19, + REWARDCASH = 20, + BATTERYDRAIN = 21, + CORRUPTIONATTACK = 22, + INFECTIONDAMAGE = 23, + KNOCKBACK = 24, + FREEDOM = 25, + PHOENIX_GROUP = 26, + RECALL = 27, + RECALL_GROUP = 28, + RETROROCKET_SELF = 29, + BLOODSUCKING = 30, + BOUNDINGBALL = 31, + INVULNERABLE = 32, + NANOSTIMPAK = 33, + RETURNHOMEHEAL = 34, + BUFFHEAL = 35, + EXTRABANK = 36, + CORRUPTIONATTACKWIN = 38, + CORRUPTIONATTACKLOSE = 39, }; -typedef void (*MobPowerHandler)(Mob*, std::vector, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t); +enum class SkillEffectTarget { + POINT = 1, + SELF = 2, + CONE = 3, + WEAPON = 4, + AREA_SELF = 5, + AREA_TARGET = 6 +}; -struct MobPower { - int16_t skillType; - int32_t bitFlag; - int16_t timeBuffID; - MobPowerHandler handler; +enum class SkillTargetType { + MOBS = 1, + PLAYERS = 2, + GROUP = 3 +}; - MobPower(int16_t s, int32_t b, int16_t t, MobPowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {} +enum class SkillDrainType { + ACTIVE = 1, + PASSIVE = 2 +}; - void handle(Mob *mob, std::vector targetData, int16_t skillID, int16_t duration, int16_t amount) { - if (handler == nullptr) - return; - - handler(mob, targetData, skillID, duration, amount, skillType, bitFlag, timeBuffID); +struct SkillResult { + size_t size; + uint8_t payload[MAX_SKILLRESULT_SIZE]; + SkillResult(size_t len, void* dat) { + assert(len <= MAX_SKILLRESULT_SIZE); + size = len; + memcpy(payload, dat, len); + } + SkillResult() { + size = 0; } }; struct SkillData { - int skillType; - int targetType; - int drainType; + SkillType skillType; // eST + SkillEffectTarget effectTarget; + int effectType; // always 1? + SkillTargetType targetType; + SkillDrainType drainType; int effectArea; + int batteryUse[4]; int durationTime[4]; - int powerIntensity[4]; + + int valueTypes[3]; + int values[3][4]; }; -namespace Nanos { - extern std::vector NanoPowers; +namespace Abilities { extern std::map SkillTable; - void nanoUnbuff(CNSocket* sock, std::vector targetData, int32_t bitFlag, int16_t timeBuffID, int16_t amount, bool groupPower); - int applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int32_t groupFlags); + void useNanoSkill(CNSocket*, SkillData*, sNano&, std::vector); + void useNPCSkill(EntityRef, int skillID, std::vector); - std::vector findTargets(Player* plr, int skillID, CNPacketData* data = nullptr); -} - -namespace Combat { - extern std::vector MobPowers; + std::vector matchTargets(ICombatant*, SkillData*, int, int32_t*); + int getCSTBFromST(SkillType skillType); } diff --git a/src/Buddies.cpp b/src/Buddies.cpp index 25f19cc..e1e6a02 100644 --- a/src/Buddies.cpp +++ b/src/Buddies.cpp @@ -1,15 +1,10 @@ -#include "servers/CNShardServer.hpp" #include "Buddies.hpp" -#include "PlayerManager.hpp" -#include "Buddies.hpp" -#include "db/Database.hpp" -#include "Items.hpp" -#include "db/Database.hpp" -#include -#include -#include -#include +#include "db/Database.hpp" +#include "servers/CNShardServer.hpp" + +#include "Player.hpp" +#include "PlayerManager.hpp" using namespace Buddies; diff --git a/src/Buddies.hpp b/src/Buddies.hpp index e7f9d2e..f0bc455 100644 --- a/src/Buddies.hpp +++ b/src/Buddies.hpp @@ -1,7 +1,5 @@ #pragma once -#include "Player.hpp" -#include "core/Core.hpp" #include "core/Core.hpp" namespace Buddies { diff --git a/src/Buffs.cpp b/src/Buffs.cpp new file mode 100644 index 0000000..d407fac --- /dev/null +++ b/src/Buffs.cpp @@ -0,0 +1,197 @@ +#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; +} + +EntityRef Buff::getLastSource() { + if(stacks.empty()) + return self; + return stacks.back().source; +} + +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::timeBuffTick(EntityRef self, Buff* buff) { + if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB) + return; // not implemented + Entity* entity = self.getEntity(); + ICombatant* combatant = dynamic_cast(entity); + + INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK, pkt); + pkt.eCT = combatant->getCharType(); + pkt.iID = combatant->getID(); + pkt.iTB_ID = buff->id; + NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK)); +} + +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 + int32_t eCharType = combatant->getCharType(); + pkt.eCT = eCharType == 4 ? 2 : eCharType; // convention not followed by client here + 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)); +} + +void Buffs::tickDrain(EntityRef self, Buff* buff) { + if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB) + return; // not implemented + Entity* entity = self.getEntity(); + ICombatant* combatant = dynamic_cast(entity); + int damage = combatant->takeDamage(buff->getLastSource(), combatant->getMaxHP() / 100); + + size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage); + assert(resplen < CN_PACKET_BUFFER_SIZE - 8); + uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; + memset(respbuf, 0, resplen); + + sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf; + pkt->iID = self.id; + pkt->eCT = combatant->getCharType(); + pkt->iTB_ID = ECSB_BOUNDINGBALL; + + sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK)); + drain->iDamage = damage; + drain->iHP = combatant->getCurrentHP(); + drain->eCT = pkt->eCT; + drain->iID = pkt->iID; + + NPCManager::sendToViewable(self.getEntity(), (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen); +} +#pragma endregion diff --git a/src/Buffs.hpp b/src/Buffs.hpp new file mode 100644 index 0000000..8f395e1 --- /dev/null +++ b/src/Buffs.hpp @@ -0,0 +1,93 @@ +#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); + EntityRef getLastSource(); + + /* + * 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 timeBuffTick(EntityRef self, Buff* buff); + void timeBuffTimeout(EntityRef self); + void tickDrain(EntityRef self, Buff* buff); +} diff --git a/src/BuiltinCommands.cpp b/src/BuiltinCommands.cpp index 6f60430..e36b2a4 100644 --- a/src/BuiltinCommands.cpp +++ b/src/BuiltinCommands.cpp @@ -1,10 +1,13 @@ #include "BuiltinCommands.hpp" + +#include "servers/CNShardServer.hpp" + +#include "Player.hpp" #include "PlayerManager.hpp" -#include "Chat.hpp" #include "Items.hpp" #include "Missions.hpp" +#include "Chat.hpp" #include "Nanos.hpp" -#include "Rand.hpp" // helper function, not a packet handler void BuiltinCommands::setSpecialState(CNSocket* sock, CNPacketData* data) { @@ -247,17 +250,17 @@ static void teleportPlayer(CNSocket *sock, CNPacketData *data) { uint64_t instance = plr->instanceID; const int unstickRange = 400; - switch (req->eTeleportType) { - case eCN_GM_TeleportMapType__MyLocation: + switch ((eCN_GM_TeleportType)req->eTeleportType) { + case eCN_GM_TeleportType::MyLocation: PlayerManager::sendPlayerTo(targetSock, plr->x, plr->y, plr->z, instance); break; - case eCN_GM_TeleportMapType__MapXYZ: + case eCN_GM_TeleportType::MapXYZ: instance = req->iToMap; // fallthrough - case eCN_GM_TeleportMapType__XYZ: + case eCN_GM_TeleportType::XYZ: PlayerManager::sendPlayerTo(targetSock, req->iToX, req->iToY, req->iToZ, instance); break; - case eCN_GM_TeleportMapType__SomeoneLocation: + case eCN_GM_TeleportType::SomeoneLocation: // player to teleport to goalSock = PlayerManager::getSockFromAny(req->eGoalPCSearchBy, req->iGoalPC_ID, req->iGoalPC_UID, AUTOU16TOU8(req->szGoalPC_FirstName), AUTOU16TOU8(req->szGoalPC_LastName)); @@ -269,7 +272,7 @@ static void teleportPlayer(CNSocket *sock, CNPacketData *data) { PlayerManager::sendPlayerTo(targetSock, goalPlr->x, goalPlr->y, goalPlr->z, goalPlr->instanceID); break; - case eCN_GM_TeleportMapType__Unstick: + case eCN_GM_TeleportType::Unstick: targetPlr = PlayerManager::getPlayer(targetSock); PlayerManager::sendPlayerTo(targetSock, targetPlr->x - unstickRange/2 + Rand::rand(unstickRange), diff --git a/src/Chat.cpp b/src/Chat.cpp index 61b9803..51d4222 100644 --- a/src/Chat.cpp +++ b/src/Chat.cpp @@ -1,6 +1,9 @@ #include "Chat.hpp" + +#include "servers/CNShardServer.hpp" + +#include "Player.hpp" #include "PlayerManager.hpp" -#include "Groups.hpp" #include "CustomCommands.hpp" #include @@ -225,10 +228,6 @@ static void tradeChatHandler(CNSocket* sock, CNPacketData* data) { static void groupChatHandler(CNSocket* sock, CNPacketData* data) { sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE*)data->buf; Player* plr = PlayerManager::getPlayer(sock); - Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup); - - if (otherPlr == nullptr) - return; std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat)); @@ -251,16 +250,15 @@ static void groupChatHandler(CNSocket* sock, CNPacketData* data) { resp.iSendPCID = plr->iID; resp.iEmoteCode = chat->iEmoteCode; - Groups::sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC)); + if (plr->group == nullptr) + sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC)); + else + Groups::sendToGroup(plr->group, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC)); } static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) { sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE*)data->buf; Player* plr = PlayerManager::getPlayer(sock); - Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup); - - if (otherPlr == nullptr) - return; std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat)); std::string logLine = "[GroupMenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat; @@ -275,7 +273,9 @@ static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) { resp.iSendPCID = plr->iID; resp.iEmoteCode = chat->iEmoteCode; - Groups::sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC)); + if (plr->group == nullptr) + sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC)); + Groups::sendToGroup(plr->group, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC)); } // we only allow plain ascii, at least for now diff --git a/src/Chat.hpp b/src/Chat.hpp index acaca06..26b4236 100644 --- a/src/Chat.hpp +++ b/src/Chat.hpp @@ -2,7 +2,10 @@ #define CMD_PREFIX '/' -#include "servers/CNShardServer.hpp" +#include "core/Core.hpp" + +#include +#include namespace Chat { extern std::vector dump; diff --git a/src/Chunking.cpp b/src/Chunking.cpp index 3d3330a..65231fe 100644 --- a/src/Chunking.cpp +++ b/src/Chunking.cpp @@ -1,9 +1,9 @@ #include "Chunking.hpp" -#include "PlayerManager.hpp" + +#include "MobAI.hpp" #include "NPCManager.hpp" -#include "settings.hpp" -#include "Combat.hpp" -#include "Eggs.hpp" + +#include using namespace Chunking; @@ -26,7 +26,7 @@ static void newChunk(ChunkPos pos) { // add the chunk to the cache of all players and NPCs in the surrounding chunks std::set surroundings = getViewableChunks(pos); for (Chunk* c : surroundings) - for (const EntityRef& ref : c->entities) + for (const EntityRef ref : c->entities) ref.getEntity()->viewableChunks.insert(chunk); } @@ -41,24 +41,24 @@ static void deleteChunk(ChunkPos pos) { // remove the chunk from the cache of all players and NPCs in the surrounding chunks std::set surroundings = getViewableChunks(pos); for(Chunk* c : surroundings) - for (const EntityRef& ref : c->entities) + for (const EntityRef ref : c->entities) ref.getEntity()->viewableChunks.erase(chunk); chunks.erase(pos); // remove from map delete chunk; // free from memory } -void Chunking::trackEntity(ChunkPos chunkPos, const EntityRef& ref) { +void Chunking::trackEntity(ChunkPos chunkPos, const EntityRef ref) { if (!chunkExists(chunkPos)) return; // shouldn't happen chunks[chunkPos]->entities.insert(ref); - if (ref.type == EntityType::PLAYER) + if (ref.kind == EntityKind::PLAYER) chunks[chunkPos]->nplayers++; } -void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef& ref) { +void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) { if (!chunkExists(chunkPos)) return; // do nothing if chunk doesn't even exist @@ -66,7 +66,7 @@ void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef& ref) { chunk->entities.erase(ref); // gone - if (ref.type == EntityType::PLAYER) + if (ref.kind == EntityKind::PLAYER) chunks[chunkPos]->nplayers--; assert(chunks[chunkPos]->nplayers >= 0); @@ -75,13 +75,13 @@ void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef& ref) { deleteChunk(chunkPos); } -void Chunking::addEntityToChunks(std::set chnks, const EntityRef& ref) { +void Chunking::addEntityToChunks(std::set chnks, const EntityRef ref) { Entity *ent = ref.getEntity(); - bool alive = ent->isAlive(); + bool alive = ent->isExtant(); // TODO: maybe optimize this, potentially using AROUND packets? for (Chunk *chunk : chnks) { - for (const EntityRef& otherRef : chunk->entities) { + for (const EntityRef otherRef : chunk->entities) { // skip oneself if (ref == otherRef) continue; @@ -89,31 +89,31 @@ void Chunking::addEntityToChunks(std::set chnks, const EntityRef& ref) { Entity *other = otherRef.getEntity(); // notify all visible players of the existence of this Entity - if (alive && otherRef.type == EntityType::PLAYER) { + if (alive && otherRef.kind == EntityKind::PLAYER) { ent->enterIntoViewOf(otherRef.sock); } // notify this *player* of the existence of all visible Entities - if (ref.type == EntityType::PLAYER && other->isAlive()) { + if (ref.kind == EntityKind::PLAYER && other->isExtant()) { other->enterIntoViewOf(ref.sock); } // for mobs, increment playersInView - if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER) + if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER) ((Mob*)ent)->playersInView++; - if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER) + if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER) ((Mob*)other)->playersInView++; } } } -void Chunking::removeEntityFromChunks(std::set chnks, const EntityRef& ref) { +void Chunking::removeEntityFromChunks(std::set chnks, const EntityRef ref) { Entity *ent = ref.getEntity(); - bool alive = ent->isAlive(); + bool alive = ent->isExtant(); // TODO: same as above for (Chunk *chunk : chnks) { - for (const EntityRef& otherRef : chunk->entities) { + for (const EntityRef otherRef : chunk->entities) { // skip oneself if (ref == otherRef) continue; @@ -121,19 +121,19 @@ void Chunking::removeEntityFromChunks(std::set chnks, const EntityRef& r Entity *other = otherRef.getEntity(); // notify all visible players of the departure of this Entity - if (alive && otherRef.type == EntityType::PLAYER) { + if (alive && otherRef.kind == EntityKind::PLAYER) { ent->disappearFromViewOf(otherRef.sock); } // notify this *player* of the departure of all visible Entities - if (ref.type == EntityType::PLAYER && other->isAlive()) { + if (ref.kind == EntityKind::PLAYER && other->isExtant()) { other->disappearFromViewOf(ref.sock); } // for mobs, decrement playersInView - if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER) + if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER) ((Mob*)ent)->playersInView--; - if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER) + if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER) ((Mob*)other)->playersInView--; } } @@ -154,8 +154,8 @@ static void emptyChunk(ChunkPos chunkPos) { // unspawn all of the mobs/npcs std::set refs(chunk->entities); - for (const EntityRef& ref : refs) { - if (ref.type == EntityType::PLAYER) + for (const EntityRef ref : refs) { + if (ref.kind == EntityKind::PLAYER) assert(0); // every call of this will check if the chunk is empty and delete it if so @@ -163,7 +163,7 @@ static void emptyChunk(ChunkPos chunkPos) { } } -void Chunking::updateEntityChunk(const EntityRef& ref, ChunkPos from, ChunkPos to) { +void Chunking::updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to) { Entity* ent = ref.getEntity(); // move to other chunk's player set @@ -267,54 +267,53 @@ void Chunking::createInstance(uint64_t instanceID) { std::cout << "Creating instance " << instanceID << std::endl; for (ChunkPos &coords : templateChunks) { - for (const EntityRef& ref : chunks[coords]->entities) { - if (ref.type == EntityType::PLAYER) + for (const EntityRef ref : chunks[coords]->entities) { + if (ref.kind == EntityKind::PLAYER) continue; int npcID = ref.id; BaseNPC* baseNPC = (BaseNPC*)ref.getEntity(); // make a copy of each NPC in the template chunks and put them in the new instance - if (baseNPC->type == EntityType::MOB) { + if (baseNPC->kind == EntityKind::MOB) { if (((Mob*)baseNPC)->groupLeader != 0 && ((Mob*)baseNPC)->groupLeader != npcID) continue; // follower; don't copy individually - Mob* newMob = new Mob(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle, - instanceID, baseNPC->appearanceData.iNPCType, NPCManager::NPCData[baseNPC->appearanceData.iNPCType], NPCManager::nextId--); - NPCManager::NPCs[newMob->appearanceData.iNPC_ID] = newMob; + Mob* newMob = new Mob(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->angle, + instanceID, baseNPC->type, NPCManager::NPCData[baseNPC->type], NPCManager::nextId--); + NPCManager::NPCs[newMob->id] = newMob; // if in a group, copy over group members as well if (((Mob*)baseNPC)->groupLeader != 0) { - newMob->groupLeader = newMob->appearanceData.iNPC_ID; // set leader ID for new leader + newMob->groupLeader = newMob->id; // set leader ID for new leader Mob* mobData = (Mob*)baseNPC; for (int i = 0; i < 4; i++) { if (mobData->groupMember[i] != 0) { int followerID = NPCManager::nextId--; // id for follower BaseNPC* baseFollower = NPCManager::NPCs[mobData->groupMember[i]]; // follower from template // new follower instance - Mob* newMobFollower = new Mob(baseFollower->x, baseFollower->y, baseFollower->z, baseFollower->appearanceData.iAngle, - instanceID, baseFollower->appearanceData.iNPCType, NPCManager::NPCData[baseFollower->appearanceData.iNPCType], followerID); + Mob* newMobFollower = new Mob(baseFollower->x, baseFollower->y, baseFollower->z, baseFollower->angle, + instanceID, baseFollower->type, NPCManager::NPCData[baseFollower->type], followerID); // add follower to NPC maps NPCManager::NPCs[followerID] = newMobFollower; // set follower-specific properties - newMobFollower->groupLeader = newMob->appearanceData.iNPC_ID; + newMobFollower->groupLeader = newMob->id; newMobFollower->offsetX = ((Mob*)baseFollower)->offsetX; newMobFollower->offsetY = ((Mob*)baseFollower)->offsetY; // add follower copy to leader copy newMob->groupMember[i] = followerID; NPCManager::updateNPCPosition(followerID, baseFollower->x, baseFollower->y, baseFollower->z, - instanceID, baseFollower->appearanceData.iAngle); + instanceID, baseFollower->angle); } } } - NPCManager::updateNPCPosition(newMob->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z, - instanceID, baseNPC->appearanceData.iAngle); + NPCManager::updateNPCPosition(newMob->id, baseNPC->x, baseNPC->y, baseNPC->z, + instanceID, baseNPC->angle); } else { - BaseNPC* newNPC = new BaseNPC(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle, - instanceID, baseNPC->appearanceData.iNPCType, NPCManager::nextId--); - NPCManager::NPCs[newNPC->appearanceData.iNPC_ID] = newNPC; - NPCManager::updateNPCPosition(newNPC->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z, - instanceID, baseNPC->appearanceData.iAngle); + BaseNPC* newNPC = new BaseNPC(baseNPC->angle, instanceID, baseNPC->type, NPCManager::nextId--); + NPCManager::NPCs[newNPC->id] = newNPC; + NPCManager::updateNPCPosition(newNPC->id, baseNPC->x, baseNPC->y, baseNPC->z, + instanceID, baseNPC->angle); } } } diff --git a/src/Chunking.hpp b/src/Chunking.hpp index 61ead49..4b4cee0 100644 --- a/src/Chunking.hpp +++ b/src/Chunking.hpp @@ -1,14 +1,10 @@ #pragma once -#include "core/Core.hpp" +#include "EntityRef.hpp" -#include #include #include -#include -#include - -struct EntityRef; +#include class Chunk { public: @@ -36,13 +32,13 @@ namespace Chunking { extern const ChunkPos INVALID_CHUNK; - void updateEntityChunk(const EntityRef& ref, ChunkPos from, ChunkPos to); + void updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to); - void trackEntity(ChunkPos chunkPos, const EntityRef& ref); - void untrackEntity(ChunkPos chunkPos, const EntityRef& ref); + void trackEntity(ChunkPos chunkPos, const EntityRef ref); + void untrackEntity(ChunkPos chunkPos, const EntityRef ref); - void addEntityToChunks(std::set chnks, const EntityRef& ref); - void removeEntityFromChunks(std::set chnks, const EntityRef& ref); + void addEntityToChunks(std::set chnks, const EntityRef ref); + void removeEntityFromChunks(std::set chnks, const EntityRef ref); bool chunkExists(ChunkPos chunk); ChunkPos chunkPosAt(int posX, int posY, uint64_t instanceID); diff --git a/src/Combat.cpp b/src/Combat.cpp index d14a794..aa8b912 100644 --- a/src/Combat.cpp +++ b/src/Combat.cpp @@ -1,22 +1,328 @@ #include "Combat.hpp" -#include "PlayerManager.hpp" -#include "Nanos.hpp" -#include "NPCManager.hpp" -#include "Items.hpp" -#include "Missions.hpp" -#include "Groups.hpp" -#include "Transport.hpp" -#include "Racing.hpp" -#include "Abilities.hpp" + +#include "servers/CNShardServer.hpp" + #include "Rand.hpp" +#include "Player.hpp" +#include "PlayerManager.hpp" +#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; +#pragma region Player +bool Player::addBuff(int buffId, BuffCallback onUpdate, BuffCallback onTick, BuffStack* stack) { + if(!isAlive()) + return false; + + if(!hasBuff(buffId)) { + buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack); + return true; + } + + buffs[buffId]->updateCallbacks(onUpdate, onTick); + buffs[buffId]->addStack(stack); + return false; +} + +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, BuffClass buffClass) { + if(hasBuff(buffId)) { + buffs[buffId]->clear(buffClass); + // buff might not be stale since another buff class might remain + if(buffs[buffId]->isStale()) { + delete buffs[buffId]; + buffs.erase(buffId); + } + } +} + +void Player::clearBuffs(bool force) { + auto it = buffs.begin(); + while(it != buffs.end()) { + Buff* buff = (*it).second; + if(!force) buff->clear(); + delete buff; + it = buffs.erase(it); + } +} + +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() { + return HP > 0; +} + +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) { + 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); + if(!isAlive()) + break; // unsafe to keep ticking if we're dead + } +} +#pragma endregion + +#pragma region CombatNPC +bool CombatNPC::addBuff(int buffId, BuffCallback onUpdate, BuffCallback onTick, BuffStack* stack) { /* stubbed */ + if(!isAlive()) + return false; + + if(!hasBuff(buffId)) { + buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack); + return true; + } + + buffs[buffId]->updateCallbacks(onUpdate, onTick); + buffs[buffId]->addStack(stack); + return false; +} + +Buff* CombatNPC::getBuff(int buffId) { /* stubbed */ + if(hasBuff(buffId)) { + return buffs[buffId]; + } + return nullptr; +} + +void CombatNPC::removeBuff(int buffId) { + if(hasBuff(buffId)) { + buffs[buffId]->clear(); + delete buffs[buffId]; + buffs.erase(buffId); + } +} + +void CombatNPC::removeBuff(int buffId, BuffClass buffClass) { + if(hasBuff(buffId)) { + buffs[buffId]->clear(buffClass); + // buff might not be stale since another buff class might remain + if(buffs[buffId]->isStale()) { + delete buffs[buffId]; + buffs.erase(buffId); + } + } +} + +void CombatNPC::clearBuffs(bool force) { + auto it = buffs.begin(); + while(it != buffs.end()) { + Buff* buff = (*it).second; + if(!force) buff->clear(); + delete buff; + it = buffs.erase(it); + } +} + +bool CombatNPC::hasBuff(int buffId) { + auto buff = buffs.find(buffId); + return buff != buffs.end() && !buff->second->isStale(); +} + +int CombatNPC::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 CombatNPC::takeDamage(EntityRef src, int amt) { + int dmg = amt; + if(hp - dmg < 0) dmg = hp; + hp -= dmg; + + if(hp <= 0) transition(AIState::DEAD, src); + + return dmg; +} + +int CombatNPC::heal(EntityRef src, int amt) { + int heal = amt; + if(hp + heal > getMaxHP()) heal = getMaxHP() - hp; + hp += heal; + + return heal; +} + +bool CombatNPC::isAlive() { + return hp > 0; +} + +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()) + stateHandlers[state](this, currTime); + else { + std::cout << "[WARN] State " << (int)state << " has no handler; going inactive" << std::endl; + transition(AIState::INACTIVE, id); + } +} + +void CombatNPC::transition(AIState newState, EntityRef src) { + + state = newState; + if (transitionHandlers.find(newState) != transitionHandlers.end()) + transitionHandlers[newState](this, src); + else { + std::cout << "[WARN] Transition to " << (int)state << " has no handler; going inactive" << std::endl; + transition(AIState::INACTIVE, id); + } + /* TODO: fire any triggered events + for (NPCEvent& event : NPCManager::NPCEvents) + if (event.trigger == ON_KILLED && event.npcType == type) + event.handler(src, this); + */ +} +#pragma endregion + static std::pair getDamage(int attackPower, int defensePower, bool shouldCrit, bool batteryBoost, int attackerStyle, int defenderStyle, int difficulty) { @@ -114,7 +420,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) { BaseNPC* npc = NPCManager::NPCs[targets[i]]; - if (npc->type != EntityType::MOB) { + if (npc->kind != EntityKind::MOB) { std::cout << "[WARN] pcAttackNpcs: NPC is not a mob" << std::endl; return; } @@ -137,11 +443,11 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) { else plr->batteryW = 0; - damage.first = hitMob(sock, mob, damage.first); + damage.first = mob->takeDamage(sock, damage.first); - respdata[i].iID = mob->appearanceData.iNPC_ID; + respdata[i].iID = mob->id; respdata[i].iDamage = damage.first; - respdata[i].iHP = mob->appearanceData.iHP; + respdata[i].iHP = mob->hp; respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade } @@ -168,7 +474,7 @@ void Combat::npcAttackPc(Mob *mob, time_t currTime) { if (!(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) plr->HP -= damage.first; - pkt->iNPC_ID = mob->appearanceData.iNPC_ID; + pkt->iNPC_ID = mob->id; pkt->iPCCnt = 1; atk->iID = plr->iID; @@ -180,52 +486,11 @@ void Combat::npcAttackPc(Mob *mob, time_t currTime) { PlayerManager::sendToViewable(mob->target, respbuf, P_FE2CL_NPC_ATTACK_PCs); if (plr->HP <= 0) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!MobAI::aggroCheck(mob, currTime)) { - MobAI::clearDebuff(mob); - if (mob->groupLeader != 0) - MobAI::groupRetreat(mob); - } + if (!MobAI::aggroCheck(mob, getTime())) + mob->transition(AIState::RETREAT, mob->target); } } -int Combat::hitMob(CNSocket *sock, Mob *mob, int damage) { - // cannot kill mobs multiple times; cannot harm retreating mobs - if (mob->state != MobState::ROAMING && mob->state != MobState::COMBAT) { - return 0; // no damage - } - - if (mob->skillStyle >= 0) - return 0; // don't hurt a mob casting corruption - - if (mob->state == MobState::ROAMING) { - assert(mob->target == nullptr); - MobAI::enterCombat(sock, mob); - - if (mob->groupLeader != 0) - MobAI::followToCombat(mob); - } - - mob->appearanceData.iHP -= damage; - - // wake up sleeping monster - if (mob->appearanceData.iConditionBitFlag & CSB_BIT_MEZ) { - mob->appearanceData.iConditionBitFlag &= ~CSB_BIT_MEZ; - - INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); - pkt1.eCT = 2; - pkt1.iID = mob->appearanceData.iNPC_ID; - pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag; - NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); - } - - if (mob->appearanceData.iHP <= 0) - killMob(mob->target, mob); - - return damage; -} - /* * When a group of players is doing missions together, we want them to all get * quest items at the same time, but we don't want the odds of quest item @@ -233,96 +498,16 @@ int Combat::hitMob(CNSocket *sock, Mob *mob, int damage) { * single RNG roll per mission task, and every group member shares that same * set of rolls. */ -static void genQItemRolls(Player *leader, std::map& rolls) { - for (int i = 0; i < leader->groupCnt; i++) { - if (leader->groupIDs[i] == 0) - continue; - - CNSocket *otherSock = PlayerManager::getSockFromID(leader->groupIDs[i]); - if (otherSock == nullptr) - continue; - - Player *member = PlayerManager::getPlayer(otherSock); +void Combat::genQItemRolls(std::vector players, std::map& rolls) { + for (int i = 0; i < players.size(); i++) { + Player* member = players[i]; for (int j = 0; j < ACTIVE_MISSION_COUNT; j++) if (member->tasks[j] != 0) rolls[member->tasks[j]] = Rand::rand(); } } -void Combat::killMob(CNSocket *sock, Mob *mob) { - mob->state = MobState::DEAD; - mob->target = nullptr; - mob->appearanceData.iConditionBitFlag = 0; - mob->skillStyle = -1; - mob->unbuffTimes.clear(); - mob->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step? - - // check for the edge case where hitting the mob did not aggro it - if (sock != nullptr) { - Player* plr = PlayerManager::getPlayer(sock); - - Items::DropRoll rolled; - Items::DropRoll eventRolled; - std::map qitemRolls; - - Player *leader = PlayerManager::getPlayerFromID(plr->iIDGroup); - assert(leader != nullptr); // should never happen - - genQItemRolls(leader, qitemRolls); - - if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) { - Items::giveMobDrop(sock, mob, rolled, eventRolled); - Missions::mobKilled(sock, mob->appearanceData.iNPCType, qitemRolls); - } else { - for (int i = 0; i < leader->groupCnt; i++) { - CNSocket* sockTo = PlayerManager::getSockFromID(leader->groupIDs[i]); - if (sockTo == nullptr) - continue; - - Player *otherPlr = PlayerManager::getPlayer(sockTo); - - // only contribute to group members' kills if they're close enough - int dist = std::hypot(plr->x - otherPlr->x + 1, plr->y - otherPlr->y + 1); - if (dist > 5000) - continue; - - Items::giveMobDrop(sockTo, mob, rolled, eventRolled); - Missions::mobKilled(sockTo, mob->appearanceData.iNPCType, qitemRolls); - } - } - } - - // delay the despawn animation - mob->despawned = false; - - // fire any triggered events - for (NPCEvent& event : NPCManager::NPCEvents) - if (event.trigger == ON_KILLED && event.npcType == mob->appearanceData.iNPCType) - event.handler(sock, mob); - - auto it = Transport::NPCQueues.find(mob->appearanceData.iNPC_ID); - if (it == Transport::NPCQueues.end() || it->second.empty()) - return; - - // rewind or empty the movement queue - if (mob->staticPath) { - /* - * This is inelegant, but we wind forward in the path until we find the point that - * corresponds with the Mob's spawn point. - * - * IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever. - */ - auto& queue = it->second; - for (auto point = queue.front(); point.x != mob->spawnX || point.y != mob->spawnY; point = queue.front()) { - queue.pop(); - queue.push(point); - } - } else { - Transport::NPCQueues.erase(mob->appearanceData.iNPC_ID); - } -} - static void combatBegin(CNSocket *sock, CNPacketData *data) { Player *plr = PlayerManager::getPlayer(sock); @@ -345,40 +530,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; @@ -402,12 +574,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); @@ -417,12 +616,12 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) { return; // Unlike the attack mob packet, attacking players packet has an 8-byte trail (Instead of 4 bytes). - if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(int32_t) * 2, data->size)) { + if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(sGM_PVPTarget), data->size)) { std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_CHARs packet size\n"; return; } - int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs)); + sGM_PVPTarget* pktdata = (sGM_PVPTarget*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs)); if (!validOutVarPacket(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC), pkt->iTargetCnt, sizeof(sAttackResult))) { std::cout << "[WARN] bad sP_FE2CL_PC_ATTACK_CHARs_SUCC packet size\n"; @@ -441,11 +640,19 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) { resp->iTargetCnt = pkt->iTargetCnt; for (int i = 0; i < pkt->iTargetCnt; i++) { - if (pktdata[i*2+1] == 1) { // eCT == 1; attack player - Player *target = nullptr; + + ICombatant* target = nullptr; + std::pair damage; + + if (pkt->iTargetCnt > 1) + damage.first = plr->groupDamage; + else + damage.first = plr->pointDamage; + + if (pktdata[i].eCT == 1) { // eCT == 1; attack player for (auto& pair : PlayerManager::players) { - if (pair.second->iID == pktdata[i*2]) { + if (pair.second->iID == pktdata[i].iID) { target = pair.second; break; } @@ -457,67 +664,41 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) { return; } - std::pair damage; + damage = getDamage(damage.first, ((Player*)target)->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0); - if (pkt->iTargetCnt > 1) - damage.first = plr->groupDamage; - else - damage.first = plr->pointDamage; - - damage = getDamage(damage.first, target->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0); - - if (plr->batteryW >= 6 + plr->level) - plr->batteryW -= 6 + plr->level; - else - plr->batteryW = 0; - - target->HP -= damage.first; - - respdata[i].eCT = pktdata[i*2+1]; - respdata[i].iID = target->iID; - respdata[i].iDamage = damage.first; - respdata[i].iHP = target->HP; - respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade } else { // eCT == 4; attack mob - if (NPCManager::NPCs.find(pktdata[i*2]) == NPCManager::NPCs.end()) { + + if (NPCManager::NPCs.find(pktdata[i].iID) == NPCManager::NPCs.end()) { // not sure how to best handle this std::cout << "[WARN] pcAttackChars: NPC ID not found" << std::endl; return; } - BaseNPC* npc = NPCManager::NPCs[pktdata[i * 2]]; - if (npc->type != EntityType::MOB) { + BaseNPC* npc = NPCManager::NPCs[pktdata[i].iID]; + if (npc->kind != EntityKind::MOB) { std::cout << "[WARN] pcAttackChars: NPC is not a mob" << std::endl; return; } Mob* mob = (Mob*)npc; - - std::pair damage; - - if (pkt->iTargetCnt > 1) - damage.first = plr->groupDamage; - else - damage.first = plr->pointDamage; - + target = mob; int difficulty = (int)mob->data["m_iNpcLevel"]; - 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; - - damage.first = hitMob(sock, mob, damage.first); - - respdata[i].eCT = pktdata[i*2+1]; - respdata[i].iID = mob->appearanceData.iNPC_ID; - respdata[i].iDamage = damage.first; - respdata[i].iHP = mob->appearanceData.iHP; - respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade } + + if (plr->batteryW >= 6 + plr->level) + plr->batteryW -= 6 + plr->level; + else + plr->batteryW = 0; + + damage.first = target->takeDamage(sock, damage.first); + + respdata[i].eCT = pktdata[i].eCT; + respdata[i].iID = target->getID(); + respdata[i].iDamage = damage.first; + respdata[i].iHP = target->getCurrentHP(); + respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade } sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_CHARs_SUCC, resplen); @@ -653,21 +834,6 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) { return; } - // rapid fire anti-cheat - time_t currTime = getTime(); - if (currTime - plr->lastShot < plr->fireRate * 80) - plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing - else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0) - plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // lose suspicion for delayed firing - - plr->lastShot = currTime; - - if (plr->suspicionRating > 10000) { // kill the socket when the player is too suspicious - sock->kill(); - CNShardServer::_killConnection(sock); - return; - } - /* * initialize response struct * rocket style hit doesn't work properly, so we're always sending this one @@ -696,7 +862,7 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) { } BaseNPC* npc = NPCManager::NPCs[pktdata[i]]; - if (npc->type != EntityType::MOB) { + if (npc->kind != EntityKind::MOB) { std::cout << "[WARN] projectileHit: NPC is not a mob" << std::endl; return; } @@ -709,11 +875,11 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) { int difficulty = (int)mob->data["m_iNpcLevel"]; damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet->weaponBoost, Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty); - damage.first = hitMob(sock, mob, damage.first); + damage.first = mob->takeDamage(sock, damage.first); - respdata[i].iID = mob->appearanceData.iNPC_ID; + respdata[i].iID = mob->id; respdata[i].iDamage = damage.first; - respdata[i].iHP = mob->appearanceData.iHP; + respdata[i].iHP = mob->hp; respdata[i].iHitFlag = damage.second; } @@ -728,6 +894,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; @@ -735,18 +902,13 @@ static void playerTick(CNServer *serv, time_t currTime) { bool transmit = false; // group ticks - if (plr->groupCnt > 1) - Groups::groupTickInfo(plr); + if (plr->group != nullptr) + 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) { @@ -758,22 +920,24 @@ static void playerTick(CNServer *serv, time_t currTime) { plr->healCooldown -= 4000; } - for (int i = 0; i < 3; i++) { - if (plr->activeNano != 0 && plr->equippedNanos[i] == plr->activeNano) { // spend stamina - plr->Nanos[plr->activeNano].iStamina -= 1 + plr->nanoDrainRate / 5; + // combat tick + if(currTime - lastCombatTIme >= 2000) { + plr->step(currTime); + transmit = true; + } - if (plr->Nanos[plr->activeNano].iStamina <= 0) - Nanos::summonNano(sock, -1, true); // unsummon nano silently - - transmit = true; - } else if (plr->Nanos[plr->equippedNanos[i]].iStamina < 150) { // regain stamina - sNano& nano = plr->Nanos[plr->equippedNanos[i]]; - nano.iStamina += 1; - - if (nano.iStamina > 150) - nano.iStamina = 150; - - transmit = true; + // nanos + 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) { + ICombatant* src = dynamic_cast(plr); + int32_t targets[] = { plr->iID }; + std::vector affectedCombatants = Abilities::matchTargets(src, skill, 1, targets); + Abilities::useNanoSkill(sock, skill, *nano, affectedCombatants); + } } } @@ -789,6 +953,20 @@ 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->combatTick() gets called in Player::step + 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); @@ -803,13 +981,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/Combat.hpp b/src/Combat.hpp index f7ee0f9..a4868d3 100644 --- a/src/Combat.hpp +++ b/src/Combat.hpp @@ -1,15 +1,10 @@ #pragma once -#include "core/Core.hpp" -#include "servers/CNShardServer.hpp" -#include "NPC.hpp" +#include "Player.hpp" #include "MobAI.hpp" -#include "JSON.hpp" - #include -#include -#include +#include struct Bullet { int pointDamage; @@ -24,6 +19,5 @@ namespace Combat { void init(); void npcAttackPc(Mob *mob, time_t currTime); - int hitMob(CNSocket *sock, Mob *mob, int damage); - void killMob(CNSocket *sock, Mob *mob); + void genQItemRolls(std::vector players, std::map& rolls); } diff --git a/src/CustomCommands.cpp b/src/CustomCommands.cpp index 06d9d69..2021d32 100644 --- a/src/CustomCommands.cpp +++ b/src/CustomCommands.cpp @@ -1,18 +1,19 @@ #include "CustomCommands.hpp" -#include "Chat.hpp" + +#include "db/Database.hpp" + +#include "Player.hpp" #include "PlayerManager.hpp" +#include "Chat.hpp" #include "TableData.hpp" #include "NPCManager.hpp" -#include "Eggs.hpp" #include "MobAI.hpp" -#include "Items.hpp" -#include "db/Database.hpp" -#include "Transport.hpp" #include "Missions.hpp" +#include "Eggs.hpp" +#include "Items.hpp" +#include "Abilities.hpp" #include -#include -#include #include typedef void (*CommandHandler)(std::string fullString, std::vector& args, CNSocket* sock); @@ -243,20 +244,20 @@ static void summonWCommand(std::string full, std::vector& args, CNS BaseNPC *npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true); // update angle - npc->appearanceData.iAngle = (plr->angle + 180) % 360; - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, plr->instanceID, npc->appearanceData.iAngle); + npc->angle = (plr->angle + 180) % 360; + NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, plr->instanceID, npc->angle); // if we're in a lair, we need to spawn the NPC in both the private instance and the template if (PLAYERID(plr->instanceID) != 0) { npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true, true); - npc->appearanceData.iAngle = (plr->angle + 180) % 360; - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, npc->instanceID, npc->appearanceData.iAngle); + npc->angle = (plr->angle + 180) % 360; + NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, npc->instanceID, npc->angle); } Chat::sendServerMessage(sock, "/summonW: placed mob with type: " + std::to_string(type) + - ", id: " + std::to_string(npc->appearanceData.iNPC_ID)); - TableData::RunningMobs[npc->appearanceData.iNPC_ID] = npc; // only record the one in the template + ", id: " + std::to_string(npc->id)); + TableData::RunningMobs[npc->id] = npc; // only record the one in the template } static void unsummonWCommand(std::string full, std::vector& args, CNSocket* sock) { @@ -269,24 +270,24 @@ static void unsummonWCommand(std::string full, std::vector& args, C return; } - if (TableData::RunningEggs.find(npc->appearanceData.iNPC_ID) != TableData::RunningEggs.end()) { - Chat::sendServerMessage(sock, "/unsummonW: removed egg with type: " + std::to_string(npc->appearanceData.iNPCType) + - ", id: " + std::to_string(npc->appearanceData.iNPC_ID)); - TableData::RunningEggs.erase(npc->appearanceData.iNPC_ID); - NPCManager::destroyNPC(npc->appearanceData.iNPC_ID); + if (TableData::RunningEggs.find(npc->id) != TableData::RunningEggs.end()) { + Chat::sendServerMessage(sock, "/unsummonW: removed egg with type: " + std::to_string(npc->type) + + ", id: " + std::to_string(npc->id)); + TableData::RunningEggs.erase(npc->id); + NPCManager::destroyNPC(npc->id); return; } - if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) == TableData::RunningMobs.end() - && TableData::RunningGroups.find(npc->appearanceData.iNPC_ID) == TableData::RunningGroups.end()) { + if (TableData::RunningMobs.find(npc->id) == TableData::RunningMobs.end() + && TableData::RunningGroups.find(npc->id) == TableData::RunningGroups.end()) { Chat::sendServerMessage(sock, "/unsummonW: Closest NPC is not a gruntwork mob."); return; } - if (NPCManager::NPCs.find(npc->appearanceData.iNPC_ID) != NPCManager::NPCs.end() && NPCManager::NPCs[npc->appearanceData.iNPC_ID]->type == EntityType::MOB) { + if (NPCManager::NPCs.find(npc->id) != NPCManager::NPCs.end() && NPCManager::NPCs[npc->id]->kind == EntityKind::MOB) { int leadId = ((Mob*)npc)->groupLeader; if (leadId != 0) { - if (NPCManager::NPCs.find(leadId) == NPCManager::NPCs.end() || NPCManager::NPCs[leadId]->type != EntityType::MOB) { + if (NPCManager::NPCs.find(leadId) == NPCManager::NPCs.end() || NPCManager::NPCs[leadId]->kind != EntityKind::MOB) { std::cout << "[WARN] unsummonW: leader not found!" << std::endl; } Mob* leadNpc = (Mob*)NPCManager::NPCs[leadId]; @@ -294,7 +295,7 @@ static void unsummonWCommand(std::string full, std::vector& args, C if (leadNpc->groupMember[i] == 0) break; - if (NPCManager::NPCs.find(leadNpc->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadNpc->groupMember[i]]->type != EntityType::MOB) { + if (NPCManager::NPCs.find(leadNpc->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadNpc->groupMember[i]]->kind != EntityKind::MOB) { std::cout << "[WARN] unsommonW: leader can't find a group member!" << std::endl; continue; } @@ -308,12 +309,12 @@ static void unsummonWCommand(std::string full, std::vector& args, C } } - Chat::sendServerMessage(sock, "/unsummonW: removed mob with type: " + std::to_string(npc->appearanceData.iNPCType) + - ", id: " + std::to_string(npc->appearanceData.iNPC_ID)); + Chat::sendServerMessage(sock, "/unsummonW: removed mob with type: " + std::to_string(npc->type) + + ", id: " + std::to_string(npc->id)); - TableData::RunningMobs.erase(npc->appearanceData.iNPC_ID); + TableData::RunningMobs.erase(npc->id); - NPCManager::destroyNPC(npc->appearanceData.iNPC_ID); + NPCManager::destroyNPC(npc->id); } static void toggleAiCommand(std::string full, std::vector& args, CNSocket* sock) { @@ -324,11 +325,11 @@ static void toggleAiCommand(std::string full, std::vector& args, CN // return all mobs to their spawn points for (auto& pair : NPCManager::NPCs) { - if (pair.second->type != EntityType::MOB) + if (pair.second->kind != EntityKind::MOB) continue; Mob* mob = (Mob*)pair.second; - mob->state = MobState::RETREAT; + mob->state = AIState::RETREAT; mob->target = nullptr; mob->nextMovement = getTime(); @@ -356,24 +357,24 @@ static void npcRotateCommand(std::string full, std::vector& args, C } int angle = (plr->angle + 180) % 360; - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, npc->instanceID, angle); + NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle); // if it's a gruntwork NPC, rotate in-place - if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) != TableData::RunningMobs.end()) { - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, npc->instanceID, angle); + if (TableData::RunningMobs.find(npc->id) != TableData::RunningMobs.end()) { + NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle); Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for gruntwork NPC " - + std::to_string(npc->appearanceData.iNPC_ID)); + + std::to_string(npc->id)); } else { - TableData::RunningNPCRotations[npc->appearanceData.iNPC_ID] = angle; + TableData::RunningNPCRotations[npc->id] = angle; Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for NPC " - + std::to_string(npc->appearanceData.iNPC_ID)); + + std::to_string(npc->id)); } // update rotation clientside INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt); - pkt.NPCAppearanceData = npc->appearanceData; + pkt.NPCAppearanceData = npc->getAppearanceData(); sock->sendPacket(pkt, P_FE2CL_NPC_ENTER); } @@ -443,9 +444,9 @@ static void npcInstanceCommand(std::string full, std::vector& args, return; } - Chat::sendServerMessage(sock, "[NPCI] Moving NPC with ID " + std::to_string(npc->appearanceData.iNPC_ID) + " to instance " + std::to_string(instance)); - TableData::RunningNPCMapNumbers[npc->appearanceData.iNPC_ID] = instance; - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, instance, npc->appearanceData.iAngle); + Chat::sendServerMessage(sock, "[NPCI] Moving NPC with ID " + std::to_string(npc->id) + " to instance " + std::to_string(instance)); + TableData::RunningNPCMapNumbers[npc->id] = instance; + NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, instance, npc->angle); } static void minfoCommand(std::string full, std::vector& args, CNSocket* sock) { @@ -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) { @@ -532,7 +536,7 @@ static void eggCommand(std::string full, std::vector& args, CNSocke int addX = 0; //-500.0f * sin(plr->angle / 180.0f * M_PI); int addY = 0; //-500.0f * cos(plr->angle / 180.0f * M_PI); - Egg* egg = new Egg(plr->x + addX, plr->y + addY, plr->z, plr->instanceID, eggType, id, false); // change last arg to true after gruntwork + Egg* egg = new Egg(plr->instanceID, eggType, id, false); // change last arg to true after gruntwork NPCManager::NPCs[id] = egg; NPCManager::updateNPCPosition(id, plr->x + addX, plr->y + addY, plr->z, plr->instanceID, plr->angle); @@ -609,40 +613,40 @@ static void summonGroupCommand(std::string full, std::vector& args, } BaseNPC *npc = NPCManager::summonNPC(x, y, z, plr->instanceID, type, wCommand); - if (team == 2 && i > 0 && npc->type == EntityType::MOB) { - leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID; - Mob* mob = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID]; - mob->groupLeader = leadNpc->appearanceData.iNPC_ID; + if (team == 2 && i > 0 && npc->kind == EntityKind::MOB) { + leadNpc->groupMember[i-1] = npc->id; + Mob* mob = (Mob*)NPCManager::NPCs[npc->id]; + mob->groupLeader = leadNpc->id; mob->offsetX = x - plr->x; mob->offsetY = y - plr->y; } - npc->appearanceData.iAngle = (plr->angle + 180) % 360; - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, x, y, z, plr->instanceID, npc->appearanceData.iAngle); + npc->angle = (plr->angle + 180) % 360; + NPCManager::updateNPCPosition(npc->id, x, y, z, plr->instanceID, npc->angle); // if we're in a lair, we need to spawn the NPC in both the private instance and the template if (PLAYERID(plr->instanceID) != 0) { npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, wCommand, true); - if (team == 2 && i > 0 && npc->type == EntityType::MOB) { - leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID; - Mob* mob = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID]; - mob->groupLeader = leadNpc->appearanceData.iNPC_ID; + if (team == 2 && i > 0 && npc->kind == EntityKind::MOB) { + leadNpc->groupMember[i-1] = npc->id; + Mob* mob = (Mob*)NPCManager::NPCs[npc->id]; + mob->groupLeader = leadNpc->id; mob->offsetX = x - plr->x; mob->offsetY = y - plr->y; } - npc->appearanceData.iAngle = (plr->angle + 180) % 360; - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, x, y, z, plr->instanceID, npc->appearanceData.iAngle); + npc->angle = (plr->angle + 180) % 360; + NPCManager::updateNPCPosition(npc->id, x, y, z, plr->instanceID, npc->angle); } Chat::sendServerMessage(sock, "/summonGroup(W): placed mob with type: " + std::to_string(type) + - ", id: " + std::to_string(npc->appearanceData.iNPC_ID)); + ", id: " + std::to_string(npc->id)); - if (i == 0 && team == 2 && npc->type == EntityType::MOB) { + if (i == 0 && team == 2 && npc->kind == EntityKind::MOB) { type = type2; - leadNpc = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID]; - leadNpc->groupLeader = leadNpc->appearanceData.iNPC_ID; + leadNpc = (Mob*)NPCManager::NPCs[npc->id]; + leadNpc->groupLeader = leadNpc->id; } } @@ -654,7 +658,7 @@ static void summonGroupCommand(std::string full, std::vector& args, return; } - TableData::RunningGroups[leadNpc->appearanceData.iNPC_ID] = leadNpc; // only record the leader + TableData::RunningGroups[leadNpc->id] = leadNpc; // only record the leader } static void flushCommand(std::string full, std::vector& args, CNSocket* sock) { @@ -671,15 +675,14 @@ static void whoisCommand(std::string full, std::vector& args, CNSoc return; } - Chat::sendServerMessage(sock, "[WHOIS] ID: " + std::to_string(npc->appearanceData.iNPC_ID)); - Chat::sendServerMessage(sock, "[WHOIS] Type: " + std::to_string(npc->appearanceData.iNPCType)); - Chat::sendServerMessage(sock, "[WHOIS] HP: " + std::to_string(npc->appearanceData.iHP)); - Chat::sendServerMessage(sock, "[WHOIS] CBF: " + std::to_string(npc->appearanceData.iConditionBitFlag)); - Chat::sendServerMessage(sock, "[WHOIS] EntityType: " + std::to_string((int)npc->type)); + Chat::sendServerMessage(sock, "[WHOIS] ID: " + std::to_string(npc->id)); + Chat::sendServerMessage(sock, "[WHOIS] Type: " + std::to_string(npc->type)); + Chat::sendServerMessage(sock, "[WHOIS] HP: " + std::to_string(npc->hp)); + Chat::sendServerMessage(sock, "[WHOIS] EntityType: " + std::to_string((int)npc->kind)); Chat::sendServerMessage(sock, "[WHOIS] X: " + std::to_string(npc->x)); Chat::sendServerMessage(sock, "[WHOIS] Y: " + std::to_string(npc->y)); Chat::sendServerMessage(sock, "[WHOIS] Z: " + std::to_string(npc->z)); - Chat::sendServerMessage(sock, "[WHOIS] Angle: " + std::to_string(npc->appearanceData.iAngle)); + Chat::sendServerMessage(sock, "[WHOIS] Angle: " + std::to_string(npc->angle)); std::string chunkPosition = std::to_string(std::get<0>(npc->chunkPos)) + ", " + std::to_string(std::get<1>(npc->chunkPos)) + ", " + std::to_string(std::get<2>(npc->chunkPos)); Chat::sendServerMessage(sock, "[WHOIS] Chunk: {" + chunkPosition + "}"); Chat::sendServerMessage(sock, "[WHOIS] MapNum: " + std::to_string(MAPNUM(npc->instanceID))); @@ -694,7 +697,7 @@ static void lairUnlockCommand(std::string full, std::vector& args, int lastDist = INT_MAX; for (Chunk *chnk : Chunking::getViewableChunks(plr->chunkPos)) { for (const EntityRef& ref : chnk->entities) { - if (ref.type == EntityType::PLAYER) + if (ref.kind == EntityKind::PLAYER) continue; BaseNPC* npc = (BaseNPC*)ref.getEntity(); @@ -705,7 +708,7 @@ static void lairUnlockCommand(std::string full, std::vector& args, continue; for (auto it = NPCManager::Warps.begin(); it != NPCManager::Warps.end(); it++) { - if (it->second.npcID == npc->appearanceData.iNPCType) { + if (it->second.npcID == npc->type) { taskID = it->second.limitTaskID; missionID = Missions::Tasks[taskID]->task["m_iHMissionID"]; lastDist = dist; @@ -977,11 +980,17 @@ static void pathCommand(std::string full, std::vector& args, CNSock // add first point at NPC's current location std::vector pathPoints; - BaseNPC* marker = new BaseNPC(npc->x, npc->y, npc->z, 0, plr->instanceID, 1386, NPCManager::nextId--); + BaseNPC* marker = new BaseNPC(0, plr->instanceID, 1386, NPCManager::nextId--); + + // assign coords manually, since we aren't actually adding markers to the world + marker->x = npc->x; + marker->y = npc->y; + marker->z = npc->z; + pathPoints.push_back(marker); // map from player TableData::RunningNPCPaths[plr->iID] = std::make_pair(npc, pathPoints); - Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is now following you"); + Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is now following you"); updatePathMarkers(sock); return; } @@ -998,7 +1007,12 @@ static void pathCommand(std::string full, std::vector& args, CNSock // /path kf if (args[1] == "kf") { - BaseNPC* marker = new BaseNPC(npc->x, npc->y, npc->z, 0, plr->instanceID, 1386, NPCManager::nextId--); + BaseNPC* marker = new BaseNPC(0, plr->instanceID, 1386, NPCManager::nextId--); + + marker->x = npc->x; + marker->y = npc->y; + marker->z = npc->z; + entry->second.push_back(marker); Chat::sendServerMessage(sock, "[PATH] Added keyframe"); updatePathMarkers(sock); @@ -1008,8 +1022,8 @@ static void pathCommand(std::string full, std::vector& args, CNSock // /path here if (args[1] == "here") { // bring the NPC to where the player is standing - Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, npc->instanceID, 0); + Transport::NPCQueues.erase(npc->id); // delete transport queue + NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, npc->instanceID, 0); npc->disappearFromViewOf(sock); npc->enterIntoViewOf(sock); Chat::sendServerMessage(sock, "[PATH] Come here"); @@ -1047,9 +1061,9 @@ static void pathCommand(std::string full, std::vector& args, CNSock speed = speedArg; } // return NPC to home - Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue + Transport::NPCQueues.erase(npc->id); // delete transport queue BaseNPC* home = entry->second[0]; - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0); + NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0); npc->disappearFromViewOf(sock); npc->enterIntoViewOf(sock); @@ -1065,7 +1079,7 @@ static void pathCommand(std::string full, std::vector& args, CNSock Transport::lerp(&keyframes, from, to, speed); // lerp from A to B from = to; // update point A } - Transport::NPCQueues[npc->appearanceData.iNPC_ID] = keyframes; + Transport::NPCQueues[npc->id] = keyframes; entry->second.pop_back(); // remove temp end point Chat::sendServerMessage(sock, "[PATH] Testing NPC path"); @@ -1075,9 +1089,9 @@ static void pathCommand(std::string full, std::vector& args, CNSock // /path cancel if (args[1] == "cancel") { // return NPC to home - Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue + Transport::NPCQueues.erase(npc->id); // delete transport queue BaseNPC* home = entry->second[0]; - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0); + NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0); npc->disappearFromViewOf(sock); npc->enterIntoViewOf(sock); // deallocate markers @@ -1087,7 +1101,7 @@ static void pathCommand(std::string full, std::vector& args, CNSock } // unmap TableData::RunningNPCPaths.erase(plr->iID); - Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is no longer following you"); + Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is no longer following you"); return; } @@ -1115,9 +1129,9 @@ static void pathCommand(std::string full, std::vector& args, CNSock } // return NPC to home and set path to repeat - Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue + Transport::NPCQueues.erase(npc->id); // delete transport queue BaseNPC* home = entry->second[0]; - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0); + NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0); npc->disappearFromViewOf(sock); npc->enterIntoViewOf(sock); npc->loopingPath = true; @@ -1134,7 +1148,7 @@ static void pathCommand(std::string full, std::vector& args, CNSock Transport::lerp(&keyframes, from, to, speed); // lerp from A to B from = to; // update point A } - Transport::NPCQueues[npc->appearanceData.iNPC_ID] = keyframes; + Transport::NPCQueues[npc->id] = keyframes; entry->second.pop_back(); // remove temp end point // save to gruntwork @@ -1161,7 +1175,7 @@ static void pathCommand(std::string full, std::vector& args, CNSock finishedPath.isLoop = true; finishedPath.speed = speed; finishedPath.points = finalPoints; - finishedPath.targetIDs.push_back(npc->appearanceData.iNPC_ID); + finishedPath.targetIDs.push_back(npc->id); TableData::FinishedNPCPaths.push_back(finishedPath); @@ -1173,7 +1187,7 @@ static void pathCommand(std::string full, std::vector& args, CNSock // unmap TableData::RunningNPCPaths.erase(plr->iID); - Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is no longer following you"); + Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is no longer following you"); TableData::flush(); Chat::sendServerMessage(sock, "[PATH] Path saved to gruntwork"); diff --git a/src/CustomCommands.hpp b/src/CustomCommands.hpp index 62103da..ae9ea7b 100644 --- a/src/CustomCommands.hpp +++ b/src/CustomCommands.hpp @@ -2,6 +2,8 @@ #include "core/Core.hpp" +#include + namespace CustomCommands { void init(); diff --git a/src/Eggs.cpp b/src/Eggs.cpp index 683582d..04e276d 100644 --- a/src/Eggs.cpp +++ b/src/Eggs.cpp @@ -1,141 +1,78 @@ -#include "core/Core.hpp" #include "Eggs.hpp" + +#include "servers/CNShardServer.hpp" + +#include "Player.hpp" #include "PlayerManager.hpp" -#include "Items.hpp" -#include "Nanos.hpp" #include "Abilities.hpp" -#include "Groups.hpp" +#include "NPCManager.hpp" +#include "Entities.hpp" +#include "Items.hpp" #include 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); - Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup); - int bitFlag = Groups::getGroupFlags(otherPlr); - int CBFlag = Nanos::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) * Nanos::SkillTable[skillId].powerIntensity[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) * Nanos::SkillTable[skillId].powerIntensity[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 = Nanos::SkillTable[skillId].skillType; - skillUse->iTargetCnt = 1; + SkillData* skill = &Abilities::SkillTable[skillId]; + if(skill->drainType == SkillDrainType::PASSIVE) { + // apply buff + if(skill->targetType != SkillTargetType::PLAYERS) { + 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); - Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup); - - int groupFlags = Groups::getGroupFlags(otherPlr); - for (auto& pwr : Nanos::NanoPowers) { - 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->type != EntityType::EGG) + if (npc.second->kind != EntityKind::EGG) continue; auto egg = (Egg*)npc.second; if (!egg->dead || !Chunking::inPopulatedChunks(&egg->viewableChunks)) continue; - if (egg->deadUntil <= timeStamp) { + if (egg->deadUntil <= currTime) { // respawn it egg->dead = false; egg->deadUntil = 0; - egg->appearanceData.iHP = 400; + egg->hp = 400; Chunking::addEntityToChunks(Chunking::getViewableChunks(egg->chunkPos), {npc.first}); } @@ -163,7 +100,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) { return; } auto egg = (Egg*)eggRef.getEntity(); - if (egg->type != EntityType::EGG) { + if (egg->kind != EntityKind::EGG) { std::cout << "[WARN] Player tried to open something other than an?!" << std::endl; return; } @@ -180,7 +117,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) { } */ - int typeId = egg->appearanceData.iNPCType; + int typeId = egg->type; if (EggTypes.find(typeId) == EggTypes.end()) { std::cout << "[WARN] Egg Type " << typeId << " not found!" << std::endl; return; @@ -188,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; @@ -255,7 +189,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) { Chunking::removeEntityFromChunks(Chunking::getViewableChunks(egg->chunkPos), eggRef); egg->dead = true; egg->deadUntil = getTime() + (time_t)type->regen * 1000; - egg->appearanceData.iHP = 0; + egg->hp = 0; } } diff --git a/src/Eggs.hpp b/src/Eggs.hpp index 2155272..9c34ec9 100644 --- a/src/Eggs.hpp +++ b/src/Eggs.hpp @@ -1,7 +1,6 @@ #pragma once #include "core/Core.hpp" -#include "Entities.hpp" struct EggType { int dropCrateId; @@ -11,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/Email.cpp b/src/Email.cpp index 3064138..9928af6 100644 --- a/src/Email.cpp +++ b/src/Email.cpp @@ -1,9 +1,9 @@ #include "Email.hpp" #include "core/Core.hpp" +#include "db/Database.hpp" #include "servers/CNShardServer.hpp" -#include "db/Database.hpp" #include "PlayerManager.hpp" #include "Items.hpp" #include "Chat.hpp" diff --git a/src/Email.hpp b/src/Email.hpp index 3e5c557..55e4302 100644 --- a/src/Email.hpp +++ b/src/Email.hpp @@ -5,6 +5,6 @@ namespace Email { extern std::vector dump; - + void init(); } diff --git a/src/Entities.cpp b/src/Entities.cpp index 973a78c..bd97981 100644 --- a/src/Entities.cpp +++ b/src/Entities.cpp @@ -1,18 +1,15 @@ -#include "core/Core.hpp" #include "Entities.hpp" -#include "Chunking.hpp" -#include "PlayerManager.hpp" -#include "NPCManager.hpp" -#include "Eggs.hpp" -#include "MobAI.hpp" -#include +#include "NPCManager.hpp" +#include "PlayerManager.hpp" + +#include static_assert(std::is_standard_layout::value); static_assert(std::is_trivially_copyable::value); EntityRef::EntityRef(CNSocket *s) { - type = EntityType::PLAYER; + kind = EntityKind::PLAYER; sock = s; } @@ -20,11 +17,11 @@ EntityRef::EntityRef(int32_t i) { id = i; assert(NPCManager::NPCs.find(id) != NPCManager::NPCs.end()); - type = NPCManager::NPCs[id]->type; + kind = NPCManager::NPCs[id]->kind; } bool EntityRef::isValid() const { - if (type == EntityType::PLAYER) + if (kind == EntityKind::PLAYER) return PlayerManager::players.find(sock) != PlayerManager::players.end(); return NPCManager::NPCs.find(id) != NPCManager::NPCs.end(); @@ -33,21 +30,38 @@ bool EntityRef::isValid() const { Entity *EntityRef::getEntity() const { assert(isValid()); - if (type == EntityType::PLAYER) + if (kind == EntityKind::PLAYER) return PlayerManager::getPlayer(sock); return NPCManager::NPCs[id]; } +sNPCAppearanceData BaseNPC::getAppearanceData() { + sNPCAppearanceData data = {}; + data.iAngle = angle; + data.iBarkerType = 0; // unused? + data.iConditionBitFlag = 0; + data.iHP = hp; + data.iNPCType = type; + data.iNPC_ID = id; + data.iX = x; + data.iY = y; + data.iZ = z; + return data; +} + +sNPCAppearanceData CombatNPC::getAppearanceData() { + sNPCAppearanceData data = BaseNPC::getAppearanceData(); + data.iConditionBitFlag = getCompositeCondition(); + return data; +} + /* * Entity coming into view. */ void BaseNPC::enterIntoViewOf(CNSocket *sock) { INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt); - pkt.NPCAppearanceData = appearanceData; - pkt.NPCAppearanceData.iX = x; - pkt.NPCAppearanceData.iY = y; - pkt.NPCAppearanceData.iZ = z; + pkt.NPCAppearanceData = getAppearanceData(); sock->sendPacket(pkt, P_FE2CL_NPC_ENTER); } @@ -56,7 +70,7 @@ void Bus::enterIntoViewOf(CNSocket *sock) { // TODO: Potentially decouple this from BaseNPC? pkt.AppearanceData = { - 3, appearanceData.iNPC_ID, appearanceData.iNPCType, + 3, id, type, x, y, z }; @@ -66,28 +80,40 @@ void Bus::enterIntoViewOf(CNSocket *sock) { void Egg::enterIntoViewOf(CNSocket *sock) { INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt); - Eggs::npcDataToEggData(x, y, z, &appearanceData, &pkt.ShinyAppearanceData); + // TODO: Potentially decouple this from BaseNPC? + pkt.ShinyAppearanceData = { + id, type, 0, // client doesn't care about map num + x, y, z + }; sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER); } - + +sNano* Player::getActiveNano() { + return &Nanos[activeNano]; +} + +sPCAppearanceData Player::getAppearanceData() { + sPCAppearanceData data = {}; + data.iID = iID; + data.iHP = HP; + data.iLv = level; + data.iX = x; + data.iY = y; + data.iZ = z; + data.iAngle = angle; + data.PCStyle = PCStyle; + data.Nano = Nanos[activeNano]; + data.iPCState = iPCState; + data.iSpecialState = iSpecialState; + memcpy(data.ItemEquip, Equip, sizeof(sItemBase) * AEQUIP_COUNT); + return data; +} + // TODO: this is less effiecient than it was, because of memset() void Player::enterIntoViewOf(CNSocket *sock) { INITSTRUCT(sP_FE2CL_PC_NEW, pkt); - - pkt.PCAppearanceData.iID = iID; - pkt.PCAppearanceData.iHP = HP; - pkt.PCAppearanceData.iLv = level; - pkt.PCAppearanceData.iX = x; - pkt.PCAppearanceData.iY = y; - pkt.PCAppearanceData.iZ = z; - pkt.PCAppearanceData.iAngle = angle; - pkt.PCAppearanceData.PCStyle = PCStyle; - pkt.PCAppearanceData.Nano = Nanos[activeNano]; - pkt.PCAppearanceData.iPCState = iPCState; - pkt.PCAppearanceData.iSpecialState = iSpecialState; - memcpy(pkt.PCAppearanceData.ItemEquip, Equip, sizeof(sItemBase) * AEQUIP_COUNT); - + pkt.PCAppearanceData = getAppearanceData(); sock->sendPacket(pkt, P_FE2CL_PC_NEW); } @@ -96,20 +122,20 @@ void Player::enterIntoViewOf(CNSocket *sock) { */ void BaseNPC::disappearFromViewOf(CNSocket *sock) { INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt); - pkt.iNPC_ID = appearanceData.iNPC_ID; + pkt.iNPC_ID = id; sock->sendPacket(pkt, P_FE2CL_NPC_EXIT); } void Bus::disappearFromViewOf(CNSocket *sock) { INITSTRUCT(sP_FE2CL_TRANSPORTATION_EXIT, pkt); pkt.eTT = 3; - pkt.iT_ID = appearanceData.iNPC_ID; + pkt.iT_ID = id; sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_EXIT); } void Egg::disappearFromViewOf(CNSocket *sock) { INITSTRUCT(sP_FE2CL_SHINY_EXIT, pkt); - pkt.iShinyID = appearanceData.iNPC_ID; + pkt.iShinyID = id; sock->sendPacket(pkt, P_FE2CL_SHINY_EXIT); } diff --git a/src/Entities.hpp b/src/Entities.hpp index 4975279..2867801 100644 --- a/src/Entities.hpp +++ b/src/Entities.hpp @@ -1,23 +1,26 @@ #pragma once #include "core/Core.hpp" + +#include "EntityRef.hpp" +#include "Buffs.hpp" #include "Chunking.hpp" +#include "Groups.hpp" -#include #include +#include +#include -enum class EntityType : uint8_t { - INVALID, - PLAYER, - SIMPLE_NPC, - COMBAT_NPC, - MOB, - EGG, - BUS +enum class AIState { + INACTIVE, + ROAMING, + COMBAT, + RETREAT, + DEAD }; struct Entity { - EntityType type = EntityType::INVALID; + EntityKind kind = EntityKind::INVALID; int x = 0, y = 0, z = 0; uint64_t instanceID = 0; ChunkPos chunkPos = {}; @@ -26,47 +29,39 @@ struct Entity { // destructor must be virtual, apparently virtual ~Entity() {} - virtual bool isAlive() { return true; } + virtual bool isExtant() { return true; } // stubs virtual void enterIntoViewOf(CNSocket *sock) = 0; virtual void disappearFromViewOf(CNSocket *sock) = 0; }; -struct EntityRef { - EntityType type; - union { - CNSocket *sock; - int32_t id; - }; +/* + * Interfaces + */ +class ICombatant { +public: + ICombatant() {} + virtual ~ICombatant() {} - EntityRef(CNSocket *s); - EntityRef(int32_t i); - - bool isValid() const; - Entity *getEntity() const; - - bool operator==(const EntityRef& other) const { - if (type != other.type) - return false; - - if (type == EntityType::PLAYER) - return sock == other.sock; - - return id == other.id; - } - - // arbitrary ordering - bool operator<(const EntityRef& other) const { - if (type == other.type) { - if (type == EntityType::PLAYER) - return sock < other.sock; - else - return id < other.id; - } - - return type < other.type; - } + virtual bool addBuff(int, BuffCallback, BuffCallback, BuffStack*) = 0; + virtual Buff* getBuff(int) = 0; + virtual void removeBuff(int) = 0; + virtual void removeBuff(int, BuffClass) = 0; + virtual void clearBuffs(bool) = 0; + virtual bool hasBuff(int) = 0; + virtual int getCompositeCondition() = 0; + virtual int takeDamage(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; }; /* @@ -74,75 +69,104 @@ struct EntityRef { */ class BaseNPC : public Entity { public: - sNPCAppearanceData appearanceData = {}; + int id; + int type; + int hp; + int angle; bool loopingPath = false; - BaseNPC(int _X, int _Y, int _Z, int angle, uint64_t iID, int t, int id) { // XXX - x = _X; - y = _Y; - z = _Z; - appearanceData.iNPCType = t; - appearanceData.iHP = 400; - appearanceData.iAngle = angle; - appearanceData.iConditionBitFlag = 0; - appearanceData.iBarkerType = 0; - appearanceData.iNPC_ID = id; - + BaseNPC(int _A, uint64_t iID, int t, int _id) { + kind = EntityKind::SIMPLE_NPC; + type = t; + hp = 400; + angle = _A; + id = _id; instanceID = iID; }; virtual void enterIntoViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override; + + virtual sNPCAppearanceData getAppearanceData(); }; -struct CombatNPC : public BaseNPC { +struct CombatNPC : public BaseNPC, public ICombatant { int maxHealth = 0; int spawnX = 0; int spawnY = 0; int spawnZ = 0; int level = 0; int speed = 300; + AIState state = AIState::INACTIVE; + Group* group = nullptr; + int playersInView = 0; // for optimizing away AI in empty chunks - void (*_stepAI)(CombatNPC*, time_t) = nullptr; + std::map stateHandlers; + std::map transitionHandlers; - // XXX - CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP) : - BaseNPC(x, y, z, angle, iID, t, id), - maxHealth(maxHP) {} + std::unordered_map buffs = {}; - virtual void stepAI(time_t currTime) { - if (_stepAI != nullptr) - _stepAI(this, currTime); + CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP) + : BaseNPC(angle, iID, t, id), maxHealth(maxHP) { + spawnX = x; + spawnY = y; + spawnZ = z; + + kind = EntityKind::COMBAT_NPC; + + stateHandlers[AIState::INACTIVE] = {}; + transitionHandlers[AIState::INACTIVE] = {}; } - virtual bool isAlive() override { return appearanceData.iHP > 0; } + virtual sNPCAppearanceData getAppearanceData() override; + + 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, BuffClass buffClass) override; + virtual void clearBuffs(bool force) override; + virtual bool hasBuff(int buffId) override; + virtual int getCompositeCondition() override; + virtual int takeDamage(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); }; // Mob is in MobAI.hpp, Player is in Player.hpp -// TODO: decouple from BaseNPC struct Egg : public BaseNPC { bool summoned = false; bool dead = false; time_t deadUntil; - Egg(int x, int y, int z, uint64_t iID, int t, int32_t id, bool summon) - : BaseNPC(x, y, z, 0, iID, t, id) { + Egg(uint64_t iID, int t, int32_t id, bool summon) + : BaseNPC(0, iID, t, id) { summoned = summon; - type = EntityType::EGG; + kind = EntityKind::EGG; } - virtual bool isAlive() override { return !dead; } + virtual bool isExtant() override { return !dead; } virtual void enterIntoViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override; }; -// TODO: decouple from BaseNPC struct Bus : public BaseNPC { - Bus(int x, int y, int z, int angle, uint64_t iID, int t, int id) : - BaseNPC(x, y, z, angle, iID, t, id) { - type = EntityType::BUS; + Bus(int angle, uint64_t iID, int t, int id) : + BaseNPC(angle, iID, t, id) { + kind = EntityKind::BUS; loopingPath = true; } 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 2fe7dfb..ac58d6e 100644 --- a/src/Groups.cpp +++ b/src/Groups.cpp @@ -1,13 +1,10 @@ -#include "servers/CNShardServer.hpp" -#include "PlayerManager.hpp" #include "Groups.hpp" -#include "Nanos.hpp" -#include "Abilities.hpp" -#include -#include -#include -#include +#include "servers/CNShardServer.hpp" + +#include "Player.hpp" +#include "PlayerManager.hpp" +#include "Entities.hpp" /* * NOTE: Variadic response packets that list group members are technically @@ -19,22 +16,177 @@ using namespace Groups; +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; + } + else if (member.kind == EntityKind::COMBAT_NPC) { + CombatNPC* npc = (CombatNPC*)member.getEntity(); + npc->group = group; + } + else { + std::cout << "[WARN] Adding a weird entity type to a group" << std::endl; + } + + 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); + } + } +} + +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] 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; + } else { + 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(); + + 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!! + bool done = false; + while(!done) { + EntityRef back = group->members.back(); + done = removeFromGroup(group, back); + } +} + static void requestGroup(CNSocket* sock, CNPacketData* data) { sP_CL2FE_REQ_PC_GROUP_INVITE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE*)data->buf; Player* plr = PlayerManager::getPlayer(sock); Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_To); - if (otherPlr == nullptr) - return; - - otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup); - if (otherPlr == nullptr) return; // fail if the group is full or the other player is already in a group - if (plr->groupCnt >= 4 || otherPlr->iIDGroup != otherPlr->iID || otherPlr->groupCnt > 1) { + if ((plr->group != nullptr && plr->group->filter(EntityKind::PLAYER).size() >= 4) || otherPlr->group != nullptr) { INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_FAIL, resp); sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_FAIL, sizeof(sP_FE2CL_PC_GROUP_INVITE_FAIL)); return; @@ -75,255 +227,80 @@ static void joinGroup(CNSocket* sock, CNPacketData* data) { Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_From); if (otherPlr == nullptr) - return; + return; // disconnect or something - otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup); - - if (otherPlr == nullptr) - return; + int size = otherPlr->group == nullptr ? 1 : otherPlr->group->filter(EntityKind::PLAYER).size(); // fail if the group is full or the other player is already in a group - if (plr->groupCnt > 1 || plr->iIDGroup != plr->iID || otherPlr->groupCnt >= 4) { + if (plr->group != nullptr || size + 1 > 4) { INITSTRUCT(sP_FE2CL_PC_GROUP_JOIN_FAIL, resp); sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_JOIN_FAIL, sizeof(sP_FE2CL_PC_GROUP_JOIN_FAIL)); return; } - if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), otherPlr->groupCnt + 1, sizeof(sPCGroupMemberInfo))) { - std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n"; - return; + if (otherPlr->group == nullptr) { + // create group + EntityRef otherPlrRef = PlayerManager::getSockFromID(recv->iID_From); + otherPlr->group = new Group(otherPlrRef); } - - plr->iIDGroup = otherPlr->iID; - otherPlr->groupCnt += 1; - otherPlr->groupIDs[otherPlr->groupCnt-1] = plr->iID; - - size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + otherPlr->groupCnt * 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 = otherPlr->groupCnt; - - int bitFlag = getGroupFlags(otherPlr); - - for (int i = 0; i < otherPlr->groupCnt; i++) { - Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]); - CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]); - - 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 - if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3) - Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 1, 1, bitFlag); - if (Nanos::SkillTable[plr->Nanos[plr->activeNano].iSkillID].targetType == 3) - Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 1, 1, bitFlag); - } - } - - sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_JOIN, resplen); + addToGroup(otherPlr->group, sock); } static void leaveGroup(CNSocket* sock, CNPacketData* data) { Player* plr = PlayerManager::getPlayer(sock); - groupKickPlayer(plr); + groupKick(plr->group, sock); } -void Groups::sendToGroup(Player* plr, void* buf, uint32_t type, size_t size) { - for (int i = 0; i < plr->groupCnt; i++) { - CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[i]); - - if (sock == nullptr) - continue; - - if (type == P_FE2CL_PC_GROUP_LEAVE_SUCC) { - Player* leavingPlr = PlayerManager::getPlayer(sock); - leavingPlr->iIDGroup = leavingPlr->iID; - } - - sock->sendPacket(buf, type, size); +void Groups::sendToGroup(Group* group, void* buf, uint32_t type, size_t size) { + auto players = group->filter(EntityKind::PLAYER); + for (EntityRef ref : players) { + ref.sock->sendPacket(buf, type, size); } } -void Groups::groupTickInfo(Player* plr) { - if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), plr->groupCnt, 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) + plr->groupCnt * 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 = plr->groupCnt; - - for (int i = 0; i < plr->groupCnt; i++) { - Player* varPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]); - - 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, (void*)&respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO, resplen); -} - -static void groupUnbuff(Player* plr) { - for (int i = 0; i < plr->groupCnt; i++) { - for (int n = 0; n < plr->groupCnt; n++) { - if (i == n) - continue; - - Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]); - CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[n]); - - Nanos::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::groupKickPlayer(Player* plr) { +void Groups::groupKick(Group* group, EntityRef ref) { + // if you are the group leader, destroy your own group and kick everybody - if (plr->iID == plr->iIDGroup) { - groupUnbuff(plr); - INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1); - sendToGroup(plr, (void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC)); - plr->groupCnt = 1; + if (group->members[0] == ref) { + disbandGroup(group); return; } - Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup); - - if (otherPlr == nullptr) - return; - - if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), otherPlr->groupCnt - 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) + (otherPlr->groupCnt - 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 = otherPlr->groupCnt - 1; - - int bitFlag = getGroupFlags(otherPlr) & ~plr->iGroupConditionBitFlag; - int moveDown = 0; - - CNSocket* sock = PlayerManager::getSockFromID(plr->iID); - - if (sock == nullptr) - return; - - for (int i = 0; i < otherPlr->groupCnt; i++) { - Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]); - CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]); - - if (varPlr == nullptr || sockTo == nullptr) - continue; - - if (moveDown == 1) - otherPlr->groupIDs[i-1] = otherPlr->groupIDs[i]; - - respdata[i-moveDown].iPC_ID = varPlr->iID; - respdata[i-moveDown].iPCUID = varPlr->PCStyle.iPC_UID; - respdata[i-moveDown].iNameCheck = varPlr->PCStyle.iNameCheck; - memcpy(respdata[i-moveDown].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName)); - memcpy(respdata[i-moveDown].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName)); - respdata[i-moveDown].iSpecialState = varPlr->iSpecialState; - respdata[i-moveDown].iLv = varPlr->level; - respdata[i-moveDown].iHP = varPlr->HP; - respdata[i-moveDown].iMaxHP = PC_MAXHEALTH(varPlr->level); - // respdata[i-moveDown]].iMapType = 0; - // respdata[i-moveDown]].iMapNum = 0; - respdata[i-moveDown].iX = varPlr->x; - respdata[i-moveDown].iY = varPlr->y; - respdata[i-moveDown].iZ = varPlr->z; - // client doesnt read nano data here - - if (varPlr == plr) { - moveDown = 1; - otherPlr->groupIDs[i] = 0; - } else { // remove the leaving member's buffs from the group and remove the group buffs from the leaving member. - if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3) - Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 2, 1, 0); - if (Nanos::SkillTable[plr->Nanos[varPlr->activeNano].iSkillID].targetType == 3) - Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 2, 1, bitFlag); - } - } - - plr->iIDGroup = plr->iID; - otherPlr->groupCnt -= 1; - - sendToGroup(otherPlr, (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)); -} - -int Groups::getGroupFlags(Player* plr) { - int bitFlag = 0; - - for (int i = 0; i < plr->groupCnt; i++) { - Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]); - - if (otherPlr == nullptr) - continue; - - bitFlag |= otherPlr->iGroupConditionBitFlag; - } - - return bitFlag; + removeFromGroup(group, ref); } void Groups::init() { diff --git a/src/Groups.hpp b/src/Groups.hpp index b12f9f9..4fe40f5 100644 --- a/src/Groups.hpp +++ b/src/Groups.hpp @@ -1,17 +1,37 @@ #pragma once -#include "Player.hpp" -#include "core/Core.hpp" -#include "servers/CNShardServer.hpp" +#include "EntityRef.hpp" -#include -#include +#include +#include + +struct Group { + std::vector members; + + std::vector filter(EntityKind kind) { + std::vector filtered; + std::copy_if(members.begin(), members.end(), std::back_inserter(filtered), [kind](EntityRef e) { + return e.kind == kind; + }); + return filtered; + } + EntityRef getLeader() { + assert(members.size() > 0); + return members[0]; + } + + Group(EntityRef leader); +}; namespace Groups { void init(); - void sendToGroup(Player* plr, void* buf, uint32_t type, size_t size); - void groupTickInfo(Player* plr); - void groupKickPlayer(Player* plr); - int getGroupFlags(Player* plr); + void sendToGroup(Group* group, void* buf, uint32_t type, size_t size); + void sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size); + void groupTickInfo(CNSocket* sock); + + 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 a3618dc..70a6b9e 100644 --- a/src/Items.cpp +++ b/src/Items.cpp @@ -1,16 +1,19 @@ -#include "servers/CNShardServer.hpp" #include "Items.hpp" + +#include "servers/CNShardServer.hpp" + +#include "Player.hpp" #include "PlayerManager.hpp" #include "Nanos.hpp" -#include "NPCManager.hpp" -#include "Player.hpp" #include "Abilities.hpp" -#include "Missions.hpp" #include "Eggs.hpp" -#include "Rand.hpp" +#include "MobAI.hpp" +#include "Missions.hpp" +#include "Buffs.hpp" #include // for memset() #include +#include using namespace Items; @@ -479,30 +482,34 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) { resp->iSlotNum = request->iSlotNum; resp->RemainItem = gumball; resp->iTargetCnt = 1; - resp->eST = EST_NANOSTIMPAK; + resp->eST = (int32_t)SkillType::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)Nanos::SkillTable[144].durationTime[0] * 100; - Eggs::EggBuffs[key] = until; } static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) { @@ -755,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; @@ -770,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; @@ -822,12 +829,12 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled) { // sanity check - if (Items::MobToDropMap.find(mob->appearanceData.iNPCType) == Items::MobToDropMap.end()) { - std::cout << "[WARN] Mob ID " << mob->appearanceData.iNPCType << " has no drops assigned" << std::endl; + if (Items::MobToDropMap.find(mob->type) == Items::MobToDropMap.end()) { + std::cout << "[WARN] Mob ID " << mob->type << " has no drops assigned" << std::endl; return; } // find mob drop id - int mobDropId = Items::MobToDropMap[mob->appearanceData.iNPCType]; + int mobDropId = Items::MobToDropMap[mob->type]; giveSingleDrop(sock, mob, mobDropId, rolled); diff --git a/src/Items.hpp b/src/Items.hpp index 97c7733..2156b92 100644 --- a/src/Items.hpp +++ b/src/Items.hpp @@ -1,9 +1,13 @@ #pragma once -#include "servers/CNShardServer.hpp" +#include "core/Core.hpp" + #include "Player.hpp" -#include "MobAI.hpp" #include "Rand.hpp" +#include "MobAI.hpp" + +#include +#include struct CrocPotEntry { int multStats, multLooks; diff --git a/src/Missions.cpp b/src/Missions.cpp index 9d2dd69..b13699a 100644 --- a/src/Missions.cpp +++ b/src/Missions.cpp @@ -1,11 +1,10 @@ -#include "servers/CNShardServer.hpp" #include "Missions.hpp" -#include "PlayerManager.hpp" -#include "Nanos.hpp" -#include "Items.hpp" -#include "Transport.hpp" -#include "string.h" +#include "servers/CNShardServer.hpp" + +#include "PlayerManager.hpp" +#include "Items.hpp" +#include "Nanos.hpp" using namespace Missions; @@ -164,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; @@ -367,15 +366,15 @@ static void taskStart(CNSocket* sock, CNPacketData* data) { sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC)); // if escort task, assign matching paths to all nearby NPCs - if (task["m_iHTaskType"] == 6) { + if (task["m_iHTaskType"] == (int)eTaskTypeProperty::EscortDefence) { for (ChunkPos& chunkPos : Chunking::getChunksInMap(plr->instanceID)) { // check all NPCs in the instance Chunk* chunk = Chunking::chunks[chunkPos]; for (EntityRef ref : chunk->entities) { - if (ref.type != EntityType::PLAYER) { + if (ref.kind != EntityKind::PLAYER) { BaseNPC* npc = (BaseNPC*)ref.getEntity(); - NPCPath* path = Transport::findApplicablePath(npc->appearanceData.iNPC_ID, npc->appearanceData.iNPCType, missionData->iTaskNum); + NPCPath* path = Transport::findApplicablePath(npc->id, npc->type, missionData->iTaskNum); if (path != nullptr) { - Transport::constructPathNPC(npc->appearanceData.iNPC_ID, path); + Transport::constructPathNPC(npc->id, path); return; } } @@ -399,7 +398,7 @@ static void taskEnd(CNSocket* sock, CNPacketData* data) { * once we comb over mission logic more throughly */ bool mobsAreKilled = false; - if (task->task["m_iHTaskType"] == 5) { + if (task->task["m_iHTaskType"] == (int)eTaskTypeProperty::Defeat) { mobsAreKilled = true; for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) { if (plr->tasks[i] == missionData->iTaskNum) { diff --git a/src/Missions.hpp b/src/Missions.hpp index 4e572a6..46eaa4c 100644 --- a/src/Missions.hpp +++ b/src/Missions.hpp @@ -1,9 +1,11 @@ #pragma once -#include "servers/CNShardServer.hpp" +#include "core/Core.hpp" +#include "JSON.hpp" + #include "Player.hpp" -#include "JSON.hpp" +#include struct Reward { int32_t id; diff --git a/src/MobAI.cpp b/src/MobAI.cpp index c31e19c..1d5b8f2 100644 --- a/src/MobAI.cpp +++ b/src/MobAI.cpp @@ -1,10 +1,15 @@ #include "MobAI.hpp" -#include "Player.hpp" + +#include "Chunking.hpp" +#include "NPCManager.hpp" +#include "Entities.hpp" +#include "PlayerManager.hpp" #include "Racing.hpp" -#include "Transport.hpp" #include "Nanos.hpp" -#include "Combat.hpp" #include "Abilities.hpp" +#include "Combat.hpp" +#include "Items.hpp" +#include "Missions.hpp" #include "Rand.hpp" #include @@ -14,7 +19,51 @@ using namespace MobAI; bool MobAI::simulateMobs = settings::SIMULATEMOBS; -static void roamingStep(Mob *mob, time_t currTime); +void Mob::step(time_t currTime) { + if (playersInView < 0) + std::cout << "[WARN] Weird playerview value " << playersInView << std::endl; + + // skip movement and combat if disabled or not in view + if ((!MobAI::simulateMobs || playersInView == 0) && state != AIState::DEAD + && state != AIState::RETREAT) + return; + + // call superclass step + CombatNPC::step(currTime); +} + +int Mob::takeDamage(EntityRef src, int amt) { + + // cannot kill mobs multiple times; cannot harm retreating mobs + if (state != AIState::ROAMING && state != AIState::COMBAT) { + return 0; // no damage + } + + if (skillStyle >= 0) + return 0; // don't hurt a mob casting corruption + + if (state == AIState::ROAMING) { + assert(target == nullptr && src.kind == EntityKind::PLAYER); // TODO: players only for now + transition(AIState::COMBAT, src); + + if (groupLeader != 0) + MobAI::followToCombat(this); + } + + // wake up sleeping monster + if (hasBuff(ECSB_MEZ)) { + removeBuff(ECSB_MEZ); + + INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); + pkt1.eCT = 2; + pkt1.iID = id; + pkt1.iConditionBitFlag = getCompositeCondition(); + NPCManager::sendToViewable(this, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); + } + + // call superclass takeDamage + return CombatNPC::takeDamage(src, amt); +} /* * Dynamic lerp; distinct from Transport::lerp(). This one doesn't care about height and @@ -47,44 +96,43 @@ static std::pair lerp(int x1, int y1, int x2, int y2, int speed) { void MobAI::clearDebuff(Mob *mob) { mob->skillStyle = -1; - mob->appearanceData.iConditionBitFlag = 0; - mob->unbuffTimes.clear(); + mob->clearBuffs(false); INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); pkt1.eCT = 2; - pkt1.iID = mob->appearanceData.iNPC_ID; - pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag; + pkt1.iID = mob->id; + pkt1.iConditionBitFlag = mob->getCompositeCondition(); NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); } void MobAI::followToCombat(Mob *mob) { - if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->type == EntityType::MOB) { + if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->kind == EntityKind::MOB) { Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader]; for (int i = 0; i < 4; i++) { if (leadMob->groupMember[i] == 0) break; - if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->type != EntityType::MOB) { + if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->kind != EntityKind::MOB) { std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl; continue; } Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]]; - if (followerMob->state != MobState::ROAMING) // only roaming mobs should transition to combat + if (followerMob->state != AIState::ROAMING) // only roaming mobs should transition to combat continue; - enterCombat(mob->target, followerMob); + followerMob->transition(AIState::COMBAT, mob->target); } - if (leadMob->state != MobState::ROAMING) + if (leadMob->state != AIState::ROAMING) return; - enterCombat(mob->target, leadMob); + leadMob->transition(AIState::COMBAT, mob->target); } } void MobAI::groupRetreat(Mob *mob) { - if (NPCManager::NPCs.find(mob->groupLeader) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupLeader]->type != EntityType::MOB) + if (NPCManager::NPCs.find(mob->groupLeader) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupLeader]->kind != EntityKind::MOB) return; Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader]; @@ -92,25 +140,25 @@ void MobAI::groupRetreat(Mob *mob) { if (leadMob->groupMember[i] == 0) break; - if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->type != EntityType::MOB) { + if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->kind != EntityKind::MOB) { std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl; continue; } Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]]; - if (followerMob->state != MobState::COMBAT) + if (followerMob->state != AIState::COMBAT) continue; followerMob->target = nullptr; - followerMob->state = MobState::RETREAT; + followerMob->state = AIState::RETREAT; clearDebuff(followerMob); } - if (leadMob->state != MobState::COMBAT) + if (leadMob->state != AIState::COMBAT) return; leadMob->target = nullptr; - leadMob->state = MobState::RETREAT; + leadMob->state = AIState::RETREAT; clearDebuff(leadMob); } @@ -127,7 +175,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) { Chunk* chunk = *it; for (const EntityRef& ref : chunk->entities) { // TODO: support targetting other CombatNPCs - if (ref.type != EntityType::PLAYER) + if (ref.kind != EntityKind::PLAYER) continue; CNSocket *s = ref.sock; @@ -138,7 +186,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; @@ -147,7 +195,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) { if (levelDifference > -10) mobRange = levelDifference < 10 ? mobRange - (levelDifference * mobRange / 15) : mobRange / 3; - if (mob->state != MobState::ROAMING && plr->inCombat) // freshly out of aggro mobs + if (mob->state != AIState::ROAMING && plr->inCombat) // freshly out of aggro mobs mobRange = mob->sightRange * 2; // should not be impacted by the above if (plr->iSpecialState & (CN_SPECIAL_STATE_FLAG__INVISIBLE|CN_SPECIAL_STATE_FLAG__INVULNERABLE)) @@ -168,7 +216,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) { if (closest != nullptr) { // found closest player. engage. - enterCombat(closest, mob); + mob->transition(AIState::COMBAT, closest); if (mob->groupLeader != 0) followToCombat(mob); @@ -196,7 +244,7 @@ static void dealCorruption(Mob *mob, std::vector targetData, int skillID, i sP_FE2CL_NPC_SKILL_CORRUPTION_HIT *resp = (sP_FE2CL_NPC_SKILL_CORRUPTION_HIT*)respbuf; sCAttackResult *respdata = (sCAttackResult*)(respbuf+sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT)); - resp->iNPC_ID = mob->appearanceData.iNPC_ID; + resp->iNPC_ID = mob->id; resp->iSkillID = skillID; resp->iStyle = style; resp->iValue1 = plr->x; @@ -234,26 +282,29 @@ static void dealCorruption(Mob *mob, std::vector targetData, int skillID, i int style2 = Nanos::nanoStyle(plr->activeNano); if (style2 == -1) { // no nano - respdata[i].iHitFlag = 8; - respdata[i].iDamage = Nanos::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500; + respdata[i].iHitFlag = HF_BIT_STYLE_TIE; + respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500; } else if (style == style2) { - respdata[i].iHitFlag = 8; // tie + respdata[i].iHitFlag = HF_BIT_STYLE_TIE; respdata[i].iDamage = 0; respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina; } else if (style - style2 == 1 || style2 - style == 2) { - respdata[i].iHitFlag = 4; // win + respdata[i].iHitFlag = HF_BIT_STYLE_WIN; respdata[i].iDamage = 0; respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina += 45; if (plr->Nanos[plr->activeNano].iStamina > 150) respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 150; // fire damage power disguised as a corruption attack back at the enemy - std::vector targetData2 = {1, mob->appearanceData.iNPC_ID, 0, 0, 0}; - for (auto& pwr : Nanos::NanoPowers) - if (pwr.skillType == EST_DAMAGE) - pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200); + SkillData skill = Abilities::SkillTable[skillID]; + skill.durationTime[0] = 0; + skill.values[0][0] = 200; // have to set + skill.values[0][1] = 200; // all of these + skill.values[0][2] = 200; // because the player might + skill.values[0][3] = 200; // have a boost + Abilities::useNanoSkill(sock, &skill, *plr->getActiveNano(), { mob }); } else { - respdata[i].iHitFlag = 16; // lose - respdata[i].iDamage = Nanos::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500; + respdata[i].iHitFlag = HF_BIT_STYLE_LOSE; + respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500; respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina -= 90; if (plr->Nanos[plr->activeNano].iStamina < 0) { respdata[i].bNanoDeactive = 1; @@ -265,16 +316,11 @@ 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) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!aggroCheck(mob, getTime())) { - clearDebuff(mob); - if (mob->groupLeader != 0) - groupRetreat(mob); - } + if (!MobAI::aggroCheck(mob, getTime())) + mob->transition(AIState::RETREAT, mob->target); } } @@ -282,12 +328,6 @@ static void dealCorruption(Mob *mob, std::vector targetData, int skillID, i } static void useAbilities(Mob *mob, time_t currTime) { - /* - * targetData approach - * first integer is the count - * second to fifth integers are IDs, these can be either player iID or mob's iID - * whether the skill targets players or mobs is determined by the skill packet being fired - */ Player *plr = PlayerManager::getPlayer(mob->target); if (mob->skillStyle >= 0) { // corruption hit @@ -302,35 +342,32 @@ static void useAbilities(Mob *mob, time_t currTime) { if (mob->skillStyle == -2) { // eruption hit int skillID = (int)mob->data["m_iMegaType"]; - std::vector targetData = {0, 0, 0, 0, 0}; + std::vector targets{}; // find the players within range of eruption for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) { Chunk* chunk = *it; for (const EntityRef& ref : chunk->entities) { // TODO: see aggroCheck() - if (ref.type != EntityType::PLAYER) + if (ref.kind != EntityKind::PLAYER) continue; - CNSocket *s= ref.sock; + CNSocket *s = ref.sock; Player *plr = PlayerManager::getPlayer(s); - if (plr->HP <= 0) + if (!plr->isAlive()) continue; int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y); - if (distance < Nanos::SkillTable[skillID].effectArea) { - targetData[0] += 1; - targetData[targetData[0]] = plr->iID; - if (targetData[0] > 3) // make sure not to have more than 4 + if (distance < Abilities::SkillTable[skillID].effectArea) { + targets.push_back(plr); + if (targets.size() > 3) // make sure not to have more than 4 break; } } } - for (auto& pwr : Combat::MobPowers) - if (pwr.skillType == Nanos::SkillTable[skillID].skillType) - pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]); + Abilities::useNPCSkill(mob->id, skillID, targets); mob->skillStyle = -3; // eruption cooldown mob->nextAttack = currTime + 1000; return; @@ -348,13 +385,11 @@ static void useAbilities(Mob *mob, time_t currTime) { if (random < prob1) { // active skill hit int skillID = (int)mob->data["m_iActiveSkill1"]; - std::vector targetData = {1, plr->iID, 0, 0, 0}; - for (auto& pwr : Combat::MobPowers) - if (pwr.skillType == Nanos::SkillTable[skillID].skillType) { - if (pwr.bitFlag != 0 && (plr->iConditionBitFlag & pwr.bitFlag)) - return; // prevent debuffing a player twice - pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]); - } + SkillData* skill = &Abilities::SkillTable[skillID]; + int debuffID = Abilities::getCSTBFromST(skill->skillType); + if(plr->hasBuff(debuffID)) + return; // prevent debuffing a player twice + Abilities::useNPCSkill(mob->getRef(), skillID, { plr }); mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100; return; } @@ -362,7 +397,7 @@ static void useAbilities(Mob *mob, time_t currTime) { if (random < prob1 + prob2) { // corruption windup int skillID = (int)mob->data["m_iCorruptionType"]; INITSTRUCT(sP_FE2CL_NPC_SKILL_CORRUPTION_READY, pkt); - pkt.iNPC_ID = mob->appearanceData.iNPC_ID; + pkt.iNPC_ID = mob->id; pkt.iSkillID = skillID; pkt.iValue1 = plr->x; pkt.iValue2 = plr->y; @@ -381,7 +416,7 @@ static void useAbilities(Mob *mob, time_t currTime) { if (random < prob1 + prob2 + prob3) { // eruption windup int skillID = (int)mob->data["m_iMegaType"]; INITSTRUCT(sP_FE2CL_NPC_SKILL_READY, pkt); - pkt.iNPC_ID = mob->appearanceData.iNPC_ID; + pkt.iNPC_ID = mob->id; pkt.iSkillID = skillID; pkt.iValue1 = mob->hitX = plr->x; pkt.iValue2 = mob->hitY = plr->y; @@ -395,95 +430,58 @@ static void useAbilities(Mob *mob, time_t currTime) { return; } -void MobAI::enterCombat(CNSocket *sock, Mob *mob) { - mob->target = sock; - mob->state = MobState::COMBAT; - mob->nextMovement = getTime(); - mob->nextAttack = 0; +void MobAI::incNextMovement(Mob* mob, time_t currTime) { + if (currTime == 0) + currTime = getTime(); - mob->roamX = mob->x; - mob->roamY = mob->y; - mob->roamZ = mob->z; - - int skillID = (int)mob->data["m_iPassiveBuff"]; // cast passive - std::vector targetData = {1, mob->appearanceData.iNPC_ID, 0, 0, 0}; - for (auto& pwr : Combat::MobPowers) - if (pwr.skillType == Nanos::SkillTable[skillID].skillType) - pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]); - - for (NPCEvent& event : NPCManager::NPCEvents) // trigger an ON_COMBAT - if (event.trigger == ON_COMBAT && event.npcType == mob->appearanceData.iNPCType) - event.handler(sock, mob); + int delay = (int)mob->data["m_iDelayTime"] * 1000; + mob->nextMovement = currTime + delay / 2 + Rand::rand(delay / 2); } -static void drainMobHP(Mob *mob, int amount) { - size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage); - assert(resplen < CN_PACKET_BUFFER_SIZE - 8); - uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; +void MobAI::deadStep(CombatNPC* npc, time_t currTime) { + Mob* self = (Mob*)npc; - memset(respbuf, 0, resplen); - - sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf; - sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK)); - - pkt->iID = mob->appearanceData.iNPC_ID; - pkt->eCT = 4; // mob - pkt->iTB_ID = ECSB_BOUNDINGBALL; - - drain->eCT = 4; - drain->iID = mob->appearanceData.iNPC_ID; - drain->iDamage = amount; - drain->iHP = mob->appearanceData.iHP -= amount; - - NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen); - - if (mob->appearanceData.iHP <= 0) - Combat::killMob(mob->target, mob); -} - -static void deadStep(Mob *mob, time_t currTime) { // despawn the mob after a short delay - if (mob->killedTime != 0 && !mob->despawned && currTime - mob->killedTime > 2000) { - mob->despawned = true; + if (self->killedTime != 0 && !self->despawned && currTime - self->killedTime > 2000) { + self->despawned = true; INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt); - pkt.iNPC_ID = mob->appearanceData.iNPC_ID; + pkt.iNPC_ID = self->id; - NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT)); + NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT)); // if it was summoned, mark it for removal - if (mob->summoned) { + if (self->summoned) { std::cout << "[INFO] Queueing killed summoned mob for removal" << std::endl; - NPCManager::queueNPCRemoval(mob->appearanceData.iNPC_ID); + NPCManager::queueNPCRemoval(self->id); return; } // pre-set spawn coordinates if not marked for removal - mob->x = mob->spawnX; - mob->y = mob->spawnY; - mob->z = mob->spawnZ; + self->x = self->spawnX; + self->y = self->spawnY; + self->z = self->spawnZ; } // to guide their groupmates, group leaders still need to move despite being dead - if (mob->groupLeader == mob->appearanceData.iNPC_ID) - roamingStep(mob, currTime); + if (self->groupLeader == self->id) + roamingStep(self, currTime); - if (mob->killedTime != 0 && currTime - mob->killedTime < mob->regenTime * 100) + if (self->killedTime != 0 && currTime - self->killedTime < self->regenTime * 100) return; - std::cout << "respawning mob " << mob->appearanceData.iNPC_ID << " with HP = " << mob->maxHealth << std::endl; + std::cout << "respawning mob " << self->id << " with HP = " << self->maxHealth << std::endl; - mob->appearanceData.iHP = mob->maxHealth; - mob->state = MobState::ROAMING; + self->transition(AIState::ROAMING, self->id); // if mob is a group leader/follower, spawn where the group is. - if (mob->groupLeader != 0) { - if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->type == EntityType::MOB) { - Mob* leaderMob = (Mob*)NPCManager::NPCs[mob->groupLeader]; - mob->x = leaderMob->x + mob->offsetX; - mob->y = leaderMob->y + mob->offsetY; - mob->z = leaderMob->z; + if (self->groupLeader != 0) { + if (NPCManager::NPCs.find(self->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[self->groupLeader]->kind == EntityKind::MOB) { + Mob* leaderMob = (Mob*)NPCManager::NPCs[self->groupLeader]; + self->x = leaderMob->x + self->offsetX; + self->y = leaderMob->y + self->offsetY; + self->z = leaderMob->z; } else { std::cout << "[WARN] deadStep: mob cannot find it's leader!" << std::endl; } @@ -491,180 +489,147 @@ static void deadStep(Mob *mob, time_t currTime) { INITSTRUCT(sP_FE2CL_NPC_NEW, pkt); - pkt.NPCAppearanceData = mob->appearanceData; - pkt.NPCAppearanceData.iX = mob->x; - pkt.NPCAppearanceData.iY = mob->y; - pkt.NPCAppearanceData.iZ = mob->z; + pkt.NPCAppearanceData = self->getAppearanceData(); // notify all nearby players - NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW)); + NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW)); } -static void combatStep(Mob *mob, time_t currTime) { - assert(mob->target != nullptr); +void MobAI::combatStep(CombatNPC* npc, time_t currTime) { + Mob* self = (Mob*)npc; + assert(self->target != nullptr); // lose aggro if the player lost connection - if (PlayerManager::players.find(mob->target) == PlayerManager::players.end()) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!aggroCheck(mob, currTime)) { - clearDebuff(mob); - if (mob->groupLeader != 0) - groupRetreat(mob); - } + if (PlayerManager::players.find(self->target) == PlayerManager::players.end()) { + if (!MobAI::aggroCheck(self, getTime())) + self->transition(AIState::RETREAT, self->target); return; } - Player *plr = PlayerManager::getPlayer(mob->target); + Player *plr = PlayerManager::getPlayer(self->target); // lose aggro if the player became invulnerable or died if (plr->HP <= 0 || (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!aggroCheck(mob, currTime)) { - clearDebuff(mob); - if (mob->groupLeader != 0) - groupRetreat(mob); - } + if (!MobAI::aggroCheck(self, getTime())) + self->transition(AIState::RETREAT, self->target); return; } - // drain - if (mob->skillStyle < 0 && (mob->lastDrainTime == 0 || currTime - mob->lastDrainTime >= 1000) - && mob->appearanceData.iConditionBitFlag & CSB_BIT_BOUNDINGBALL) { - drainMobHP(mob, mob->maxHealth / 20); // lose 5% every second - mob->lastDrainTime = currTime; - } + // tick buffs + auto it = npc->buffs.begin(); + while(it != npc->buffs.end()) { + Buff* buff = (*it).second; + buff->combatTick(currTime); - // if drain killed the mob, return early - if (mob->appearanceData.iHP <= 0) - return; + // if mob state changed, end the step + if(self->state != AIState::COMBAT) + return; - // unbuffing - std::unordered_map::iterator it = mob->unbuffTimes.begin(); - while (it != mob->unbuffTimes.end()) { - - if (currTime >= it->second) { - mob->appearanceData.iConditionBitFlag &= ~it->first; - - INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); - pkt1.eCT = 2; - pkt1.iID = mob->appearanceData.iNPC_ID; - pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag; - NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); - - it = mob->unbuffTimes.erase(it); - } else { - it++; + buff->tick(currTime); + if(buff->isStale()) { + // garbage collect + it = npc->buffs.erase(it); + delete buff; } + else it++; } // skip attack if stunned or asleep - if (mob->appearanceData.iConditionBitFlag & (CSB_BIT_STUN|CSB_BIT_MEZ)) { - mob->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up. + if (self->hasBuff(ECSB_STUN) || self->hasBuff(ECSB_MEZ)) { + self->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up. return; } - int distance = hypot(plr->x - mob->x, plr->y - mob->y); - int mobRange = (int)mob->data["m_iAtkRange"] + (int)mob->data["m_iRadius"]; + int distance = hypot(plr->x - self->x, plr->y - self->y); + int mobRange = (int)self->data["m_iAtkRange"] + (int)self->data["m_iRadius"]; - if (currTime >= mob->nextAttack) { - if (mob->skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance. - useAbilities(mob, currTime); - if (mob->target == nullptr) + if (currTime >= self->nextAttack) { + if (self->skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance. + useAbilities(self, currTime); + if (self->target == nullptr) return; } int distanceToTravel = INT_MAX; - int speed = mob->speed; + int speed = self->speed; // movement logic: move when out of range but don't move while casting a skill - if (distance > mobRange && mob->skillStyle == -1) { - if (mob->nextMovement != 0 && currTime < mob->nextMovement) + if (distance > mobRange && self->skillStyle == -1) { + if (self->nextMovement != 0 && currTime < self->nextMovement) return; - mob->nextMovement = currTime + 400; - if (currTime >= mob->nextAttack) - mob->nextAttack = 0; + self->nextMovement = currTime + 400; + if (currTime >= self->nextAttack) + self->nextAttack = 0; // halve movement speed if snared - if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED) + if (self->hasBuff(ECSB_DN_MOVE_SPEED)) speed /= 2; int targetX = plr->x; int targetY = plr->y; - if (mob->groupLeader != 0) { - targetX += mob->offsetX*distance/(mob->idleRange + 1); - targetY += mob->offsetY*distance/(mob->idleRange + 1); + if (self->groupLeader != 0) { + targetX += self->offsetX*distance/(self->idleRange + 1); + targetY += self->offsetY*distance/(self->idleRange + 1); } distanceToTravel = std::min(distance-mobRange+1, speed*2/5); - auto targ = lerp(mob->x, mob->y, targetX, targetY, distanceToTravel); - if (distanceToTravel < speed*2/5 && currTime >= mob->nextAttack) - mob->nextAttack = 0; + auto targ = lerp(self->x, self->y, targetX, targetY, distanceToTravel); + if (distanceToTravel < speed*2/5 && currTime >= self->nextAttack) + self->nextAttack = 0; - NPCManager::updateNPCPosition(mob->appearanceData.iNPC_ID, targ.first, targ.second, mob->z, mob->instanceID, mob->appearanceData.iAngle); + NPCManager::updateNPCPosition(self->id, targ.first, targ.second, self->z, self->instanceID, self->angle); INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt); - pkt.iNPC_ID = mob->appearanceData.iNPC_ID; + pkt.iNPC_ID = self->id; pkt.iSpeed = speed; - pkt.iToX = mob->x = targ.first; - pkt.iToY = mob->y = targ.second; + pkt.iToX = self->x = targ.first; + pkt.iToY = self->y = targ.second; pkt.iToZ = plr->z; pkt.iMoveStyle = 1; // notify all nearby players - NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE)); + NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE)); } /* attack logic * 2/5 represents 400 ms which is the time interval mobs use per movement logic step * if the mob is one move interval away, we should just start attacking anyways. */ - if (distance <= mobRange || distanceToTravel < speed*2/5) { - if (mob->nextAttack == 0 || currTime >= mob->nextAttack) { - mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100; - Combat::npcAttackPc(mob, currTime); + if (distance <= mobRange || distanceToTravel < self->speed*2/5) { + if (self->nextAttack == 0 || currTime >= self->nextAttack) { + self->nextAttack = currTime + (int)self->data["m_iDelayTime"] * 100; + Combat::npcAttackPc(self, currTime); } } // retreat if the player leaves combat range - int xyDistance = hypot(plr->x - mob->roamX, plr->y - mob->roamY); - distance = hypot(xyDistance, plr->z - mob->roamZ); - if (distance >= mob->data["m_iCombatRange"]) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - clearDebuff(mob); - if (mob->groupLeader != 0) - groupRetreat(mob); + int xyDistance = hypot(plr->x - self->roamX, plr->y - self->roamY); + distance = hypot(xyDistance, plr->z - self->roamZ); + if (distance >= self->data["m_iCombatRange"]) { + self->transition(AIState::RETREAT, self->target); } } -void MobAI::incNextMovement(Mob *mob, time_t currTime) { - if (currTime == 0) - currTime = getTime(); +void MobAI::roamingStep(CombatNPC* npc, time_t currTime) { + Mob* self = (Mob*)npc; - int delay = (int)mob->data["m_iDelayTime"] * 1000; - mob->nextMovement = currTime + delay/2 + Rand::rand(delay/2); -} - -static void roamingStep(Mob *mob, time_t currTime) { /* * We reuse nextAttack to avoid scanning for players all the time, but to still * do so more often than if we waited for nextMovement (which is way too slow). * In the case of group leaders, this step will be called by dead mobs, so disable attack. */ - if (mob->state != MobState::DEAD && (mob->nextAttack == 0 || currTime >= mob->nextAttack)) { - mob->nextAttack = currTime + 500; - if (aggroCheck(mob, currTime)) + if (self->state != AIState::DEAD && (self->nextAttack == 0 || currTime >= self->nextAttack)) { + self->nextAttack = currTime + 500; + if (aggroCheck(self, currTime)) return; } // no random roaming if the mob already has a set path - if (mob->staticPath) + if (self->staticPath) return; - if (mob->groupLeader != 0 && mob->groupLeader != mob->appearanceData.iNPC_ID) // don't roam by yourself without group leader + if (self->groupLeader != 0 && self->groupLeader != self->id) // don't roam by yourself without group leader return; /* @@ -672,142 +637,208 @@ static void roamingStep(Mob *mob, time_t currTime) { * Transport::stepNPCPathing() (which ticks at a higher frequency than nextMovement), * so we don't have to check if there's already entries in the queue since we know there won't be. */ - if (mob->nextMovement != 0 && currTime < mob->nextMovement) + if (self->nextMovement != 0 && currTime < self->nextMovement) return; - incNextMovement(mob, currTime); + incNextMovement(self, currTime); - int xStart = mob->spawnX - mob->idleRange/2; - int yStart = mob->spawnY - mob->idleRange/2; - int speed = mob->speed; + int xStart = self->spawnX - self->idleRange/2; + int yStart = self->spawnY - self->idleRange/2; // some mobs don't move (and we mustn't divide/modulus by zero) - if (mob->idleRange == 0 || speed == 0) + if (self->idleRange == 0 || self->speed == 0) return; int farX, farY, distance; - int minDistance = mob->idleRange / 2; + int minDistance = self->idleRange / 2; // pick a random destination - farX = xStart + Rand::rand(mob->idleRange); - farY = yStart + Rand::rand(mob->idleRange); + farX = xStart + Rand::rand(self->idleRange); + farY = yStart + Rand::rand(self->idleRange); - distance = std::abs(std::max(farX - mob->x, farY - mob->y)); + distance = std::abs(std::max(farX - self->x, farY - self->y)); if (distance == 0) distance += 1; // hack to avoid FPE // if it's too short a walk, go further in that direction - farX = mob->x + (farX - mob->x) * minDistance / distance; - farY = mob->y + (farY - mob->y) * minDistance / distance; + farX = self->x + (farX - self->x) * minDistance / distance; + farY = self->y + (farY - self->y) * minDistance / distance; // but don't got out of bounds - farX = std::clamp(farX, xStart, xStart + mob->idleRange); - farY = std::clamp(farY, yStart, yStart + mob->idleRange); + farX = std::clamp(farX, xStart, xStart + self->idleRange); + farY = std::clamp(farY, yStart, yStart + self->idleRange); // halve movement speed if snared - if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED) - speed /= 2; + if (self->hasBuff(ECSB_DN_MOVE_SPEED)) + self->speed /= 2; std::queue queue; - Vec3 from = { mob->x, mob->y, mob->z }; - Vec3 to = { farX, farY, mob->z }; + Vec3 from = { self->x, self->y, self->z }; + Vec3 to = { farX, farY, self->z }; // add a route to the queue; to be processed in Transport::stepNPCPathing() - Transport::lerp(&queue, from, to, speed); - Transport::NPCQueues[mob->appearanceData.iNPC_ID] = queue; + Transport::lerp(&queue, from, to, self->speed); + Transport::NPCQueues[self->id] = queue; - if (mob->groupLeader != 0 && mob->groupLeader == mob->appearanceData.iNPC_ID) { + if (self->groupLeader != 0 && self->groupLeader == self->id) { // make followers follow this npc. for (int i = 0; i < 4; i++) { - if (mob->groupMember[i] == 0) + if (self->groupMember[i] == 0) break; - if (NPCManager::NPCs.find(mob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupMember[i]]->type != EntityType::MOB) { + if (NPCManager::NPCs.find(self->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[self->groupMember[i]]->kind != EntityKind::MOB) { std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl; continue; } std::queue queue2; - Mob* followerMob = (Mob*)NPCManager::NPCs[mob->groupMember[i]]; + Mob* followerMob = (Mob*)NPCManager::NPCs[self->groupMember[i]]; from = { followerMob->x, followerMob->y, followerMob->z }; to = { farX + followerMob->offsetX, farY + followerMob->offsetY, followerMob->z }; - Transport::lerp(&queue2, from, to, speed); - Transport::NPCQueues[followerMob->appearanceData.iNPC_ID] = queue2; + Transport::lerp(&queue2, from, to, self->speed); + Transport::NPCQueues[followerMob->id] = queue2; } } } -static void retreatStep(Mob *mob, time_t currTime) { - if (mob->nextMovement != 0 && currTime < mob->nextMovement) +void MobAI::retreatStep(CombatNPC* npc, time_t currTime) { + Mob* self = (Mob*)npc; + + if (self->nextMovement != 0 && currTime < self->nextMovement) return; - mob->nextMovement = currTime + 400; + self->nextMovement = currTime + 400; // distance between spawn point and current location - int distance = hypot(mob->x - mob->roamX, mob->y - mob->roamY); + int distance = hypot(self->x - self->roamX, self->y - self->roamY); //if (distance > mob->data["m_iIdleRange"]) { if (distance > 10) { INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt); - auto targ = lerp(mob->x, mob->y, mob->roamX, mob->roamY, (int)mob->speed*4/5); + auto targ = lerp(self->x, self->y, self->roamX, self->roamY, (int)self->speed*4/5); - pkt.iNPC_ID = mob->appearanceData.iNPC_ID; - pkt.iSpeed = (int)mob->speed * 2; - pkt.iToX = mob->x = targ.first; - pkt.iToY = mob->y = targ.second; - pkt.iToZ = mob->z = mob->spawnZ; + pkt.iNPC_ID = self->id; + pkt.iSpeed = (int)self->speed * 2; + pkt.iToX = self->x = targ.first; + pkt.iToY = self->y = targ.second; + pkt.iToZ = self->z = self->spawnZ; pkt.iMoveStyle = 1; // notify all nearby players - NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE)); + NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE)); } // if we got there //if (distance <= mob->data["m_iIdleRange"]) { if (distance <= 10) { // retreat back to the spawn point - mob->state = MobState::ROAMING; - mob->appearanceData.iHP = mob->maxHealth; - mob->killedTime = 0; - mob->nextAttack = 0; - mob->appearanceData.iConditionBitFlag = 0; - - // cast a return home heal spell, this is the right way(tm) - std::vector targetData = {1, 0, 0, 0, 0}; - for (auto& pwr : Combat::MobPowers) - if (pwr.skillType == Nanos::SkillTable[110].skillType) - pwr.handle(mob, targetData, 110, Nanos::SkillTable[110].durationTime[0], Nanos::SkillTable[110].powerIntensity[0]); - // clear outlying debuffs - clearDebuff(mob); + self->transition(AIState::ROAMING, self->id); } } -void MobAI::step(CombatNPC *npc, time_t currTime) { - assert(npc->type == EntityType::MOB); - auto mob = (Mob*)npc; +void MobAI::onRoamStart(CombatNPC* npc, EntityRef src) { + Mob* self = (Mob*)npc; - if (mob->playersInView < 0) - std::cout << "[WARN] Weird playerview value " << mob->playersInView << std::endl; + self->hp = self->maxHealth; + self->killedTime = 0; + self->nextAttack = 0; - // skip mob movement and combat if disabled or not in view - if ((!simulateMobs || mob->playersInView == 0) && mob->state != MobState::DEAD - && mob->state != MobState::RETREAT) + // cast a return home heal spell, this is the right way(tm) + Abilities::useNPCSkill(npc->getRef(), 110, { npc }); + + // clear outlying debuffs + clearDebuff(self); +} + +void MobAI::onCombatStart(CombatNPC* npc, EntityRef src) { + Mob* self = (Mob*)npc; + + assert(src.kind == EntityKind::PLAYER); + self->target = src.sock; + self->nextMovement = getTime(); + self->nextAttack = 0; + + self->roamX = self->x; + self->roamY = self->y; + self->roamZ = self->z; + + int skillID = (int)self->data["m_iPassiveBuff"]; + if(skillID != 0) // cast passive + Abilities::useNPCSkill(npc->getRef(), skillID, { npc }); +} + +void MobAI::onRetreat(CombatNPC* npc, EntityRef src) { + Mob* self = (Mob*)npc; + + self->target = nullptr; + MobAI::clearDebuff(self); + if (self->groupLeader != 0) + MobAI::groupRetreat(self); +} + +void MobAI::onDeath(CombatNPC* npc, EntityRef src) { + Mob* self = (Mob*)npc; + + self->target = nullptr; + self->skillStyle = -1; + self->clearBuffs(true); + self->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step? + + // check for the edge case where hitting the mob did not aggro it + if (src.kind == EntityKind::PLAYER && src.isValid()) { + Player* plr = PlayerManager::getPlayer(src.sock); + + Items::DropRoll rolled; + Items::DropRoll eventRolled; + std::map qitemRolls; + std::vector playerRefs; + + if (plr->group == nullptr) { + playerRefs.push_back(plr); + Combat::genQItemRolls(playerRefs, qitemRolls); + Items::giveMobDrop(src.sock, self, rolled, eventRolled); + Missions::mobKilled(src.sock, self->type, qitemRolls); + } + 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); + + // only contribute to group members' kills if they're close enough + int dist = std::hypot(plr->x - otherPlr->x + 1, plr->y - otherPlr->y + 1); + if (dist > 5000) + continue; + + Items::giveMobDrop(sockTo, self, rolled, eventRolled); + Missions::mobKilled(sockTo, self->type, qitemRolls); + } + } + } + + // delay the despawn animation + self->despawned = false; + + auto it = Transport::NPCQueues.find(self->id); + if (it == Transport::NPCQueues.end() || it->second.empty()) return; - switch (mob->state) { - case MobState::INACTIVE: - // no-op - break; - case MobState::ROAMING: - roamingStep(mob, currTime); - break; - case MobState::COMBAT: - combatStep(mob, currTime); - break; - case MobState::RETREAT: - retreatStep(mob, currTime); - break; - case MobState::DEAD: - deadStep(mob, currTime); - break; + // rewind or empty the movement queue + if (self->staticPath) { + /* + * This is inelegant, but we wind forward in the path until we find the point that + * corresponds with the Mob's spawn point. + * + * IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever. + */ + auto& queue = it->second; + for (auto point = queue.front(); point.x != self->spawnX || point.y != self->spawnY; point = queue.front()) { + queue.pop(); + queue.push(point); + } + } + else { + Transport::NPCQueues.erase(self->id); } } diff --git a/src/MobAI.hpp b/src/MobAI.hpp index c526561..3e74d3f 100644 --- a/src/MobAI.hpp +++ b/src/MobAI.hpp @@ -1,26 +1,26 @@ #pragma once #include "core/Core.hpp" -#include "NPCManager.hpp" +#include "JSON.hpp" -enum class MobState { - INACTIVE, - ROAMING, - COMBAT, - RETREAT, - DEAD -}; +#include "Entities.hpp" + +#include +#include namespace MobAI { - // needs to be declared before Mob's constructor - void step(CombatNPC*, time_t); -}; + void deadStep(CombatNPC* self, time_t currTime); + void combatStep(CombatNPC* self, time_t currTime); + void roamingStep(CombatNPC* self, time_t currTime); + void retreatStep(CombatNPC* self, time_t currTime); + + void onRoamStart(CombatNPC* self, EntityRef src); + void onCombatStart(CombatNPC* self, EntityRef src); + void onRetreat(CombatNPC* self, EntityRef src); + void onDeath(CombatNPC* self, EntityRef src); +} struct Mob : public CombatNPC { - // general - MobState state = MobState::INACTIVE; - - std::unordered_map unbuffTimes = {}; // dead time_t killedTime = 0; @@ -47,16 +47,13 @@ struct Mob : public CombatNPC { int offsetX = 0, offsetY = 0; int groupMember[4] = {}; - // for optimizing away AI in empty chunks - int playersInView = 0; - // temporary; until we're sure what's what nlohmann::json data = {}; Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id) : CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]), sightRange(d["m_iSightRange"]) { - state = MobState::ROAMING; + state = AIState::ROAMING; data = d; @@ -65,20 +62,28 @@ struct Mob : public CombatNPC { idleRange = (int)data["m_iIdleRange"]; level = data["m_iNpcLevel"]; - roamX = spawnX = x; - roamY = spawnY = y; - roamZ = spawnZ = z; + roamX = x; + roamY = y; + roamZ = z; offsetX = 0; offsetY = 0; - appearanceData.iConditionBitFlag = 0; - // NOTE: there appear to be discrepancies in the dump - appearanceData.iHP = maxHealth; + hp = maxHealth; - type = EntityType::MOB; - _stepAI = MobAI::step; + kind = EntityKind::MOB; + + // AI + stateHandlers[AIState::DEAD] = MobAI::deadStep; + stateHandlers[AIState::COMBAT] = MobAI::combatStep; + stateHandlers[AIState::ROAMING] = MobAI::roamingStep; + stateHandlers[AIState::RETREAT] = MobAI::retreatStep; + + transitionHandlers[AIState::DEAD] = MobAI::onDeath; + transitionHandlers[AIState::COMBAT] = MobAI::onCombatStart; + transitionHandlers[AIState::ROAMING] = MobAI::onRoamStart; + transitionHandlers[AIState::RETREAT] = MobAI::onRetreat; } // constructor for /summon @@ -89,6 +94,9 @@ struct Mob : public CombatNPC { ~Mob() {} + virtual int takeDamage(EntityRef src, int amt) override; + virtual void step(time_t currTime) override; + auto operator[](std::string s) { return data[s]; } @@ -103,5 +111,4 @@ namespace MobAI { void clearDebuff(Mob *mob); void followToCombat(Mob *mob); void groupRetreat(Mob *mob); - void enterCombat(CNSocket *sock, Mob *mob); } diff --git a/src/NPC.hpp b/src/NPC.hpp deleted file mode 100644 index b5d57dd..0000000 --- a/src/NPC.hpp +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -#include "Chunking.hpp" -#include "Entities.hpp" diff --git a/src/NPCManager.cpp b/src/NPCManager.cpp index 901bfb3..cba6656 100644 --- a/src/NPCManager.cpp +++ b/src/NPCManager.cpp @@ -1,4 +1,8 @@ #include "NPCManager.hpp" + +#include "servers/CNShardServer.hpp" + +#include "PlayerManager.hpp" #include "Items.hpp" #include "settings.hpp" #include "Combat.hpp" @@ -20,8 +24,6 @@ #include #include -#include "JSON.hpp" - using namespace NPCManager; std::unordered_map NPCManager::NPCs; @@ -67,7 +69,7 @@ void NPCManager::destroyNPC(int32_t id) { void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I, int angle) { BaseNPC* npc = NPCs[id]; - npc->appearanceData.iAngle = angle; + npc->angle = angle; ChunkPos oldChunk = npc->chunkPos; ChunkPos newChunk = Chunking::chunkPosAt(X, Y, I); npc->x = X; @@ -79,11 +81,11 @@ 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) { - if (ref.type == EntityType::PLAYER) + if (ref.kind == EntityKind::PLAYER) ref.sock->sendPacket(buf, type, size); } } @@ -122,7 +124,6 @@ static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) { // type must already be checked and updateNPCPosition() must be called on the result BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn, bool baseInstance) { uint64_t inst = baseInstance ? MAPNUM(instance) : instance; -#define EXTRA_HEIGHT 0 //assert(nextId < INT32_MAX); int id = nextId--; @@ -130,12 +131,12 @@ BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, BaseNPC *npc = nullptr; if (team == 2) { - npc = new Mob(x, y, z + EXTRA_HEIGHT, inst, type, NPCData[type], id); + npc = new Mob(x, y, z, inst, type, NPCData[type], id); // re-enable respawning, if desired ((Mob*)npc)->summoned = !respawn; } else - npc = new BaseNPC(x, y, z + EXTRA_HEIGHT, 0, inst, type, id); + npc = new BaseNPC(0, inst, type, id); NPCs[id] = npc; @@ -154,7 +155,7 @@ static void npcSummonHandler(CNSocket* sock, CNPacketData* data) { for (int i = 0; i < req->iNPCCnt; i++) { BaseNPC *npc = summonNPC(plr->x, plr->y, plr->z, plr->instanceID, req->iNPCType); - updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, plr->instanceID, 0); + updateNPCPosition(npc->id, plr->x, plr->y, plr->z, plr->instanceID, 0); } } @@ -183,9 +184,12 @@ static void handleWarp(CNSocket* sock, int32_t warpId) { if (Warps[warpId].isInstance) { uint64_t instanceID = Warps[warpId].instanceID; + Player* leader = plr; + if (plr->group != nullptr) leader = PlayerManager::getPlayer(plr->group->filter(EntityKind::PLAYER)[0].sock); + // if warp requires you to be on a mission, it's gotta be a unique instance if (Warps[warpId].limitTaskID != 0 || instanceID == 14) { // 14 is a special case for the Time Lab - instanceID += ((uint64_t)plr->iIDGroup << 32); // upper 32 bits are leader ID + instanceID += ((uint64_t)leader->iID << 32); // upper 32 bits are leader ID Chunking::createInstance(instanceID); // save Lair entrance coords as a pseudo-Resurrect 'Em @@ -195,14 +199,13 @@ static void handleWarp(CNSocket* sock, int32_t warpId) { plr->recallInstance = instanceID; } - if (plr->iID == plr->iIDGroup && plr->groupCnt == 1) + if (plr->group == nullptr) PlayerManager::sendPlayerTo(sock, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID); else { - Player* leaderPlr = PlayerManager::getPlayerFromID(plr->iIDGroup); - - for (int i = 0; i < leaderPlr->groupCnt; i++) { - Player* otherPlr = PlayerManager::getPlayerFromID(leaderPlr->groupIDs[i]); - CNSocket* sockTo = PlayerManager::getSockFromID(leaderPlr->groupIDs[i]); + auto players = plr->group->filter(EntityKind::PLAYER); + for (int i = 0; i < players.size(); i++) { + CNSocket* sockTo = players[i].sock; + Player* otherPlr = PlayerManager::getPlayer(sockTo); if (otherPlr == nullptr || sockTo == nullptr) continue; @@ -276,7 +279,7 @@ BaseNPC* NPCManager::getNearestNPC(std::set* chunks, int X, int Y, int Z for (auto c = chunks->begin(); c != chunks->end(); c++) { // haha get it Chunk* chunk = *c; for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) { - if (ent->type == EntityType::PLAYER) + if (ent->kind == EntityKind::PLAYER) continue; BaseNPC* npcTemp = (BaseNPC*)ent->getEntity(); @@ -301,20 +304,20 @@ static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) { std::cout << "Lord Fuse stage two" << std::endl; - // Fuse doesn't move; spawnX, etc. is shorter to write than *appearanceData* + // Fuse doesn't move // Blastons, Heal - Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2467); + Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, plr->instanceID, 2467); - newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle; - NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ, - plr->instanceID, oldbody->appearanceData.iAngle); + newbody->angle = oldbody->angle; + NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z, + plr->instanceID, oldbody->angle); // right arm, Adaptium, Stun - Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX - 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2469); + Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, plr->instanceID, 2469); - arm->appearanceData.iAngle = oldbody->appearanceData.iAngle; - NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ, - plr->instanceID, oldbody->appearanceData.iAngle); + arm->angle = oldbody->angle; + NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z, + plr->instanceID, oldbody->angle); } // summon left arm and stage 3 body @@ -325,18 +328,18 @@ static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) { std::cout << "Lord Fuse stage three" << std::endl; // Cosmix, Damage Point - Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2468); + Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, plr->instanceID, 2468); - newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle; - NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ, - plr->instanceID, oldbody->appearanceData.iAngle); + newbody->angle = oldbody->angle; + NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z, + plr->instanceID, oldbody->angle); // Blastons, Heal - Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX + 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2470); + Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, plr->instanceID, 2470); - arm->appearanceData.iAngle = oldbody->appearanceData.iAngle; - NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ, - plr->instanceID, oldbody->appearanceData.iAngle); + arm->angle = oldbody->angle; + NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z, + plr->instanceID, oldbody->angle); } std::vector NPCManager::NPCEvents = { @@ -352,11 +355,11 @@ void NPCManager::queueNPCRemoval(int32_t id) { static void step(CNServer *serv, time_t currTime) { for (auto& pair : NPCs) { - if (pair.second->type != EntityType::COMBAT_NPC && pair.second->type != EntityType::MOB) + if (pair.second->kind != EntityKind::COMBAT_NPC && pair.second->kind != EntityKind::MOB) continue; auto npc = (CombatNPC*)pair.second; - npc->stepAI(currTime); + npc->step(currTime); } // deallocate all NPCs queued for removal @@ -373,5 +376,5 @@ void NPCManager::init() { REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_UNSUMMON, npcUnsummonHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler); - REGISTER_SHARD_TIMER(step, 200); + REGISTER_SHARD_TIMER(step, MS_PER_COMBAT_TICK); } diff --git a/src/NPCManager.hpp b/src/NPCManager.hpp index 8a4ff62..c8738d0 100644 --- a/src/NPCManager.hpp +++ b/src/NPCManager.hpp @@ -1,15 +1,16 @@ #pragma once #include "core/Core.hpp" -#include "PlayerManager.hpp" -#include "NPC.hpp" -#include "Transport.hpp" - #include "JSON.hpp" +#include "Transport.hpp" +#include "Chunking.hpp" +#include "Entities.hpp" + #include #include #include +#include #define RESURRECT_HEIGHT 400 @@ -29,8 +30,6 @@ struct NPCEvent { : npcType(t), trigger(tr), handler(hndlr) {} }; -struct WarpLocation; - namespace NPCManager { extern std::unordered_map NPCs; extern std::map Warps; @@ -44,7 +43,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 c6d4adf..8fa9629 100644 --- a/src/Nanos.cpp +++ b/src/Nanos.cpp @@ -1,11 +1,11 @@ -#include "servers/CNShardServer.hpp" #include "Nanos.hpp" + +#include "servers/CNShardServer.hpp" + #include "PlayerManager.hpp" -#include "NPCManager.hpp" -#include "Combat.hpp" #include "Missions.hpp" -#include "Groups.hpp" #include "Abilities.hpp" +#include "NPCManager.hpp" #include @@ -82,40 +82,21 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) { if (slot != -1 && plr->Nanos[nanoID].iSkillID == 0) return; // prevent powerless nanos from summoning - plr->nanoDrainRate = 0; - int16_t skillID = plr->Nanos[plr->activeNano].iSkillID; - - // passive nano unbuffing - if (SkillTable[skillID].drainType == 2) { - std::vector targetData = findTargets(plr, skillID); - - for (auto& pwr : NanoPowers) - if (pwr.skillType == SkillTable[skillID].skillType) - nanoUnbuff(sock, targetData, pwr.bitFlag, pwr.timeBuffID, 0,(SkillTable[skillID].targetType == 3)); - } - if (nanoID >= NANO_COUNT || nanoID < 0) return; // sanity check plr->activeNano = nanoID; - skillID = plr->Nanos[nanoID].iSkillID; + sNano& nano = plr->Nanos[nanoID]; - // passive nano buffing - if (SkillTable[skillID].drainType == 2) { - std::vector targetData = findTargets(plr, skillID); - - int boost = 0; - if (getNanoBoost(plr)) - boost = 1; - - for (auto& pwr : NanoPowers) { - if (pwr.skillType == SkillTable[skillID].skillType) { - resp.eCSTB___Add = 1; // the part that makes nano go ZOOMAZOOM - plr->nanoDrainRate = SkillTable[skillID].batteryUse[boost*3]; - - pwr.handle(sock, targetData, nanoID, skillID, 0, SkillTable[skillID].powerIntensity[boost]); - } - } + 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; + ICombatant* src = dynamic_cast(plr); + int32_t targets[] = { plr->iID }; + std::vector affectedCombatants = Abilities::matchTargets(src, skill, 1, targets); + Abilities::useNanoSkill(sock, skill, nano, affectedCombatants); } if (!silent) // silent nano death but only for the summoning player @@ -124,7 +105,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); } @@ -211,7 +192,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; } @@ -235,18 +216,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); @@ -289,28 +258,26 @@ static void nanoSummonHandler(CNSocket* sock, CNPacketData* data) { static void nanoSkillUseHandler(CNSocket* sock, CNPacketData* data) { Player *plr = PlayerManager::getPlayer(sock); - int16_t nanoID = plr->activeNano; - int16_t skillID = plr->Nanos[nanoID].iSkillID; + // validate request check + sP_CL2FE_REQ_NANO_SKILL_USE* pkt = (sP_CL2FE_REQ_NANO_SKILL_USE*)data->buf; + if (!validInVarPacket(sizeof(sP_CL2FE_REQ_NANO_SKILL_USE), pkt->iTargetCnt, sizeof(int32_t), data->size)) { + std::cout << "[WARN] bad sP_CL2FE_REQ_NANO_SKILL_USE packet size" << std::endl; + return; + } + + sNano& nano = plr->Nanos[plr->activeNano]; + int16_t skillID = nano.iSkillID; + SkillData* skillData = &Abilities::SkillTable[skillID]; DEBUGLOG( std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano skill " << std::endl; ) - std::vector targetData = findTargets(plr, skillID, data); + ICombatant* plrCombatant = dynamic_cast(plr); + std::vector targetData = Abilities::matchTargets(plrCombatant, skillData, pkt->iTargetCnt, (int32_t*)(pkt + 1)); + Abilities::useNanoSkill(sock, skillData, nano, targetData); - int boost = 0; - if (getNanoBoost(plr)) - boost = 1; - - plr->Nanos[plr->activeNano].iStamina -= SkillTable[skillID].batteryUse[boost*3]; - if (plr->Nanos[plr->activeNano].iStamina < 0) - plr->Nanos[plr->activeNano].iStamina = 0; - - for (auto& pwr : NanoPowers) - if (pwr.skillType == SkillTable[skillID].skillType) - pwr.handle(sock, targetData, nanoID, skillID, SkillTable[skillID].durationTime[boost], SkillTable[skillID].powerIntensity[boost]); - - if (plr->Nanos[plr->activeNano].iStamina < 0) + if (plr->Nanos[plr->activeNano].iStamina <= 0) summonNano(sock, -1); } diff --git a/src/Nanos.hpp b/src/Nanos.hpp index c41318e..e21a297 100644 --- a/src/Nanos.hpp +++ b/src/Nanos.hpp @@ -1,10 +1,11 @@ #pragma once -#include -#include +#include "core/Core.hpp" #include "Player.hpp" -#include "servers/CNShardServer.hpp" +#include "Abilities.hpp" + +#include struct NanoData { int style; diff --git a/src/Player.hpp b/src/Player.hpp index baa77b1..14019ab 100644 --- a/src/Player.hpp +++ b/src/Player.hpp @@ -1,17 +1,21 @@ #pragma once -#include -#include - #include "core/Core.hpp" -#include "Chunking.hpp" + #include "Entities.hpp" +#include "Groups.hpp" + +#include + +/* forward declaration(s) */ +class Buff; +struct BuffStack; #define ACTIVE_MISSION_COUNT 6 #define PC_MAXHEALTH(level) (925 + 75 * (level)) -struct Player : public Entity { +struct Player : public Entity, public ICombatant { int accountId = 0; int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums) int32_t iID = 0; @@ -32,9 +36,8 @@ struct Player : public Entity { 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; @@ -49,7 +52,6 @@ struct Player : public Entity { bool inCombat = false; bool onMonkey = false; - int nanoDrainRate = 0; int healCooldown = 0; int pointDamage = 0; @@ -65,10 +67,7 @@ struct Player : public Entity { sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {}; - int32_t iIDGroup = 0; - int groupCnt = 0; - int32_t groupIDs[4] = {}; - int32_t iGroupConditionBitFlag = 0; + Group* group = nullptr; bool notify = false; bool hidden = false; @@ -85,8 +84,31 @@ struct Player : public Entity { time_t lastShot = 0; std::vector buyback = {}; - Player() { type = EntityType::PLAYER; } + Player() { kind = EntityKind::PLAYER; } 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, BuffClass buffClass) override; + virtual void clearBuffs(bool force) override; + virtual bool hasBuff(int buffId) override; + virtual int getCompositeCondition() override; + virtual int takeDamage(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 f920f76..c07df89 100644 --- a/src/PlayerManager.cpp +++ b/src/PlayerManager.cpp @@ -1,25 +1,20 @@ -#include "core/Core.hpp" +#include "PlayerManager.hpp" + +#include "db/Database.hpp" #include "core/CNShared.hpp" #include "servers/CNShardServer.hpp" -#include "db/Database.hpp" -#include "PlayerManager.hpp" + #include "NPCManager.hpp" -#include "Missions.hpp" -#include "Items.hpp" -#include "Nanos.hpp" -#include "Groups.hpp" -#include "Chat.hpp" -#include "Buddies.hpp" #include "Combat.hpp" #include "Racing.hpp" -#include "BuiltinCommands.hpp" -#include "Abilities.hpp" #include "Eggs.hpp" - -#include "settings.hpp" +#include "Missions.hpp" +#include "Chat.hpp" +#include "Items.hpp" +#include "Buddies.hpp" +#include "BuiltinCommands.hpp" #include - #include #include #include @@ -41,7 +36,13 @@ void PlayerManager::removePlayer(CNSocket* key) { Player* plr = getPlayer(key); uint64_t fromInstance = plr->instanceID; - Groups::groupKickPlayer(plr); + // free buff memory + for(auto buffEntry : plr->buffs) + delete buffEntry.second; + + // leave group + if(plr->group != nullptr) + Groups::groupKick(plr->group, key); // remove player's bullets Combat::Bullets.erase(plr->iID); @@ -65,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; } @@ -232,8 +223,7 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) { Database::getPlayer(plr, lm->playerId); } - plr->groupCnt = 1; - plr->iIDGroup = plr->groupIDs[0] = plr->iID; + plr->group = nullptr; response.iID = plr->iID; response.uiSvrTime = getTime(); @@ -348,12 +338,20 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) { delete lm; } +void PlayerManager::sendToGroup(CNSocket* sock, void* buf, uint32_t type, size_t size) { + Player* plr = getPlayer(sock); + if (plr->group == nullptr) + return; + for(const EntityRef& ref : plr->group->filter(EntityKind::PLAYER)) + ref.sock->sendPacket(buf, type, size); +} + void PlayerManager::sendToViewable(CNSocket* sock, void* buf, uint32_t type, size_t size) { Player* plr = getPlayer(sock); for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) { Chunk* chunk = *it; for (const EntityRef& ref : chunk->entities) { - if (ref.type != EntityType::PLAYER || ref.sock == sock) + if (ref.kind != EntityKind::PLAYER || ref.sock == sock) continue; ref.sock->sendPacket(buf, type, size); @@ -406,21 +404,22 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) { int activeSlot = -1; bool move = false; - if (reviveData->iRegenType == 3 && plr->iConditionBitFlag & CSB_BIT_PHOENIX) { - // nano revive + switch ((ePCRegenType)reviveData->iRegenType) { + case ePCRegenType::HereByPhoenix: // nano revive + if (!(plr->hasBuff(ECSB_PHOENIX))) + return; // sanity check plr->Nanos[plr->activeNano].iStamina = 0; + // fallthrough + case ePCRegenType::HereByPhoenixGroup: // revived by group member's nano plr->HP = PC_MAXHEALTH(plr->level) / 2; - Nanos::applyBuff(sock, plr->Nanos[plr->activeNano].iSkillID, 2, 1, 0); - } else if (reviveData->iRegenType == 4) { - // revived by group member's nano + break; + + default: // plain respawn plr->HP = PC_MAXHEALTH(plr->level) / 2; - } else if (reviveData->iRegenType == 5) { - // warp away + // fallthrough + case ePCRegenType::Unstick: // warp away move = true; - } else { - // plain respawn - move = true; - plr->HP = PC_MAXHEALTH(plr->level) / 2; + break; } for (int i = 0; i < 3; i++) { @@ -472,11 +471,9 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) { resp2.PCRegenDataForOtherPC.iHP = plr->HP; resp2.PCRegenDataForOtherPC.iAngle = plr->angle; - Player *otherPlr = getPlayerFromID(plr->iIDGroup); - if (otherPlr != nullptr) { - int bitFlag = Groups::getGroupFlags(otherPlr); - resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->iConditionBitFlag = plr->iSelfConditionBitFlag | bitFlag; - + if (plr->group != nullptr) { + + resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->getCompositeCondition(); resp2.PCRegenDataForOtherPC.iPCState = plr->iPCState; resp2.PCRegenDataForOtherPC.iSpecialState = plr->iSpecialState; resp2.PCRegenDataForOtherPC.Nano = plr->Nanos[plr->activeNano]; @@ -667,16 +664,16 @@ CNSocket *PlayerManager::getSockFromName(std::string firstname, std::string last } CNSocket *PlayerManager::getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname) { - switch (by) { - case eCN_GM_TargetSearchBy__PC_ID: + switch ((eCN_GM_TargetSearchBy)by) { + case eCN_GM_TargetSearchBy::PC_ID: assert(id != 0); return getSockFromID(id); - case eCN_GM_TargetSearchBy__PC_UID: // account id; not player id + case eCN_GM_TargetSearchBy::PC_UID: // account id; not player id assert(uid != 0); for (auto& pair : players) if (pair.second->accountId == uid) return pair.first; - case eCN_GM_TargetSearchBy__PC_Name: + case eCN_GM_TargetSearchBy::PC_Name: assert(firstname != "" && lastname != ""); // XXX: remove this if we start messing around with edited names? return getSockFromName(firstname, lastname); } diff --git a/src/PlayerManager.hpp b/src/PlayerManager.hpp index 0ab2049..ad9c767 100644 --- a/src/PlayerManager.hpp +++ b/src/PlayerManager.hpp @@ -1,16 +1,15 @@ #pragma once -#include "Player.hpp" #include "core/Core.hpp" -#include "servers/CNShardServer.hpp" -#include "Chunking.hpp" -#include +#include "Player.hpp" +#include "Chunking.hpp" +#include "Transport.hpp" +#include "Entities.hpp" + #include #include -struct WarpLocation; - namespace PlayerManager { extern std::map players; void init(); @@ -34,6 +33,7 @@ namespace PlayerManager { CNSocket *getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname); WarpLocation *getRespawnPoint(Player *plr); + void sendToGroup(CNSocket *sock, void* buf, uint32_t type, size_t size); void sendToViewable(CNSocket *sock, void* buf, uint32_t type, size_t size); // TODO: unify this under the new Entity system @@ -43,7 +43,7 @@ namespace PlayerManager { for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) { Chunk* chunk = *it; for (const EntityRef& ref : chunk->entities) { - if (ref.type != EntityType::PLAYER || ref.sock == sock) + if (ref.kind != EntityKind::PLAYER || ref.sock == sock) continue; ref.sock->sendPacket(pkt, type); diff --git a/src/PlayerMovement.cpp b/src/PlayerMovement.cpp index c19af28..c4d5150 100644 --- a/src/PlayerMovement.cpp +++ b/src/PlayerMovement.cpp @@ -1,4 +1,7 @@ #include "PlayerMovement.hpp" + +#include "servers/CNShardServer.hpp" + #include "PlayerManager.hpp" #include "TableData.hpp" #include "core/Core.hpp" @@ -33,7 +36,7 @@ static void movePlayer(CNSocket* sock, CNPacketData* data) { // [gruntwork] check if player has a follower and move it if (TableData::RunningNPCPaths.find(plr->iID) != TableData::RunningNPCPaths.end()) { BaseNPC* follower = TableData::RunningNPCPaths[plr->iID].first; - Transport::NPCQueues.erase(follower->appearanceData.iNPC_ID); // erase existing points + Transport::NPCQueues.erase(follower->id); // erase existing points std::queue queue; Vec3 from = { follower->x, follower->y, follower->z }; float drag = 0.95f; // this ensures that they don't bump into the player @@ -45,7 +48,7 @@ static void movePlayer(CNSocket* sock, CNPacketData* data) { // add a route to the queue; to be processed in Transport::stepNPCPathing() Transport::lerp(&queue, from, to, NPC_DEFAULT_SPEED * 1.5); // little faster than typical - Transport::NPCQueues[follower->appearanceData.iNPC_ID] = queue; + Transport::NPCQueues[follower->id] = queue; } } diff --git a/src/Racing.cpp b/src/Racing.cpp index 565850c..c3b7f6f 100644 --- a/src/Racing.cpp +++ b/src/Racing.cpp @@ -1,10 +1,12 @@ -#include "servers/CNShardServer.hpp" #include "Racing.hpp" + +#include "db/Database.hpp" +#include "servers/CNShardServer.hpp" + +#include "NPCManager.hpp" #include "PlayerManager.hpp" #include "Missions.hpp" #include "Items.hpp" -#include "db/Database.hpp" -#include "NPCManager.hpp" using namespace Racing; diff --git a/src/Racing.hpp b/src/Racing.hpp index f6e6acb..a4f3b6d 100644 --- a/src/Racing.hpp +++ b/src/Racing.hpp @@ -1,8 +1,10 @@ #pragma once -#include +#include "core/Core.hpp" -#include "servers/CNShardServer.hpp" +#include +#include +#include struct EPInfo { int zoneX, zoneY, EPID, maxScore, maxTime; diff --git a/src/Rand.hpp b/src/Rand.hpp index 8f93053..2a73937 100644 --- a/src/Rand.hpp +++ b/src/Rand.hpp @@ -2,6 +2,7 @@ #include #include +#include namespace Rand { extern std::unique_ptr generator; diff --git a/src/TableData.cpp b/src/TableData.cpp index a685cee..68b7cce 100644 --- a/src/TableData.cpp +++ b/src/TableData.cpp @@ -1,18 +1,14 @@ #include "TableData.hpp" + #include "NPCManager.hpp" -#include "Transport.hpp" -#include "Items.hpp" -#include "settings.hpp" #include "Missions.hpp" -#include "Chunking.hpp" -#include "Nanos.hpp" -#include "Racing.hpp" +#include "Items.hpp" #include "Vendors.hpp" +#include "Racing.hpp" +#include "Nanos.hpp" #include "Abilities.hpp" #include "Eggs.hpp" -#include "JSON.hpp" - #include #include #include @@ -231,18 +227,34 @@ static void loadXDT(json& xdtData) { // load nano powers json skills = xdtData["m_pSkillTable"]["m_pSkillData"]; - for (json::iterator _skills = skills.begin(); _skills != skills.end(); _skills++) { - auto skills = _skills.value(); - SkillData skillData = { skills["m_iSkillType"], skills["m_iTargetType"], skills["m_iBatteryDrainType"], skills["m_iEffectArea"] }; + for (json::iterator _skill = skills.begin(); _skill != skills.end(); _skill++) { + auto skill = _skill.value(); + SkillData skillData = { + skill["m_iSkillType"], + skill["m_iEffectTarget"], + skill["m_iEffectType"], + skill["m_iTargetType"], + skill["m_iBatteryDrainType"], + skill["m_iEffectArea"] + }; + + skillData.valueTypes[0] = skill["m_iValueA_Type"]; + skillData.valueTypes[1] = skill["m_iValueB_Type"]; + skillData.valueTypes[2] = skill["m_iValueC_Type"]; + for (int i = 0; i < 4; i++) { - skillData.batteryUse[i] = skills["m_iBatteryDrainUse"][i]; - skillData.durationTime[i] = skills["m_iDurationTime"][i]; - skillData.powerIntensity[i] = skills["m_iValueA"][i]; + skillData.batteryUse[i] = skill["m_iBatteryDrainUse"][i]; + skillData.durationTime[i] = skill["m_iDurationTime"][i]; + + skillData.values[0][i] = skill["m_iValueA"][i]; + skillData.values[1][i] = skill["m_iValueB"][i]; + skillData.values[2][i] = skill["m_iValueC"][i]; } - Nanos::SkillTable[skills["m_iSkillNumber"]] = skillData; + + Abilities::SkillTable[skill["m_iSkillNumber"]] = skillData; } - std::cout << "[INFO] Loaded " << Nanos::SkillTable.size() << " nano skills" << std::endl; + std::cout << "[INFO] Loaded " << Abilities::SkillTable.size() << " nano skills" << std::endl; // load EP data json instances = xdtData["m_pInstanceTable"]["m_pInstanceData"]; @@ -314,10 +326,10 @@ static void loadPaths(json& pathData, int32_t* nextId) { if (passedDistance >= SLIDER_GAP_SIZE) { // space them out uniformaly passedDistance -= SLIDER_GAP_SIZE; // step down // spawn a slider - Bus* slider = new Bus(point.x, point.y, point.z, 0, INSTANCE_OVERWORLD, 1, (*nextId)--); - NPCManager::NPCs[slider->appearanceData.iNPC_ID] = slider; - NPCManager::updateNPCPosition(slider->appearanceData.iNPC_ID, slider->x, slider->y, slider->z, INSTANCE_OVERWORLD, 0); - Transport::NPCQueues[slider->appearanceData.iNPC_ID] = route; + Bus* slider = new Bus(0, INSTANCE_OVERWORLD, 1, (*nextId)--); + NPCManager::NPCs[slider->id] = slider; + NPCManager::updateNPCPosition(slider->id, point.x, point.y, point.z, INSTANCE_OVERWORLD, 0); + Transport::NPCQueues[slider->id] = route; } // rotate route.pop(); @@ -659,7 +671,7 @@ static void loadEggs(json& eggData, int32_t* nextId) { int id = (*nextId)--; uint64_t instanceID = egg.find("iMapNum") == egg.end() ? INSTANCE_OVERWORLD : (int)egg["iMapNum"]; - Egg* addEgg = new Egg((int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, (int)egg["iType"], id, false); + Egg* addEgg = new Egg(instanceID, (int)egg["iType"], id, false); NPCManager::NPCs[id] = addEgg; eggCount++; NPCManager::updateNPCPosition(id, (int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, 0); @@ -755,7 +767,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) { if (NPCManager::NPCs.find(npcID) == NPCManager::NPCs.end()) continue; // NPC not found BaseNPC* npc = NPCManager::NPCs[npcID]; - npc->appearanceData.iAngle = angle; + npc->angle = angle; RunningNPCRotations[npcID] = angle; } @@ -768,8 +780,8 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) { if (NPCManager::NPCs.find(npcID) == NPCManager::NPCs.end()) continue; // NPC not found BaseNPC* npc = NPCManager::NPCs[npcID]; - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, - npc->z, instanceID, npc->appearanceData.iAngle); + NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, + npc->z, instanceID, npc->angle); RunningNPCMapNumbers[npcID] = instanceID; } @@ -791,12 +803,12 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) { // re-enable respawning ((Mob*)npc)->summoned = false; } else { - npc = new BaseNPC(mob["iX"], mob["iY"], mob["iZ"], mob["iAngle"], instanceID, mob["iNPCType"], id); + npc = new BaseNPC(mob["iAngle"], instanceID, mob["iNPCType"], id); } - NPCManager::NPCs[npc->appearanceData.iNPC_ID] = npc; - RunningMobs[npc->appearanceData.iNPC_ID] = npc; - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iAngle"]); + NPCManager::NPCs[npc->id] = npc; + RunningMobs[npc->id] = npc; + NPCManager::updateNPCPosition(npc->id, mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iAngle"]); } // mob groups @@ -840,7 +852,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) { tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"]; tmpFol->offsetY = follower.find("iOffsetY") == follower.end() ? 0 : (int)follower["iOffsetY"]; - tmpFol->groupLeader = tmp->appearanceData.iNPC_ID; + tmpFol->groupLeader = tmp->id; tmp->groupMember[followerCount++] = *nextId; (*nextId)--; @@ -850,7 +862,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) { std::cout << "[WARN] Mob group leader with ID " << *nextId << " has too many followers (" << followers.size() << ")\n"; } - RunningGroups[tmp->appearanceData.iNPC_ID] = tmp; // store as running + RunningGroups[tmp->id] = tmp; // store as running } auto eggs = gruntwork["eggs"]; @@ -859,7 +871,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) { int id = (*nextId)--; uint64_t instanceID = egg.find("iMapNum") == egg.end() ? INSTANCE_OVERWORLD : (int)egg["iMapNum"]; - Egg* addEgg = new Egg((int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, (int)egg["iType"], id, false); + Egg* addEgg = new Egg(instanceID, (int)egg["iType"], id, false); NPCManager::NPCs[id] = addEgg; NPCManager::updateNPCPosition(id, (int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, 0); RunningEggs[id] = addEgg; @@ -893,7 +905,7 @@ static void loadNPCs(json& npcData) { if (npc["iX"] > 512000 && npc["iY"] < 256000) continue; #endif - BaseNPC* tmp = new BaseNPC(npc["iX"], npc["iY"], npc["iZ"], npc["iAngle"], instanceID, type, npcID); + BaseNPC* tmp = new BaseNPC(npc["iAngle"], instanceID, type, npcID); NPCManager::NPCs[npcID] = tmp; NPCManager::updateNPCPosition(npcID, npc["iX"], npc["iY"], npc["iZ"], instanceID, npc["iAngle"]); @@ -1003,7 +1015,7 @@ static void loadMobs(json& npcData, int32_t* nextId) { tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"]; tmpFol->offsetY = follower.find("iOffsetY") == follower.end() ? 0 : (int)follower["iOffsetY"]; - tmpFol->groupLeader = tmp->appearanceData.iNPC_ID; + tmpFol->groupLeader = tmp->id; tmp->groupMember[followerCount++] = *nextId; (*nextId)--; @@ -1226,7 +1238,7 @@ void TableData::flush() { continue; int x, y, z; - if (npc->type == EntityType::MOB) { + if (npc->kind == EntityKind::MOB) { Mob *m = (Mob*)npc; x = m->spawnX; y = m->spawnY; @@ -1238,13 +1250,13 @@ void TableData::flush() { } // NOTE: this format deviates slightly from the one in mobs.json - mob["iNPCType"] = (int)npc->appearanceData.iNPCType; + mob["iNPCType"] = (int)npc->type; mob["iX"] = x; mob["iY"] = y; mob["iZ"] = z; mob["iMapNum"] = MAPNUM(npc->instanceID); // this is a bit imperfect, since this is a live angle, not a spawn angle so it'll change often, but eh - mob["iAngle"] = npc->appearanceData.iAngle; + mob["iAngle"] = npc->angle; // it's called mobs, but really it's everything gruntwork["mobs"].push_back(mob); @@ -1259,19 +1271,19 @@ void TableData::flush() { int x, y, z; std::vector followers; - if (npc->type == EntityType::MOB) { + if (npc->kind == EntityKind::MOB) { Mob* m = (Mob*)npc; x = m->spawnX; y = m->spawnY; z = m->spawnZ; - if (m->groupLeader != m->appearanceData.iNPC_ID) { // make sure this is a leader + if (m->groupLeader != m->id) { // make sure this is a leader std::cout << "[WARN] Non-leader mob found in running groups; ignoring\n"; continue; } // add follower data to vector; go until OOB or until follower ID is 0 for (int i = 0; i < 4 && m->groupMember[i] > 0; i++) { - if (NPCManager::NPCs.find(m->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[m->groupMember[i]]->type != EntityType::MOB) { + if (NPCManager::NPCs.find(m->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[m->groupMember[i]]->kind != EntityKind::MOB) { std::cout << "[WARN] Follower with ID " << m->groupMember[i] << " not found; skipping\n"; continue; } @@ -1285,13 +1297,13 @@ void TableData::flush() { } // NOTE: this format deviates slightly from the one in mobs.json - mob["iNPCType"] = (int)npc->appearanceData.iNPCType; + mob["iNPCType"] = (int)npc->type; mob["iX"] = x; mob["iY"] = y; mob["iZ"] = z; mob["iMapNum"] = MAPNUM(npc->instanceID); // this is a bit imperfect, since this is a live angle, not a spawn angle so it'll change often, but eh - mob["iAngle"] = npc->appearanceData.iAngle; + mob["iAngle"] = npc->angle; // followers while (followers.size() > 0) { @@ -1300,7 +1312,7 @@ void TableData::flush() { // populate JSON entry json fol; - fol["iNPCType"] = follower->appearanceData.iNPCType; + fol["iNPCType"] = follower->type; fol["iOffsetX"] = follower->offsetX; fol["iOffsetY"] = follower->offsetY; @@ -1325,7 +1337,7 @@ void TableData::flush() { int mapnum = MAPNUM(npc->instanceID); if (mapnum != 0) egg["iMapNum"] = mapnum; - egg["iType"] = npc->appearanceData.iNPCType; + egg["iType"] = npc->type; gruntwork["eggs"].push_back(egg); } diff --git a/src/TableData.hpp b/src/TableData.hpp index d8be3fc..5f54071 100644 --- a/src/TableData.hpp +++ b/src/TableData.hpp @@ -1,8 +1,13 @@ #pragma once -#include +#include "JSON.hpp" -#include "NPCManager.hpp" +#include "Entities.hpp" +#include "Transport.hpp" + +#include +#include +#include // these are added to the NPC's static key to avoid collisions const int NPC_ID_OFFSET = 1; diff --git a/src/Trading.cpp b/src/Trading.cpp index 2a258f1..8d70fd9 100644 --- a/src/Trading.cpp +++ b/src/Trading.cpp @@ -1,4 +1,7 @@ #include "Trading.hpp" + +#include "servers/CNShardServer.hpp" + #include "PlayerManager.hpp" #include "db/Database.hpp" diff --git a/src/Trading.hpp b/src/Trading.hpp index 108eefb..d7a0398 100644 --- a/src/Trading.hpp +++ b/src/Trading.hpp @@ -1,7 +1,5 @@ #pragma once -#include "Items.hpp" - namespace Trading { void init(); } \ No newline at end of file diff --git a/src/Transport.cpp b/src/Transport.cpp index 0fe4f10..0c43d9c 100644 --- a/src/Transport.cpp +++ b/src/Transport.cpp @@ -1,9 +1,12 @@ +#include "Transport.hpp" + #include "servers/CNShardServer.hpp" + #include "PlayerManager.hpp" #include "Nanos.hpp" -#include "Transport.hpp" #include "TableData.hpp" -#include "Combat.hpp" +#include "Entities.hpp" +#include "NPCManager.hpp" #include "MobAI.hpp" #include @@ -257,13 +260,13 @@ static void stepNPCPathing() { } // skip if not simulating mobs - if (npc->type == EntityType::MOB && !MobAI::simulateMobs) { + if (npc->kind == EntityKind::MOB && !MobAI::simulateMobs) { it++; continue; } // do not roam if not roaming - if (npc->type == EntityType::MOB && ((Mob*)npc)->state != MobState::ROAMING) { + if (npc->kind == EntityKind::MOB && ((Mob*)npc)->state != AIState::ROAMING) { it++; continue; } @@ -276,15 +279,15 @@ static void stepNPCPathing() { int distanceBetween = hypot(dXY, point.z - npc->z); // total distance // update NPC location to update viewables - NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, point.x, point.y, point.z, npc->instanceID, npc->appearanceData.iAngle); + NPCManager::updateNPCPosition(npc->id, point.x, point.y, point.z, npc->instanceID, npc->angle); // TODO: move walking logic into Entity stack - switch (npc->type) { - case EntityType::BUS: + switch (npc->kind) { + case EntityKind::BUS: INITSTRUCT(sP_FE2CL_TRANSPORTATION_MOVE, busMove); busMove.eTT = 3; - busMove.iT_ID = npc->appearanceData.iNPC_ID; + busMove.iT_ID = npc->id; busMove.iMoveStyle = 0; // ??? busMove.iToX = point.x; busMove.iToY = point.y; @@ -293,12 +296,12 @@ static void stepNPCPathing() { NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE)); break; - case EntityType::MOB: + case EntityKind::MOB: MobAI::incNextMovement((Mob*)npc); /* fallthrough */ default: INITSTRUCT(sP_FE2CL_NPC_MOVE, move); - move.iNPC_ID = npc->appearanceData.iNPC_ID; + move.iNPC_ID = npc->id; move.iMoveStyle = 0; // ??? move.iToX = point.x; move.iToY = point.y; @@ -385,7 +388,7 @@ NPCPath* Transport::findApplicablePath(int32_t id, int32_t type, int taskID) { void Transport::constructPathNPC(int32_t id, NPCPath* path) { BaseNPC* npc = NPCManager::NPCs[id]; - if (npc->type == EntityType::MOB) + if (npc->kind == EntityKind::MOB) ((Mob*)(npc))->staticPath = true; npc->loopingPath = path->isLoop; diff --git a/src/Transport.hpp b/src/Transport.hpp index 1efe304..fbacb60 100644 --- a/src/Transport.hpp +++ b/src/Transport.hpp @@ -1,8 +1,11 @@ #pragma once -#include "servers/CNShardServer.hpp" +#include "core/Core.hpp" #include +#include +#include +#include const int SLIDER_SPEED = 1200; const int SLIDER_STOP_TICKS = 16; diff --git a/src/Vendors.cpp b/src/Vendors.cpp index 732cc28..5e2a7a3 100644 --- a/src/Vendors.cpp +++ b/src/Vendors.cpp @@ -1,4 +1,9 @@ #include "Vendors.hpp" + +#include "servers/CNShardServer.hpp" + +#include "PlayerManager.hpp" +#include "Items.hpp" #include "Rand.hpp" // 7 days diff --git a/src/Vendors.hpp b/src/Vendors.hpp index 851791a..8df4c0f 100644 --- a/src/Vendors.hpp +++ b/src/Vendors.hpp @@ -1,10 +1,9 @@ #pragma once #include "core/Core.hpp" -#include "servers/CNShardServer.hpp" -#include "Items.hpp" -#include "PlayerManager.hpp" +#include +#include struct VendorListing { int sort, type, id; 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/CNStructs.hpp b/src/core/CNStructs.hpp index fbadff2..290f8ca 100644 --- a/src/core/CNStructs.hpp +++ b/src/core/CNStructs.hpp @@ -23,6 +23,7 @@ #include #include #include +#include // yes this is ugly, but this is needed to zero out the memory so we don't have random stackdata in our structs. #define INITSTRUCT(T, x) T x; \ diff --git a/src/core/Defines.hpp b/src/core/Defines.hpp index 3cb0e05..7939d14 100644 --- a/src/core/Defines.hpp +++ b/src/core/Defines.hpp @@ -10,63 +10,44 @@ const float CN_EP_RANK_4 = 0.3f; const float CN_EP_RANK_5 = 0.29f; // methods of finding players for GM commands -enum eCN_GM_TargetSearchBy { - eCN_GM_TargetSearchBy__PC_ID, // player id - eCN_GM_TargetSearchBy__PC_Name, // firstname, lastname - eCN_GM_TargetSearchBy__PC_UID // account id +enum class eCN_GM_TargetSearchBy { + PC_ID, // player id + PC_Name, // firstname, lastname + PC_UID // account id }; -enum eCN_GM_TeleportType { - eCN_GM_TeleportMapType__XYZ, - eCN_GM_TeleportMapType__MapXYZ, - eCN_GM_TeleportMapType__MyLocation, - eCN_GM_TeleportMapType__SomeoneLocation, - eCN_GM_TeleportMapType__Unstick +enum class eCN_GM_TeleportType { + XYZ, + MapXYZ, + MyLocation, + SomeoneLocation, + Unstick }; -// nano powers +enum class eTaskTypeProperty { + None = -1, + Talk = 1, + GotoLocation = 2, + UseItems = 3, + Delivery = 4, + Defeat = 5, + EscortDefence = 6, + Max = 7 +}; + +enum class ePCRegenType { + None, + Xcom, + Here, + HereByPhoenix, + HereByPhoenixGroup, + Unstick, + HereByPhoenixItem, + End +}; + +// nano power flags enum { - EST_NONE = 0, - EST_DAMAGE = 1, - EST_HEAL_HP = 2, - EST_KNOCKDOWN = 3, - EST_SLEEP = 4, - EST_SNARE = 5, - EST_HEAL_STAMINA = 6, - EST_STAMINA_SELF = 7, - EST_STUN = 8, - EST_WEAPONSLOW = 9, - EST_JUMP = 10, - EST_RUN = 11, - EST_STEALTH = 12, - EST_SWIM = 13, - EST_MINIMAPENEMY = 14, - EST_MINIMAPTRESURE = 15, - EST_PHOENIX = 16, - EST_PROTECTBATTERY = 17, - EST_PROTECTINFECTION = 18, - EST_REWARDBLOB = 19, - EST_REWARDCASH = 20, - EST_BATTERYDRAIN = 21, - EST_CORRUPTIONATTACK = 22, - EST_INFECTIONDAMAGE = 23, - EST_KNOCKBACK = 24, - EST_FREEDOM = 25, - EST_PHOENIX_GROUP = 26, - EST_RECALL = 27, - EST_RECALL_GROUP = 28, - EST_RETROROCKET_SELF = 29, - EST_BLOODSUCKING = 30, - EST_BOUNDINGBALL = 31, - EST_INVULNERABLE = 32, - EST_NANOSTIMPAK = 33, - EST_RETURNHOMEHEAL = 34, - EST_BUFFHEAL = 35, - EST_EXTRABANK = 36, - EST__END = 37, - EST_CORRUPTIONATTACKWIN = 38, - EST_CORRUPTIONATTACKLOSE = 39, - ECSB_NONE = 0, ECSB_UP_MOVE_SPEED = 1, ECSB_UP_SWIM_SPEED = 2, @@ -96,6 +77,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/core/Packets.hpp b/src/core/Packets.hpp index 55a0358..f79bb2a 100644 --- a/src/core/Packets.hpp +++ b/src/core/Packets.hpp @@ -47,8 +47,8 @@ struct PacketDesc { * really should. */ struct sGM_PVPTarget { - uint32_t eCT; uint32_t iID; + uint32_t eCT; }; struct sSkillResult_Leech { diff --git a/src/servers/CNLoginServer.cpp b/src/servers/CNLoginServer.cpp index 41e4b04..427e7bb 100644 --- a/src/servers/CNLoginServer.cpp +++ b/src/servers/CNLoginServer.cpp @@ -1,13 +1,15 @@ #include "servers/CNLoginServer.hpp" + #include "core/CNShared.hpp" #include "db/Database.hpp" -#include "PlayerManager.hpp" -#include "Items.hpp" -#include #include "bcrypt/BCrypt.hpp" +#include "PlayerManager.hpp" +#include "Items.hpp" #include "settings.hpp" +#include + std::map CNLoginServer::loginSessions; CNLoginServer::CNLoginServer(uint16_t p) { diff --git a/src/servers/CNLoginServer.hpp b/src/servers/CNLoginServer.hpp index 6547b44..9b13829 100644 --- a/src/servers/CNLoginServer.hpp +++ b/src/servers/CNLoginServer.hpp @@ -1,6 +1,7 @@ #pragma once #include "core/Core.hpp" + #include "Player.hpp" #include diff --git a/src/servers/CNShardServer.cpp b/src/servers/CNShardServer.cpp index c56657e..c6d93b3 100644 --- a/src/servers/CNShardServer.cpp +++ b/src/servers/CNShardServer.cpp @@ -1,10 +1,12 @@ +#include "servers/CNShardServer.hpp" + #include "core/Core.hpp" +#include "core/CNShared.hpp" #include "db/Database.hpp" #include "servers/Monitor.hpp" -#include "servers/CNShardServer.hpp" + #include "PlayerManager.hpp" #include "MobAI.hpp" -#include "core/CNShared.hpp" #include "settings.hpp" #include "TableData.hpp" // for flush() diff --git a/src/servers/CNShardServer.hpp b/src/servers/CNShardServer.hpp index a16f38a..7ed4e3f 100644 --- a/src/servers/CNShardServer.hpp +++ b/src/servers/CNShardServer.hpp @@ -3,9 +3,12 @@ #include "core/Core.hpp" #include +#include #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 +#define MS_PER_COMBAT_TICK 200 class CNShardServer : public CNServer { private: diff --git a/src/servers/Monitor.cpp b/src/servers/Monitor.cpp index 4b7819f..5a28719 100644 --- a/src/servers/Monitor.cpp +++ b/src/servers/Monitor.cpp @@ -1,8 +1,10 @@ +#include "servers/Monitor.hpp" + #include "servers/CNShardServer.hpp" + #include "PlayerManager.hpp" #include "Chat.hpp" #include "Email.hpp" -#include "servers/Monitor.hpp" #include "settings.hpp" #include diff --git a/src/servers/Monitor.hpp b/src/servers/Monitor.hpp index dd40877..3edcf73 100644 --- a/src/servers/Monitor.hpp +++ b/src/servers/Monitor.hpp @@ -2,9 +2,6 @@ #include "core/Core.hpp" -#include -#include - namespace Monitor { SOCKET init(); bool acceptConnection(SOCKET, uint16_t); diff --git a/src/settings.cpp b/src/settings.cpp index 14d8de4..45d2cfc 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1,9 +1,10 @@ -#include + #include "settings.hpp" + +#include "core/CNStructs.hpp" // so we get the ACADEMY definition #include "INIReader.hpp" -// so we get the ACADEMY definition -#include "core/CNStructs.hpp" +#include // defaults :) int settings::VERBOSITY = 1; diff --git a/src/settings.hpp b/src/settings.hpp index 1ea3a43..097a1cd 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -1,5 +1,7 @@ #pragma once +#include + namespace settings { extern int VERBOSITY; extern bool SANDBOX; diff --git a/vendor/JSON.hpp b/vendor/JSON.hpp index 242f034..1866ad4 100644 --- a/vendor/JSON.hpp +++ b/vendor/JSON.hpp @@ -27,6 +27,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +#pragma once + #ifndef INCLUDE_NLOHMANN_JSON_HPP_ #define INCLUDE_NLOHMANN_JSON_HPP_