fix(combat): area attacks handle each target once, in the client's order (issues 917, 1189)

The client's AreaOfEffectBehavior::Cast (0x004ec590) writes every target id, then runs and writes the action once
per unique target in ascending id order. The server ran it for every listed target in list order, so a target
listed twice (the caster with a magnet, Everlasting items' refill) was handled twice, and later targets' data was
read against the wrong target. Handle and the server's own Calculate now go through the unique ids in ascending
order. Check: Thumpin' Bass / Flowin' MC refill once; Shinobi charge with a magnet gives imagination once; area
attacks on several enemies still hit each of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 09:39:17 -05:00
parent 32fe441d57
commit 45f9ffdf34

View File

@@ -1,5 +1,7 @@
#include "AreaOfEffectBehavior.h" #include "AreaOfEffectBehavior.h"
#include <set>
#include <vector> #include <vector>
#include "EntityManager.h" #include "EntityManager.h"
@@ -47,7 +49,10 @@ void AreaOfEffectBehavior::Handle(BehaviorContext* context, RakNet::BitStream& b
targets.push_back(target); targets.push_back(target);
} }
for (auto target : targets) { // The client writes the action once per unique target, in ascending id order (AreaOfEffectBehavior::Cast,
// 0x004ec590, runs it over a set of the ids), so a target listed twice is handled once
const std::set<LWOOBJID> uniqueTargets(targets.begin(), targets.end());
for (auto target : uniqueTargets) {
branch.target = target; branch.target = target;
this->m_action->Handle(context, bitStream, branch); this->m_action->Handle(context, bitStream, branch);
} }
@@ -96,9 +101,11 @@ void AreaOfEffectBehavior::Calculate(BehaviorContext* context, RakNet::BitStream
bitStream.Write(target->GetObjectID()); bitStream.Write(target->GetObjectID());
} }
// then cast all the actions // then cast the action once per unique target in ascending id order, the order clients read them in
for (auto* target : targets) { std::set<LWOOBJID> uniqueTargets;
branch.target = target->GetObjectID(); for (auto* target : targets) uniqueTargets.insert(target->GetObjectID());
for (const auto target : uniqueTargets) {
branch.target = target;
this->m_action->Calculate(context, bitStream, branch); this->m_action->Calculate(context, bitStream, branch);
} }
PlayFx(u"cast", context->originator); PlayFx(u"cast", context->originator);