From 45f9ffdf348fefb9e1aea16719402e46528a392f Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 09:39:17 -0500 Subject: [PATCH] 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 --- dGame/dBehaviors/AreaOfEffectBehavior.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/dGame/dBehaviors/AreaOfEffectBehavior.cpp b/dGame/dBehaviors/AreaOfEffectBehavior.cpp index 6f1b76fd1..0af255cda 100644 --- a/dGame/dBehaviors/AreaOfEffectBehavior.cpp +++ b/dGame/dBehaviors/AreaOfEffectBehavior.cpp @@ -1,5 +1,7 @@ #include "AreaOfEffectBehavior.h" +#include + #include #include "EntityManager.h" @@ -47,7 +49,10 @@ void AreaOfEffectBehavior::Handle(BehaviorContext* context, RakNet::BitStream& b 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 uniqueTargets(targets.begin(), targets.end()); + for (auto target : uniqueTargets) { branch.target = target; this->m_action->Handle(context, bitStream, branch); } @@ -96,9 +101,11 @@ void AreaOfEffectBehavior::Calculate(BehaviorContext* context, RakNet::BitStream bitStream.Write(target->GetObjectID()); } - // then cast all the actions - for (auto* target : targets) { - branch.target = target->GetObjectID(); + // then cast the action once per unique target in ascending id order, the order clients read them in + std::set uniqueTargets; + for (auto* target : targets) uniqueTargets.insert(target->GetObjectID()); + for (const auto target : uniqueTargets) { + branch.target = target; this->m_action->Calculate(context, bitStream, branch); } PlayFx(u"cast", context->originator);