From 8544afebfdb06b3e04453e6f71578bbf58dacca6 Mon Sep 17 00:00:00 2001 From: Zheng Bian Date: Thu, 19 Feb 2026 22:40:15 +0800 Subject: [PATCH 1/7] Update Actor.cs --- Maple2.Server.Game/Model/Field/Actor/Actor.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index 427af9f9..72cca56a 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -213,6 +213,10 @@ public virtual void TargetAttack(SkillRecord record) { }; foreach (IActor target in record.Targets.Values) { + // Skip applying damage to caster for hostile skills (caster may be in targets from a previous Friendly attack point) + if (record.Attack.Range.ApplyTarget == ApplyTargetType.Hostile && target.ObjectId == record.Caster.ObjectId) { + continue; + } target.ApplyDamage(this, damage, record.Attack); } @@ -221,12 +225,20 @@ public virtual void TargetAttack(SkillRecord record) { ApplyEffects(record.Attack.Skills, record.Caster, this, skillId: record.SkillId, targets: record.Targets.Values.ToArray()); ApplyEffects(record.Attack.SkillsOnDamage, record.Caster, damage, record.Targets.Values.ToArray()); + + // Create splash skills at target positions, excluding the caster foreach (IActor target in record.Targets.Values) { + // Skip creating splash skill at caster's own position + if (target.ObjectId == record.Caster.ObjectId) { + continue; + } + foreach (SkillEffectMetadata effect in record.Attack.Skills.Where(e => e.Splash != null)) { Field.AddSkill(record.Caster, effect, [target.Position], record.Caster.Rotation); } } + } public virtual void SkillAttackPoint(SkillRecord record, byte attackPoint) { From 6eaf6c55d4f7e5ab5613b07fdf0c4cf195ce7579 Mon Sep 17 00:00:00 2001 From: Zheng Bian Date: Fri, 20 Feb 2026 00:24:48 +0800 Subject: [PATCH 2/7] 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. --- Maple2.Server.Game/Model/Field/Actor/Actor.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index 72cca56a..0a866bf6 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -222,14 +222,19 @@ public virtual void TargetAttack(SkillRecord record) { Field.Broadcast(SkillDamagePacket.Damage(damage)); + // Filter out caster from targets for Hostile attacks to prevent effects from being applied to caster + IActor[] effectTargets = record.Attack.Range.ApplyTarget == ApplyTargetType.Hostile + ? record.Targets.Values.Where(t => t.ObjectId != record.Caster.ObjectId).ToArray() + : record.Targets.Values.ToArray(); - ApplyEffects(record.Attack.Skills, record.Caster, this, skillId: record.SkillId, targets: record.Targets.Values.ToArray()); - ApplyEffects(record.Attack.SkillsOnDamage, record.Caster, damage, record.Targets.Values.ToArray()); + ApplyEffects(record.Attack.Skills, record.Caster, this, skillId: record.SkillId, targets: effectTargets); + ApplyEffects(record.Attack.SkillsOnDamage, record.Caster, damage, effectTargets); - // Create splash skills at target positions, excluding the caster + // Create splash skills at target positions foreach (IActor target in record.Targets.Values) { - // Skip creating splash skill at caster's own position - if (target.ObjectId == record.Caster.ObjectId) { + // Skip creating splash at caster's position for hostile attacks only + // (caster may be in Targets from a prior Friendly attack point) + if (record.Attack.Range.ApplyTarget == ApplyTargetType.Hostile && target.ObjectId == record.Caster.ObjectId) { continue; } @@ -237,8 +242,6 @@ public virtual void TargetAttack(SkillRecord record) { Field.AddSkill(record.Caster, effect, [target.Position], record.Caster.Rotation); } } - - } public virtual void SkillAttackPoint(SkillRecord record, byte attackPoint) { From e1cc040f22af06061971828cb1f87a43cdb98da1 Mon Sep 17 00:00:00 2001 From: Zheng Bian Date: Fri, 20 Feb 2026 01:20:19 +0800 Subject: [PATCH 3/7] move filter to target select stage --- Maple2.Server.Game/Model/Field/Actor/Actor.cs | 21 +++---------------- .../PacketHandlers/SkillHandler.cs | 4 ++++ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index 0a866bf6..427af9f9 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -213,35 +213,20 @@ public virtual void TargetAttack(SkillRecord record) { }; foreach (IActor target in record.Targets.Values) { - // Skip applying damage to caster for hostile skills (caster may be in targets from a previous Friendly attack point) - if (record.Attack.Range.ApplyTarget == ApplyTargetType.Hostile && target.ObjectId == record.Caster.ObjectId) { - continue; - } target.ApplyDamage(this, damage, record.Attack); } Field.Broadcast(SkillDamagePacket.Damage(damage)); - // Filter out caster from targets for Hostile attacks to prevent effects from being applied to caster - IActor[] effectTargets = record.Attack.Range.ApplyTarget == ApplyTargetType.Hostile - ? record.Targets.Values.Where(t => t.ObjectId != record.Caster.ObjectId).ToArray() - : record.Targets.Values.ToArray(); - - ApplyEffects(record.Attack.Skills, record.Caster, this, skillId: record.SkillId, targets: effectTargets); - ApplyEffects(record.Attack.SkillsOnDamage, record.Caster, damage, effectTargets); - // Create splash skills at target positions + ApplyEffects(record.Attack.Skills, record.Caster, this, skillId: record.SkillId, targets: record.Targets.Values.ToArray()); + ApplyEffects(record.Attack.SkillsOnDamage, record.Caster, damage, record.Targets.Values.ToArray()); foreach (IActor target in record.Targets.Values) { - // Skip creating splash at caster's position for hostile attacks only - // (caster may be in Targets from a prior Friendly attack point) - if (record.Attack.Range.ApplyTarget == ApplyTargetType.Hostile && target.ObjectId == record.Caster.ObjectId) { - continue; - } - foreach (SkillEffectMetadata effect in record.Attack.Skills.Where(e => e.Splash != null)) { Field.AddSkill(record.Caster, effect, [target.Position], record.Caster.Rotation); } } + } public virtual void SkillAttackPoint(SkillRecord record, byte attackPoint) { diff --git a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs index 2680d707..2fe01c48 100644 --- a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs @@ -232,6 +232,10 @@ private void HandleTarget(GameSession session, IByteReader packet) { switch (record.Attack.Range.ApplyTarget) { case ApplyTargetType.Hostile: + // Skip adding caster to hostile targets (caster may have been sent by client from a prior Friendly attack point) + if (targetId == session.Player.ObjectId) { + continue; + } if (session.Field.Mobs.TryGetValue(targetId, out FieldNpc? npc)) { record.Targets.TryAdd(npc.ObjectId, npc); } From bd75f061671eef3b1f6a1fe970c00835835b7db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Thu, 19 Feb 2026 14:45:49 -0300 Subject: [PATCH 4/7] 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 --- Maple2.Server.Game/Model/Field/Actor/Actor.cs | 18 +++++++++++++++--- Maple2.Server.Game/Model/Skill/SkillRecord.cs | 4 ++++ .../PacketHandlers/SkillHandler.cs | 10 ++++------ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index 427af9f9..8183e5bc 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -212,21 +212,33 @@ public virtual void TargetAttack(SkillRecord record) { Direction = record.Direction, }; + SkillEffectMetadata[] splashEffects = record.Attack.Skills.Where(e => e.Splash != null).ToArray(); + foreach (IActor target in record.Targets.Values) { target.ApplyDamage(this, damage, record.Attack); } Field.Broadcast(SkillDamagePacket.Damage(damage)); - ApplyEffects(record.Attack.Skills, record.Caster, this, skillId: record.SkillId, targets: record.Targets.Values.ToArray()); ApplyEffects(record.Attack.SkillsOnDamage, record.Caster, damage, record.Targets.Values.ToArray()); + + // Create splash skills at target positions. + // Always skip the caster: when IncludeCaster is set, the attack also has a CubeMagicPathId + // which handles splash placement independently — this loop must not create a duplicate. foreach (IActor target in record.Targets.Values) { - foreach (SkillEffectMetadata effect in record.Attack.Skills.Where(e => e.Splash != null)) { + if (target.ObjectId == record.Caster.ObjectId) { + if (splashEffects.Length > 0 && record.Attack.CubeMagicPathId == 0) { + Logger.Warning("[TargetAttack] SkillId={SkillId} AttackPoint={AttackPoint} IncludeCaster={IncludeCaster} — caster skipped in splash loop but CubeMagicPathId=0. Splash may be lost.", + record.SkillId, record.AttackPoint, record.Attack.Range.IncludeCaster); + } + continue; + } + + foreach (SkillEffectMetadata effect in splashEffects) { Field.AddSkill(record.Caster, effect, [target.Position], record.Caster.Rotation); } } - } public virtual void SkillAttackPoint(SkillRecord record, byte attackPoint) { diff --git a/Maple2.Server.Game/Model/Skill/SkillRecord.cs b/Maple2.Server.Game/Model/Skill/SkillRecord.cs index c975f7e0..34d1ac38 100644 --- a/Maple2.Server.Game/Model/Skill/SkillRecord.cs +++ b/Maple2.Server.Game/Model/Skill/SkillRecord.cs @@ -58,6 +58,10 @@ public bool TrySetAttackPoint(byte attackPoint) { } AttackPoint = attackPoint; + // Each attack point must start with a clean target set. + // Without this, targets from a prior attack point (e.g. a Friendly AP that includes the caster) + // bleed into subsequent attack points, causing incorrect damage and splash placement. + Targets.Clear(); return true; } diff --git a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs index 2fe01c48..d89c9f05 100644 --- a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs @@ -209,8 +209,10 @@ private void HandleTarget(GameSession session, IByteReader packet) { byte count = packet.ReadByte(); if (count > record.Attack.TargetCount) { - Logger.Error("Attack too many targets {Count} for {Record}", count, record); - // Adjust count + // Skills with BounceCount send all bounce targets in one packet but TargetCount is per-bounce. + // This may indicate an unimplemented bounce mechanic rather than a true exploit. + Logger.Warning("SkillId={SkillId} AttackPoint={AttackPoint} sent {Count} targets but TargetCount={TargetCount} — clamping. BounceCount={BounceCount}", + record.SkillId, attackPoint, count, record.Attack.TargetCount, record.Attack.Arrow.BounceCount); count = (byte) record.Attack.TargetCount; } @@ -232,10 +234,6 @@ private void HandleTarget(GameSession session, IByteReader packet) { switch (record.Attack.Range.ApplyTarget) { case ApplyTargetType.Hostile: - // Skip adding caster to hostile targets (caster may have been sent by client from a prior Friendly attack point) - if (targetId == session.Player.ObjectId) { - continue; - } if (session.Field.Mobs.TryGetValue(targetId, out FieldNpc? npc)) { record.Targets.TryAdd(npc.ObjectId, npc); } From a02fde94042872bd8d943fcaf335e7a33cf7d1cd Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:03:06 -0800 Subject: [PATCH 5/7] skill targeting fix --- Maple2.File.Ingest/Mapper/SkillMapper.cs | 2 +- Maple2.Model/Enum/Skill.cs | 6 +++++ Maple2.Model/Metadata/SkillMetadata.cs | 2 +- .../Field/FieldManager/FieldManager.State.cs | 27 ++++++++++++++++++- .../Manager/Field/FieldManager/IField.cs | 3 ++- .../Actor/ActorStateComponent/SkillState.cs | 2 +- .../Model/Field/Entity/FieldSkill.cs | 8 +++--- .../PacketHandlers/SkillHandler.cs | 10 ++++++- 8 files changed, 50 insertions(+), 10 deletions(-) diff --git a/Maple2.File.Ingest/Mapper/SkillMapper.cs b/Maple2.File.Ingest/Mapper/SkillMapper.cs index 9461f8be..9a7b3b62 100644 --- a/Maple2.File.Ingest/Mapper/SkillMapper.cs +++ b/Maple2.File.Ingest/Mapper/SkillMapper.cs @@ -163,7 +163,7 @@ private static SkillMetadataRange Convert(RegionSkill region) { RotateZDegree: region.rangeZRotateDegree, RangeAdd: region.rangeAdd, RangeOffset: region.rangeOffset, - IncludeCaster: (SkillTargetType) region.includeCaster, + IncludeCaster: (IncludeCasterType) region.includeCaster, ApplyTarget: (ApplyTargetType) region.applyTarget, CastTarget: (SkillTargetType) region.castTarget ); diff --git a/Maple2.Model/Enum/Skill.cs b/Maple2.Model/Enum/Skill.cs index 198e988e..f04a3782 100644 --- a/Maple2.Model/Enum/Skill.cs +++ b/Maple2.Model/Enum/Skill.cs @@ -89,6 +89,12 @@ public enum SkillTargetType { RegionPet = 8, } +public enum IncludeCasterType { + Exclude = 0, + Priority = 1, + Last = 2, +} + public enum DotTargetType { Caster = 0, Owner = 1, diff --git a/Maple2.Model/Metadata/SkillMetadata.cs b/Maple2.Model/Metadata/SkillMetadata.cs index bc2adc36..31635e3d 100644 --- a/Maple2.Model/Metadata/SkillMetadata.cs +++ b/Maple2.Model/Metadata/SkillMetadata.cs @@ -129,7 +129,7 @@ public record SkillMetadataRange( float RotateZDegree, Vector3 RangeAdd, Vector3 RangeOffset, - SkillTargetType IncludeCaster, // 0,1,2 + IncludeCasterType IncludeCaster, ApplyTargetType ApplyTarget, // 0,1,2,3,5,6,7,8 SkillTargetType CastTarget); // 0,1,2,3,4,5,7 diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index 3da48aa8..790481d6 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -562,7 +562,7 @@ public void AddSkill(SkillRecord record) { } } - public IEnumerable GetTargets(IActor caster, Prism[] prisms, ApplyTargetType targetType, int limit, ICollection? ignore = null) { + private IEnumerable GetTargetPool(IActor caster, Prism[] prisms, ApplyTargetType targetType, int limit, ICollection? ignore) { switch (targetType) { case ApplyTargetType.Friendly: if (caster is FieldNpc) { @@ -592,6 +592,31 @@ public IEnumerable GetTargets(IActor caster, Prism[] prisms, ApplyTarget } } + public IEnumerable GetTargets(IActor caster, Prism[] prisms, SkillMetadataRange range, int targetCount, ICollection? ignore = null) { + // Caster is always excluded from the pool; re-added explicitly per IncludeCaster semantics + ICollection poolIgnore = ignore != null ? [..ignore, caster] : [caster]; + + switch (range.IncludeCaster) { + case IncludeCasterType.Priority: { + // Caster guaranteed as first target; pool fills remaining slots + IActor[] pool = GetTargetPool(caster, prisms, range.ApplyTarget, targetCount - 1, poolIgnore).ToArray(); + return Enumerable.Repeat(caster, 1).Concat(pool); + } + case IncludeCasterType.Last: { + // Pool fills all slots; caster appended only if fewer than targetCount were found + IActor[] pool = GetTargetPool(caster, prisms, range.ApplyTarget, targetCount, poolIgnore).ToArray(); + return pool.Length < targetCount ? pool.Concat(Enumerable.Repeat(caster, 1)) : pool; + } + default: // Exclude + return GetTargetPool(caster, prisms, range.ApplyTarget, targetCount, poolIgnore); + } + } + + public IEnumerable GetTargets(SkillRecord record, ICollection? ignore = null) { + Prism[] prisms = [record.Attack.Range.GetPrism(record.ImpactPosition, record.Rotation.Z)]; + return GetTargets(record.Caster, prisms, record.Attack.Range, record.Attack.TargetCount, ignore); + } + public void RemoveSkill(int objectId) { if (fieldSkills.Remove(objectId, out _)) { Broadcast(RegionSkillPacket.Remove(objectId)); diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs index f5d6ec58..3b3c93eb 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs @@ -62,7 +62,8 @@ public virtual void Init() { } public void AddSkill(SkillMetadata metadata, int interval, in Vector3 position, in Vector3 rotation = default, int triggerId = 0); public void AddSkill(SkillRecord record); public void AddSkill(IActor caster, SkillEffectMetadata effect, Vector3[] points, in Vector3 rotation = default); - public IEnumerable GetTargets(IActor actor, Prism[] prisms, ApplyTargetType targetType, int limit, ICollection? ignore = null); + public IEnumerable GetTargets(IActor caster, Prism[] prisms, SkillMetadataRange range, int targetCount, ICollection? ignore = null); + public IEnumerable GetTargets(SkillRecord record, ICollection? ignore = null); public void RemoveSkill(int objectId); public void Broadcast(ByteWriter packet, GameSession? sender = null); public void BroadcastAiMessage(ByteWriter packet); diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs index 8eb4038a..6e53abf3 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs @@ -71,7 +71,7 @@ public void SkillCastAttack(SkillRecord cast, byte attackPoint, List att Tools.Collision.Prism attackPrism = attack.Range.GetPrism(actor.Position, actor.Rotation.Z); var resolvedTargets = new List(); int queryLimit = attack.TargetCount > 0 ? attack.TargetCount : 1; - foreach (IActor target in actor.Field.GetTargets(actor, [attackPrism], attack.Range.ApplyTarget, queryLimit)) { + foreach (IActor target in actor.Field.GetTargets(actor, [attackPrism], attack.Range, queryLimit)) { resolvedTargets.Add(target); } diff --git a/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs b/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs index c9d45a74..e270c2c7 100644 --- a/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs +++ b/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs @@ -81,7 +81,7 @@ public override void Update(long tickCount) { foreach (SkillMetadataMotion motion in Value.Data.Motions) { foreach (SkillMetadataAttack attack in motion.Attacks) { Prism[] prisms = Points.Select(point => attack.Range.GetPrism(point, UseDirection ? Rotation.Z : 0)).ToArray(); - if (Field.GetTargets(Caster, prisms, attack.Range.ApplyTarget, attack.TargetCount).Any()) { + if (Field.GetTargets(Caster, prisms, attack.Range, attack.TargetCount).Any()) { Active = true; goto activated; } @@ -134,8 +134,8 @@ public override void Update(long tickCount) { var prism = new Prism(circle, position.Z, box.Z); targets = attack.Arrow.BounceOverlap - ? Field.GetTargets(Caster, [prism], record.Attack.Range.ApplyTarget, 1, targets).ToArray() - : Field.GetTargets(Caster, [prism], record.Attack.Range.ApplyTarget, 1, bounceTargets).ToArray(); + ? Field.GetTargets(Caster, [prism], record.Attack.Range, 1, targets).ToArray() + : Field.GetTargets(Caster, [prism], record.Attack.Range, 1, bounceTargets).ToArray(); if (targets.Length <= 0) { break; } @@ -168,7 +168,7 @@ public override void Update(long tickCount) { } } else { Prism[] prisms = Points.Select(point => attack.Range.GetPrism(point, UseDirection ? Rotation.Z : 0)).ToArray(); - IActor[] targets = Field.GetTargets(Caster, prisms, attack.Range.ApplyTarget, attack.TargetCount).ToArray(); + IActor[] targets = Field.GetTargets(Caster, prisms, attack.Range, attack.TargetCount).ToArray(); // if (targets.Length > 0) { // logger.Debug("[{Tick}] {ObjectId}:{AttackPoint} Targeting: {Count}/{Limit} {Type}", // NextTick, ObjectId, attack.Point, targets.Length, attack.TargetCount, attack.Range.ApplyTarget); diff --git a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs index d89c9f05..156dfbb3 100644 --- a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs @@ -11,6 +11,7 @@ using Maple2.Server.Game.PacketHandlers.Field; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; +using Maple2.Server.Game.Util; namespace Maple2.Server.Game.PacketHandlers; @@ -225,7 +226,9 @@ private void HandleTarget(GameSession session, IByteReader packet) { session.Send(NoticePacket.Message($"Skill.Attack.Damage: {skillUid}; AttackPoint: {attackPoint}")); } - for (byte i = 0; i < count; i++) { + // Although the client feeds us this information and is right, we cannot rely on it and must validate it + // we should keep it just to ensure what the server gathers as proper targets is the same as the client + /*for (byte i = 0; i < count; i++) { int targetId = packet.ReadInt(); if (record.Targets.ContainsKey(targetId)) { continue; @@ -254,6 +257,11 @@ private void HandleTarget(GameSession session, IByteReader packet) { Logger.Debug("Unhandled Target-SkillEntity:{Entity}", record.Attack.Range.ApplyTarget); continue; } + }*/ + + IEnumerable targets = session.Field.GetTargets(record); + foreach (IActor target in targets) { + record.Targets.TryAdd(target.ObjectId, target); } session.Player.TargetAttack(record); } From 80c51d1ff440f64e3e94e8f0a8f59b737244642b Mon Sep 17 00:00:00 2001 From: Zin <62830952+Zintixx@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:14:15 -0800 Subject: [PATCH 6/7] early exit --- .../Manager/Field/FieldManager/FieldManager.State.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index 790481d6..90ee808d 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -593,6 +593,10 @@ private IEnumerable GetTargetPool(IActor caster, Prism[] prisms, ApplyTa } public IEnumerable GetTargets(IActor caster, Prism[] prisms, SkillMetadataRange range, int targetCount, ICollection? ignore = null) { + if (targetCount <= 0) { + return []; + } + // Caster is always excluded from the pool; re-added explicitly per IncludeCaster semantics ICollection poolIgnore = ignore != null ? [..ignore, caster] : [caster]; From 03bcde3101a7c3f40937e8e10e4de1723cab40cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 21 Feb 2026 12:52:45 -0300 Subject: [PATCH 7/7] format --- .../Field/FieldManager/FieldManager.State.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index f08a9d95..7517738c 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -678,19 +678,19 @@ public IEnumerable GetTargets(IActor caster, Prism[] prisms, SkillMetada } // Caster is always excluded from the pool; re-added explicitly per IncludeCaster semantics - ICollection poolIgnore = ignore != null ? [..ignore, caster] : [caster]; + ICollection poolIgnore = ignore != null ? [.. ignore, caster] : [caster]; switch (range.IncludeCaster) { case IncludeCasterType.Priority: { - // Caster guaranteed as first target; pool fills remaining slots - IActor[] pool = GetTargetPool(caster, prisms, range.ApplyTarget, targetCount - 1, poolIgnore).ToArray(); - return Enumerable.Repeat(caster, 1).Concat(pool); - } + // Caster guaranteed as first target; pool fills remaining slots + IActor[] pool = GetTargetPool(caster, prisms, range.ApplyTarget, targetCount - 1, poolIgnore).ToArray(); + return Enumerable.Repeat(caster, 1).Concat(pool); + } case IncludeCasterType.Last: { - // Pool fills all slots; caster appended only if fewer than targetCount were found - IActor[] pool = GetTargetPool(caster, prisms, range.ApplyTarget, targetCount, poolIgnore).ToArray(); - return pool.Length < targetCount ? pool.Concat(Enumerable.Repeat(caster, 1)) : pool; - } + // Pool fills all slots; caster appended only if fewer than targetCount were found + IActor[] pool = GetTargetPool(caster, prisms, range.ApplyTarget, targetCount, poolIgnore).ToArray(); + return pool.Length < targetCount ? pool.Concat(Enumerable.Repeat(caster, 1)) : pool; + } default: // Exclude return GetTargetPool(caster, prisms, range.ApplyTarget, targetCount, poolIgnore); }