mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
The client's TacArcBehavior::DoHit (0x00fb10c0) writes the closest max-targets ids from a set, so ascending and each once, then the action data per id in that same order; DoUnserializeBS (0x00fb26a0) reads them the same way and skips empty ids. The server handled the targets in list order and skipped ids whose object it could not find without reading their action data, so every later target read the wrong bits (several pirates under a Doom Slicer). Handle now reads the action for every listed id in ascending order, and the server's own casts write ids and actions in ascending order after picking the closest targets. TacArcBehavior::Cast (0x00fb2d10): a picked target that passes the filter gets the action with no TacArc data (the server's own cast now calculates that action instead of handling it); otherwise the target is dropped, and an arc measured from the target's position writes nothing. Check in game: Doom Slicer and multi-target katanas on groups of pirates/admirals damage each of them; apes still take damage during their stun; enemies with arc attacks (apes, Maelstrom horsemen) still hit players. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
267 lines
10 KiB
C++
267 lines
10 KiB
C++
#include "TacArcBehavior.h"
|
|
#include "BehaviorBranchContext.h"
|
|
#include "Game.h"
|
|
#include "Logger.h"
|
|
#include "Entity.h"
|
|
#include "BehaviorContext.h"
|
|
#include "BaseCombatAIComponent.h"
|
|
#include "EntityManager.h"
|
|
#include "QuickBuildComponent.h"
|
|
#include "DestroyableComponent.h"
|
|
|
|
#include <set>
|
|
#include <vector>
|
|
|
|
void TacArcBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bitStream, BehaviorBranchContext branch) {
|
|
// TacArcBehavior::Cast (0x00fb2d10): a picked target that passes the filter gets the action and no TacArc data
|
|
if (this->m_usePickedTarget && branch.target != LWOOBJID_EMPTY) {
|
|
std::vector<Entity*> targets = { Game::entityManager->GetEntity(branch.target) };
|
|
context->FilterTargets(targets, this->m_ignoreFactionList, this->m_includeFactionList, this->m_targetSelf, this->m_targetEnemy, this->m_targetFriend, this->m_targetTeam);
|
|
if (!targets.empty()) {
|
|
this->m_action->Handle(context, bitStream, branch);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Otherwise the client drops the target, so an arc around the target's position has nothing to measure from and
|
|
// writes nothing
|
|
branch.target = LWOOBJID_EMPTY;
|
|
if (this->m_useTargetPostion) return;
|
|
|
|
bool hasTargets = false;
|
|
if (!bitStream.Read(hasTargets)) {
|
|
LOG("Unable to read hasTargets from bitStream, aborting Handle! %i", bitStream.GetNumberOfUnreadBits());
|
|
return;
|
|
};
|
|
|
|
if (this->m_checkEnv) {
|
|
bool blocked = false;
|
|
|
|
if (!bitStream.Read(blocked)) {
|
|
LOG("Unable to read blocked from bitStream, aborting Handle! %i", bitStream.GetNumberOfUnreadBits());
|
|
return;
|
|
};
|
|
|
|
if (blocked) {
|
|
this->m_blockedAction->Handle(context, bitStream, branch);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (hasTargets) {
|
|
std::set<LWOOBJID> targets;
|
|
if (!ReadTargets(bitStream, this->m_maxTargets, targets)) return;
|
|
|
|
// The caster wrote the action for each of these targets, so it is read for each, even one that is gone here
|
|
for (const auto target : targets) {
|
|
branch.target = target;
|
|
this->m_action->Handle(context, bitStream, branch);
|
|
}
|
|
} else this->m_missAction->Handle(context, bitStream, branch);
|
|
}
|
|
|
|
bool TacArcBehavior::ReadTargets(RakNet::BitStream& bitStream, const uint32_t maxTargets, std::set<LWOOBJID>& targets) {
|
|
uint32_t count = 0;
|
|
if (!bitStream.Read(count)) {
|
|
LOG("Unable to read count from bitStream, aborting Handle! %i", bitStream.GetNumberOfUnreadBits());
|
|
return false;
|
|
}
|
|
|
|
if (count > maxTargets) {
|
|
LOG("Bitstream has too many targets Max:%i Recv:%i", maxTargets, count);
|
|
return false;
|
|
}
|
|
|
|
// TacArcBehavior::DoUnserializeBS (0x00fb26a0) puts the ids in a set: ascending, each once, no empty id
|
|
for (auto i = 0u; i < count; i++) {
|
|
LWOOBJID id{};
|
|
if (!bitStream.Read(id)) {
|
|
LOG("Unable to read id from bitStream, aborting Handle! %i", bitStream.GetNumberOfUnreadBits());
|
|
return false;
|
|
}
|
|
|
|
if (id == LWOOBJID_EMPTY) {
|
|
LOG("Bitstream has LWOOBJID_EMPTY as a target!");
|
|
continue;
|
|
}
|
|
targets.insert(id);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::set<LWOOBJID> TacArcBehavior::WriteTargets(RakNet::BitStream& bitStream, const std::vector<LWOOBJID>& closestFirst, const uint32_t maxTargets) {
|
|
// TacArcBehavior::DoHit (0x00fb10c0): the closest maxTargets targets, written and acted on in ascending id order
|
|
std::set<LWOOBJID> targets;
|
|
for (const auto target : closestFirst) {
|
|
if (targets.size() >= maxTargets) break;
|
|
if (target != LWOOBJID_EMPTY) targets.insert(target);
|
|
}
|
|
|
|
bitStream.Write<uint32_t>(targets.size());
|
|
for (const auto target : targets) bitStream.Write(target);
|
|
return targets;
|
|
}
|
|
|
|
void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitStream, BehaviorBranchContext branch) {
|
|
auto* self = Game::entityManager->GetEntity(context->originator);
|
|
if (self == nullptr) {
|
|
LOG("Invalid self for (%llu)!", context->originator);
|
|
return;
|
|
}
|
|
|
|
std::vector<Entity*> targets = {};
|
|
if (this->m_usePickedTarget && branch.target != LWOOBJID_EMPTY) {
|
|
auto target = Game::entityManager->GetEntity(branch.target);
|
|
targets.push_back(target);
|
|
context->FilterTargets(targets, this->m_ignoreFactionList, this->m_includeFactionList, this->m_targetSelf, this->m_targetEnemy, this->m_targetFriend, this->m_targetTeam);
|
|
if (!targets.empty()) {
|
|
this->m_action->Calculate(context, bitStream, branch);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// As the client: past the picked target check there is no target
|
|
branch.target = LWOOBJID_EMPTY;
|
|
|
|
auto* combatAi = self->GetComponent<BaseCombatAIComponent>();
|
|
|
|
const auto casterPosition = self->GetPosition();
|
|
|
|
auto reference = self->GetPosition() + m_offset;
|
|
|
|
targets.clear();
|
|
|
|
std::vector<Entity*> validTargets = Game::entityManager->GetEntitiesByProximity(reference, this->m_maxRange);
|
|
|
|
// filter all valid targets, based on whether we target enemies or friends
|
|
context->FilterTargets(validTargets, this->m_ignoreFactionList, this->m_includeFactionList, this->m_targetSelf, this->m_targetEnemy, this->m_targetFriend, this->m_targetTeam);
|
|
|
|
for (auto validTarget : validTargets) {
|
|
if (std::find(targets.begin(), targets.end(), validTarget) != targets.end()) continue;
|
|
if (validTarget->GetIsDead()) continue;
|
|
|
|
const auto targetPos = validTarget->GetPosition();
|
|
|
|
// make sure we aren't too high or low in comparison to the target
|
|
if (targetPos.y > (reference.y + m_upperBound) || targetPos.y < (reference.y + m_lowerBound))
|
|
continue;
|
|
|
|
const auto forward = QuatUtils::Forward(self->GetRotation());
|
|
|
|
// forward is a normalized vector of where the caster is facing.
|
|
// targetPos is the position of the target.
|
|
// reference is the position of the caster.
|
|
// If we cast a ray forward from the caster, does it come within m_farWidth of the target?
|
|
|
|
const auto distance = Vector3::Distance(reference, targetPos);
|
|
|
|
if (m_method == 2) {
|
|
NiPoint3 rayPoint = casterPosition + forward * distance;
|
|
if (m_farWidth > 0 && Vector3::DistanceSquared(rayPoint, targetPos) > this->m_farWidth * this->m_farWidth)
|
|
continue;
|
|
}
|
|
|
|
auto normalized = (reference - targetPos) / distance;
|
|
const float degreeAngle = std::abs(Vector3::Angle(forward, normalized) * (180 / 3.14) - 180);
|
|
if (distance >= this->m_minRange && this->m_maxRange >= distance && degreeAngle <= 2 * this->m_angle) {
|
|
targets.push_back(validTarget);
|
|
}
|
|
}
|
|
|
|
std::sort(targets.begin(), targets.end(), [this, reference, combatAi](Entity* a, Entity* b) {
|
|
const auto aDistance = Vector3::DistanceSquared(reference, a->GetPosition());
|
|
const auto bDistance = Vector3::DistanceSquared(reference, b->GetPosition());
|
|
|
|
return aDistance < bDistance;
|
|
});
|
|
|
|
|
|
if (m_useAttackPriority) {
|
|
// this should be using the attack priority column on the destroyable component
|
|
// We want targets with no threat level to remain the same order as above
|
|
// std::stable_sort(targets.begin(), targets.end(), [combatAi](Entity* a, Entity* b) {
|
|
// const auto aThreat = combatAi->GetThreat(a->GetObjectID());
|
|
// const auto bThreat = combatAi->GetThreat(b->GetObjectID());
|
|
|
|
// If enabled for this behavior, prioritize threat over distance
|
|
// return aThreat > bThreat;
|
|
// });
|
|
}
|
|
|
|
// After we've sorted and found our closest targets, size the vector down in case there are too many
|
|
if (m_maxTargets > 0 && targets.size() > m_maxTargets) targets.resize(m_maxTargets);
|
|
const auto hit = !targets.empty();
|
|
bitStream.Write(hit);
|
|
|
|
if (this->m_checkEnv) {
|
|
const auto blocked = false; // TODO
|
|
bitStream.Write(blocked);
|
|
}
|
|
|
|
if (hit) {
|
|
if (combatAi) combatAi->LookAt(targets[0]->GetPosition());
|
|
|
|
context->foundTarget = true; // We want to continue with this behavior
|
|
|
|
std::vector<LWOOBJID> closestFirst;
|
|
for (const auto* target : targets) closestFirst.push_back(target->GetObjectID());
|
|
for (const auto target : WriteTargets(bitStream, closestFirst, this->m_maxTargets)) {
|
|
branch.target = target;
|
|
this->m_action->Calculate(context, bitStream, branch);
|
|
}
|
|
} else {
|
|
this->m_missAction->Calculate(context, bitStream, branch);
|
|
}
|
|
}
|
|
|
|
void TacArcBehavior::Load() {
|
|
this->m_maxRange = GetFloat("max range");
|
|
this->m_height = GetFloat("height", 2.2f);
|
|
this->m_distanceWeight = GetFloat("distance_weight", 0.0f);
|
|
this->m_angleWeight = GetFloat("angle_weight", 0.0f);
|
|
this->m_angle = GetFloat("angle", 45.0f);
|
|
this->m_minRange = GetFloat("min range", 0.0f);
|
|
this->m_offset = NiPoint3(
|
|
GetFloat("offset_x", 0.0f),
|
|
GetFloat("offset_y", 0.0f),
|
|
GetFloat("offset_z", 0.0f)
|
|
);
|
|
// https://explorer.lu/skills/behaviors/6212/6203 HACK: i cant figure out why the dragon fire wall doesnt work with the offset, probably has to be fixed with the near/far height parameters
|
|
if (m_behaviorId == 6203) {
|
|
this->m_offset = NiPoint3Constant::ZERO;
|
|
}
|
|
this->m_method = GetInt("method", 1);
|
|
this->m_upperBound = GetFloat("upper_bound", 4.4f);
|
|
this->m_lowerBound = GetFloat("lower_bound", 0.4f) - 5.0f; // Makes it so players and objects can still be targetted when slightly below the caster. FIXME: use bounding spheres at some point
|
|
this->m_usePickedTarget = GetBoolean("use_picked_target", false);
|
|
this->m_useTargetPostion = GetBoolean("use_target_position", false);
|
|
this->m_checkEnv = GetBoolean("check_env", false);
|
|
this->m_useAttackPriority = GetBoolean("use_attack_priority", false);
|
|
|
|
this->m_action = GetAction("action");
|
|
this->m_missAction = GetAction("miss action");
|
|
this->m_blockedAction = GetAction("blocked action");
|
|
|
|
this->m_maxTargets = GetInt("max targets", 100);
|
|
if (this->m_maxTargets == 0) this->m_maxTargets = 100;
|
|
|
|
this->m_farHeight = GetFloat("far_height", 5.0f);
|
|
this->m_farWidth = GetFloat("far_width", 5.0f);
|
|
this->m_nearHeight = GetFloat("near_height", 5.0f);
|
|
this->m_nearWidth = GetFloat("near_width", 5.0f);
|
|
|
|
// params after this are needed for filter targets
|
|
const auto parameters = GetParameterNames();
|
|
for (const auto& parameter : parameters) {
|
|
if (parameter.first.rfind("include_faction", 0) == 0) {
|
|
this->m_includeFactionList.push_front(parameter.second);
|
|
} else if (parameter.first.rfind("ignore_faction", 0) == 0) {
|
|
this->m_ignoreFactionList.push_front(parameter.second);
|
|
}
|
|
}
|
|
this->m_targetSelf = GetBoolean("target_caster", false);
|
|
this->m_targetEnemy = GetBoolean("target_enemy", false);
|
|
this->m_targetFriend = GetBoolean("target_friend", false);
|
|
this->m_targetTeam = GetBoolean("target_team", false);
|
|
}
|