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/CONTRIBUTING.md b/CONTRIBUTING.md index d8d819d..2d76f31 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ Both are pretty short reads and following them will get you up to speed with bra I will now cover a few examples of the complications people have encountered contributing to this project, how to understand them, overcome them and henceforth avoid them. -## Dirty pull requests +### Dirty pull requests Many Pull Requests OpenFusion receives fail to present a clean set of commits to merge. These are generally either: @@ -44,7 +44,7 @@ If you read the above links, you'll note that this isn't exactly a perfect solut The obvious issue, then, is that the people submitting dirty PRs are the exact people who don't *know* how to rebase their fork to continue submitting their work cleanly. So they end up creating countless merge commits when pulling upstream on top of their own incompatible histories, and then submitting those merge commits in their PRs and the cycle continues. -## The details +### The details A git commit is uniquely identified by its SHA1 hash. Its hash is generated from its contents, as well as the hash(es) of its parent commit(s). @@ -52,7 +52,7 @@ Its hash is generated from its contents, as well as the hash(es) of its parent c That means that even if two commits are exactly the same in terms of content, they're not the same commit if their parent commits differ (ex. if they've been rebased). So if you keep issuing `git pull`s after upstream has merged a rebased version of your PR, you will never re-synchronize with it, and will instead construct an alternate history polluted by pointless merge commits. -## The solution +### The solution If you already have a messed-up fork and you have no changes on it that you're afraid to lose, the solution is simple: @@ -67,7 +67,7 @@ If you do have some committed changes that haven't yet been merged upstream, you If you do end up messing something up, don't worry, it most likely isn't really lost and `git reflog` is your friend. (You can checkout an arbitrary commit, and make it into its own branch with `git checkout -b BRANCH` or set a pre-exisitng branch to it with `git reset --hard COMMIT`) -## Avoiding the problem +### Avoiding the problem When working on a changeset you want to submit back upstream, don't do it on the main branch. Create a work branch just for your changeset with `git checkout -b work`. @@ -81,3 +81,34 @@ That way you can always keep master in sync with upstream with `git pull --ff-on Creating new branches for the rebase isn't strictly necessary since you can always return a branch to its previous state with `git reflog`. For moving uncommited changes around between branches, `git stash` is a real blessing. + +## Code guidelines + +Alright, you're up to speed on Git and ready to go. Here are a few specific code guidelines to try and follow: + +### Match the styling + +Pretty straightforward, make sure your code looks similar to the code around it. Match whitespacing, bracket styling, variable naming conventions, etc. + +### Prefer short-circuiting + +To minimize branching complexity (as this makes the code hard to read), we prefer to keep the number of `if-else` statements as low as possible. One easy way to achieve this is by doing an early return after branching into an `if` block and then writing the code for the other path outside the block entirely. You can find examples of this in practically every source file. Note that in a few select situations, this might actually make your code less elegant, which is why this isn't a strict rule. Lean towards short-circuiting and use your better judgement. + +### Follow the include convention + +This one matters a lot as it can cause cyclic dependencies and other code-breaking issues. + +FOR HEADER FILES (.hpp): +- everything you use IN THE HEADER must be EXPLICITLY INCLUDED with the exception of things that fall under Core.hpp +- you may NOT include ANYTHING ELSE + +FOR SOURCE FILES (.cpp): +- you can #include whatever you want as long as the partner header is included first +- anything that gets included by another include is fair game +- redundant includes are ok because they'll be harmless AS LONG AS our header files stay lean. + +The point of this is NOT to optimize the number of includes used all around or make things more efficient necessarily. it's to improve readability & coherence and make it easier to avoid cyclical issues. + +## When in doubt, ask + +If you still have questions that were not answered here, feel free to ping a dev in the Discord server or on GitHub. 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..bed931b 100644 --- a/src/Abilities.cpp +++ b/src/Abilities.cpp @@ -1,804 +1,505 @@ #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 + } + Buffs::timeBuffUpdate(self, buff, status, stack); + 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; + const bool blocked = target->hasBuff(ECSB_PROTECT_BATTERY); + + int boostDrain = 0; + int potionDrain = 0; + if(!blocked) { + boostDrain = (int)(skill->values[0][power] * scalingFactor); + if(boostDrain > plr->batteryW) boostDrain = plr->batteryW; + plr->batteryW -= boostDrain; + + 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 = blocked; + 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::CORRUPTIONATTACK: + case SkillType::CORRUPTIONATTACKLOSE: + case SkillType::CORRUPTIONATTACKWIN: + // skillHandler = handleSkillCorruptionReflect; + // break; + 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..ae29a20 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); @@ -179,7 +227,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) { return false; } -static void dealCorruption(Mob *mob, std::vector targetData, int skillID, int style) { +static void dealCorruption(Mob *mob, std::vector targetData, int skillID, int mobStyle) { Player *plr = PlayerManager::getPlayer(mob->target); size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT) + targetData[0] * sizeof(sCAttackResult); @@ -196,9 +244,9 @@ 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->iStyle = mobStyle; resp->iValue1 = plr->x; resp->iValue2 = plr->y; resp->iValue3 = plr->z; @@ -232,28 +280,41 @@ static void dealCorruption(Mob *mob, std::vector targetData, int skillID, i respdata[i].iActiveNanoSlotNum = n; respdata[i].iNanoID = plr->activeNano; - 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; - } else if (style == style2) { - respdata[i].iHitFlag = 8; // tie + int nanoStyle = Nanos::nanoStyle(plr->activeNano); + if (nanoStyle == -1) { // no nano + 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 (mobStyle == nanoStyle) { + 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 + } else if (mobStyle - nanoStyle == 1 || nanoStyle - mobStyle == 2) { + 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 = { + SkillType::DAMAGE, // skillType + SkillEffectTarget::POINT, // effectTarget + 1, // effectType + SkillTargetType::MOBS, // targetType + SkillDrainType::ACTIVE, // drainType + 0, // effectArea + {0, 0, 0, 0}, // batteryUse + {0, 0, 0, 0}, // durationTime + {0, 0, 0}, // valueTypes (unused) + { + {200, 200, 200, 200}, + {200, 200, 200, 200}, + {200, 200, 200, 200}, + } + }; + 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 +326,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 +338,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 +352,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 +395,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 +407,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 +426,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 +440,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 +499,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 +647,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_