Skip to content

Commit 2d896a9

Browse files
zbian99AngeloTadeucciclaudeZintixx
authored
Fix: skill hitting caster due to shared Targets across attack points (#650)
* Update Actor.cs * Update Actor.cs ### Fix In `Actor.TargetAttack()`, for Hostile attacks only: 1. Skip applying damage to the caster 2. Filter caster from targets when applying skill effects 3. Skip creating splash skills at the caster's position The Hostile-only condition ensures Friendly skills (self-buffs, AoE heals) continue to work correctly. * move filter to target select stage * Refine fix: clear Targets per attack point, simplify downstream caster checks - TrySetAttackPoint now calls Targets.Clear() to ensure each attack point starts with a clean target set. This is the authoritative fix for caster bleed-through from a Friendly AP into subsequent Hostile APs. - Actor.TargetAttack: remove per-ApplyTarget caster-skip checks (now redundant), pre-compute splashEffects, and unconditionally skip the caster in the splash loop with a diagnostic warning for the edge case where CubeMagicPathId == 0. - SkillHandler.HandleTarget: remove redundant caster-skip guard for Hostile targets (player ID is never in Field.Mobs, and Targets.Clear() handles cross-AP bleed). - SkillHandler: downgrade over-target-count log to Warning and include BounceCount for better context. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * skill targeting fix * early exit * format --------- Co-authored-by: Ângelo Tadeucci <angelo_tadeucci@hotmail.com.br> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Zin <62830952+Zintixx@users.noreply.github.com>
1 parent fe0b8bd commit 2d896a9

10 files changed

Lines changed: 77 additions & 15 deletions

File tree

Maple2.File.Ingest/Mapper/SkillMapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ private static SkillMetadataRange Convert(RegionSkill region) {
163163
RotateZDegree: region.rangeZRotateDegree,
164164
RangeAdd: region.rangeAdd,
165165
RangeOffset: region.rangeOffset,
166-
IncludeCaster: (SkillTargetType) region.includeCaster,
166+
IncludeCaster: (IncludeCasterType) region.includeCaster,
167167
ApplyTarget: (ApplyTargetType) region.applyTarget,
168168
CastTarget: (SkillTargetType) region.castTarget
169169
);

Maple2.Model/Enum/Skill.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ public enum SkillTargetType {
8989
RegionPet = 8,
9090
}
9191

92+
public enum IncludeCasterType {
93+
Exclude = 0,
94+
Priority = 1,
95+
Last = 2,
96+
}
97+
9298
public enum DotTargetType {
9399
Caster = 0,
94100
Owner = 1,

Maple2.Model/Metadata/SkillMetadata.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public record SkillMetadataRange(
129129
float RotateZDegree,
130130
Vector3 RangeAdd,
131131
Vector3 RangeOffset,
132-
SkillTargetType IncludeCaster, // 0,1,2
132+
IncludeCasterType IncludeCaster,
133133
ApplyTargetType ApplyTarget, // 0,1,2,3,5,6,7,8
134134
SkillTargetType CastTarget); // 0,1,2,3,4,5,7
135135

Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ public void AddSkill(SkillRecord record) {
642642
}
643643
}
644644

645-
public IEnumerable<IActor> GetTargets(IActor caster, Prism[] prisms, ApplyTargetType targetType, int limit, ICollection<IActor>? ignore = null) {
645+
private IEnumerable<IActor> GetTargetPool(IActor caster, Prism[] prisms, ApplyTargetType targetType, int limit, ICollection<IActor>? ignore) {
646646
switch (targetType) {
647647
case ApplyTargetType.Friendly:
648648
if (caster is FieldNpc) {
@@ -672,6 +672,35 @@ public IEnumerable<IActor> GetTargets(IActor caster, Prism[] prisms, ApplyTarget
672672
}
673673
}
674674

675+
public IEnumerable<IActor> GetTargets(IActor caster, Prism[] prisms, SkillMetadataRange range, int targetCount, ICollection<IActor>? ignore = null) {
676+
if (targetCount <= 0) {
677+
return [];
678+
}
679+
680+
// Caster is always excluded from the pool; re-added explicitly per IncludeCaster semantics
681+
ICollection<IActor> poolIgnore = ignore != null ? [.. ignore, caster] : [caster];
682+
683+
switch (range.IncludeCaster) {
684+
case IncludeCasterType.Priority: {
685+
// Caster guaranteed as first target; pool fills remaining slots
686+
IActor[] pool = GetTargetPool(caster, prisms, range.ApplyTarget, targetCount - 1, poolIgnore).ToArray();
687+
return Enumerable.Repeat<IActor>(caster, 1).Concat(pool);
688+
}
689+
case IncludeCasterType.Last: {
690+
// Pool fills all slots; caster appended only if fewer than targetCount were found
691+
IActor[] pool = GetTargetPool(caster, prisms, range.ApplyTarget, targetCount, poolIgnore).ToArray();
692+
return pool.Length < targetCount ? pool.Concat(Enumerable.Repeat<IActor>(caster, 1)) : pool;
693+
}
694+
default: // Exclude
695+
return GetTargetPool(caster, prisms, range.ApplyTarget, targetCount, poolIgnore);
696+
}
697+
}
698+
699+
public IEnumerable<IActor> GetTargets(SkillRecord record, ICollection<IActor>? ignore = null) {
700+
Prism[] prisms = [record.Attack.Range.GetPrism(record.ImpactPosition, record.Rotation.Z)];
701+
return GetTargets(record.Caster, prisms, record.Attack.Range, record.Attack.TargetCount, ignore);
702+
}
703+
675704
public void RemoveSkill(int objectId) {
676705
if (fieldSkills.Remove(objectId, out _)) {
677706
Broadcast(RegionSkillPacket.Remove(objectId));

Maple2.Server.Game/Manager/Field/FieldManager/IField.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ public virtual void Init() { }
6262
public void AddSkill(SkillMetadata metadata, int interval, in Vector3 position, in Vector3 rotation = default, int triggerId = 0);
6363
public void AddSkill(SkillRecord record);
6464
public void AddSkill(IActor caster, SkillEffectMetadata effect, Vector3[] points, in Vector3 rotation = default);
65-
public IEnumerable<IActor> GetTargets(IActor actor, Prism[] prisms, ApplyTargetType targetType, int limit, ICollection<IActor>? ignore = null);
65+
public IEnumerable<IActor> GetTargets(IActor caster, Prism[] prisms, SkillMetadataRange range, int targetCount, ICollection<IActor>? ignore = null);
66+
public IEnumerable<IActor> GetTargets(SkillRecord record, ICollection<IActor>? ignore = null);
6667
public void RemoveSkill(int objectId);
6768
public void Broadcast(ByteWriter packet, GameSession? sender = null);
6869
public void BroadcastAiMessage(ByteWriter packet);

Maple2.Server.Game/Model/Field/Actor/Actor.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,21 +215,33 @@ public virtual void TargetAttack(SkillRecord record) {
215215
Direction = record.Direction,
216216
};
217217

218+
SkillEffectMetadata[] splashEffects = record.Attack.Skills.Where(e => e.Splash != null).ToArray();
219+
218220
foreach (IActor target in record.Targets.Values) {
219221
target.ApplyDamage(this, damage, record.Attack);
220222
}
221223

222224
Field.Broadcast(SkillDamagePacket.Damage(damage));
223225

224-
225226
ApplyEffects(record.Attack.Skills, record.Caster, this, skillId: record.SkillId, targets: record.Targets.Values.ToArray());
226227
ApplyEffects(record.Attack.SkillsOnDamage, record.Caster, damage, record.Targets.Values.ToArray());
228+
229+
// Create splash skills at target positions.
230+
// Always skip the caster: when IncludeCaster is set, the attack also has a CubeMagicPathId
231+
// which handles splash placement independently — this loop must not create a duplicate.
227232
foreach (IActor target in record.Targets.Values) {
228-
foreach (SkillEffectMetadata effect in record.Attack.Skills.Where(e => e.Splash != null)) {
233+
if (target.ObjectId == record.Caster.ObjectId) {
234+
if (splashEffects.Length > 0 && record.Attack.CubeMagicPathId == 0) {
235+
Logger.Warning("[TargetAttack] SkillId={SkillId} AttackPoint={AttackPoint} IncludeCaster={IncludeCaster} — caster skipped in splash loop but CubeMagicPathId=0. Splash may be lost.",
236+
record.SkillId, record.AttackPoint, record.Attack.Range.IncludeCaster);
237+
}
238+
continue;
239+
}
240+
241+
foreach (SkillEffectMetadata effect in splashEffects) {
229242
Field.AddSkill(record.Caster, effect, [target.Position], record.Caster.Rotation);
230243
}
231244
}
232-
233245
}
234246

235247
public virtual void SkillAttackPoint(SkillRecord record, byte attackPoint) {

Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public void SkillCastAttack(SkillRecord cast, byte attackPoint, List<IActor> att
7171
Tools.Collision.Prism attackPrism = attack.Range.GetPrism(actor.Position, actor.Rotation.Z);
7272
var resolvedTargets = new List<IActor>();
7373
int queryLimit = attack.TargetCount > 0 ? attack.TargetCount : 1;
74-
foreach (IActor target in actor.Field.GetTargets(actor, [attackPrism], attack.Range.ApplyTarget, queryLimit)) {
74+
foreach (IActor target in actor.Field.GetTargets(actor, [attackPrism], attack.Range, queryLimit)) {
7575
resolvedTargets.Add(target);
7676
}
7777

Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public override void Update(long tickCount) {
9595
Prism[] prisms = Points
9696
.Select(point => attack.Range.GetPrism(point, skillAngle, attack.Range.ApplyTarget))
9797
.ToArray();
98-
if (Field.GetTargets(Caster, prisms, attack.Range.ApplyTarget, attack.TargetCount).Any()) {
98+
if (Field.GetTargets(Caster, prisms, attack.Range, attack.TargetCount).Any()) {
9999
Active = true;
100100
goto activated;
101101
}
@@ -148,8 +148,8 @@ public override void Update(long tickCount) {
148148
var prism = new Prism(circle, position.Z, box.Z);
149149

150150
targets = attack.Arrow.BounceOverlap
151-
? Field.GetTargets(Caster, [prism], record.Attack.Range.ApplyTarget, 1, targets).ToArray()
152-
: Field.GetTargets(Caster, [prism], record.Attack.Range.ApplyTarget, 1, bounceTargets).ToArray();
151+
? Field.GetTargets(Caster, [prism], record.Attack.Range, 1, targets).ToArray()
152+
: Field.GetTargets(Caster, [prism], record.Attack.Range, 1, bounceTargets).ToArray();
153153
if (targets.Length <= 0) {
154154
break;
155155
}
@@ -185,7 +185,7 @@ public override void Update(long tickCount) {
185185
Prism[] prisms = Points
186186
.Select(point => attack.Range.GetPrism(point, skillAngle, attack.Range.ApplyTarget))
187187
.ToArray();
188-
IActor[] targets = Field.GetTargets(Caster, prisms, attack.Range.ApplyTarget, attack.TargetCount).ToArray();
188+
IActor[] targets = Field.GetTargets(Caster, prisms, attack.Range, attack.TargetCount).ToArray();
189189
// if (targets.Length > 0) {
190190
// logger.Debug("[{Tick}] {ObjectId}:{AttackPoint} Targeting: {Count}/{Limit} {Type}",
191191
// NextTick, ObjectId, attack.Point, targets.Length, attack.TargetCount, attack.Range.ApplyTarget);

Maple2.Server.Game/Model/Skill/SkillRecord.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ public bool TrySetAttackPoint(byte attackPoint) {
5858
}
5959

6060
AttackPoint = attackPoint;
61+
// Each attack point must start with a clean target set.
62+
// Without this, targets from a prior attack point (e.g. a Friendly AP that includes the caster)
63+
// bleed into subsequent attack points, causing incorrect damage and splash placement.
64+
Targets.Clear();
6165
return true;
6266
}
6367

Maple2.Server.Game/PacketHandlers/SkillHandler.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Maple2.Server.Game.PacketHandlers.Field;
1212
using Maple2.Server.Game.Packets;
1313
using Maple2.Server.Game.Session;
14+
using Maple2.Server.Game.Util;
1415

1516
namespace Maple2.Server.Game.PacketHandlers;
1617

@@ -209,8 +210,10 @@ private void HandleTarget(GameSession session, IByteReader packet) {
209210

210211
byte count = packet.ReadByte();
211212
if (count > record.Attack.TargetCount) {
212-
Logger.Error("Attack too many targets {Count} for {Record}", count, record);
213-
// Adjust count
213+
// Skills with BounceCount send all bounce targets in one packet but TargetCount is per-bounce.
214+
// This may indicate an unimplemented bounce mechanic rather than a true exploit.
215+
Logger.Warning("SkillId={SkillId} AttackPoint={AttackPoint} sent {Count} targets but TargetCount={TargetCount} — clamping. BounceCount={BounceCount}",
216+
record.SkillId, attackPoint, count, record.Attack.TargetCount, record.Attack.Arrow.BounceCount);
214217
count = (byte) record.Attack.TargetCount;
215218
}
216219

@@ -223,7 +226,9 @@ private void HandleTarget(GameSession session, IByteReader packet) {
223226
session.Send(NoticePacket.Message($"Skill.Attack.Damage: {skillUid}; AttackPoint: {attackPoint}"));
224227
}
225228

226-
for (byte i = 0; i < count; i++) {
229+
// Although the client feeds us this information and is right, we cannot rely on it and must validate it
230+
// we should keep it just to ensure what the server gathers as proper targets is the same as the client
231+
/*for (byte i = 0; i < count; i++) {
227232
int targetId = packet.ReadInt();
228233
if (record.Targets.ContainsKey(targetId)) {
229234
continue;
@@ -252,6 +257,11 @@ private void HandleTarget(GameSession session, IByteReader packet) {
252257
Logger.Debug("Unhandled Target-SkillEntity:{Entity}", record.Attack.Range.ApplyTarget);
253258
continue;
254259
}
260+
}*/
261+
262+
IEnumerable<IActor> targets = session.Field.GetTargets(record);
263+
foreach (IActor target in targets) {
264+
record.Targets.TryAdd(target.ObjectId, target);
255265
}
256266
session.Player.TargetAttack(record);
257267
}

0 commit comments

Comments
 (0)