Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Maple2.File.Ingest/Mapper/SkillMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down
6 changes: 6 additions & 0 deletions Maple2.Model/Enum/Skill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion Maple2.Model/Metadata/SkillMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ public void AddSkill(SkillRecord record) {
}
}

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

public IEnumerable<IActor> GetTargets(IActor caster, Prism[] prisms, SkillMetadataRange range, int targetCount, ICollection<IActor>? ignore = null) {
if (targetCount <= 0) {
return [];
}

// Caster is always excluded from the pool; re-added explicitly per IncludeCaster semantics
ICollection<IActor> 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<IActor>(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<IActor>(caster, 1)) : pool;
}
default: // Exclude
return GetTargetPool(caster, prisms, range.ApplyTarget, targetCount, poolIgnore);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public IEnumerable<IActor> GetTargets(SkillRecord record, ICollection<IActor>? 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));
Expand Down
3 changes: 2 additions & 1 deletion Maple2.Server.Game/Manager/Field/FieldManager/IField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IActor> GetTargets(IActor actor, Prism[] prisms, ApplyTargetType targetType, int limit, ICollection<IActor>? ignore = null);
public IEnumerable<IActor> GetTargets(IActor caster, Prism[] prisms, SkillMetadataRange range, int targetCount, ICollection<IActor>? ignore = null);
public IEnumerable<IActor> GetTargets(SkillRecord record, ICollection<IActor>? ignore = null);
public void RemoveSkill(int objectId);
public void Broadcast(ByteWriter packet, GameSession? sender = null);
public void BroadcastAiMessage(ByteWriter packet);
Expand Down
18 changes: 15 additions & 3 deletions Maple2.Server.Game/Model/Field/Actor/Actor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,21 +215,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public void SkillCastAttack(SkillRecord cast, byte attackPoint, List<IActor> att
Tools.Collision.Prism attackPrism = attack.Range.GetPrism(actor.Position, actor.Rotation.Z);
var resolvedTargets = new List<IActor>();
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);
}

Expand Down
8 changes: 4 additions & 4 deletions Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public override void Update(long tickCount) {
Prism[] prisms = Points
.Select(point => attack.Range.GetPrism(point, skillAngle, attack.Range.ApplyTarget))
.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;
}
Expand Down Expand Up @@ -148,8 +148,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;
}
Expand Down Expand Up @@ -185,7 +185,7 @@ public override void Update(long tickCount) {
Prism[] prisms = Points
.Select(point => attack.Range.GetPrism(point, skillAngle, attack.Range.ApplyTarget))
.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);
Expand Down
4 changes: 4 additions & 0 deletions Maple2.Server.Game/Model/Skill/SkillRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
16 changes: 13 additions & 3 deletions Maple2.Server.Game/PacketHandlers/SkillHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -209,8 +210,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;
}

Expand All @@ -223,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;
Expand Down Expand Up @@ -252,6 +257,11 @@ private void HandleTarget(GameSession session, IByteReader packet) {
Logger.Debug("Unhandled Target-SkillEntity:{Entity}", record.Attack.Range.ApplyTarget);
continue;
}
}*/

IEnumerable<IActor> targets = session.Field.GetTargets(record);
foreach (IActor target in targets) {
record.Targets.TryAdd(target.ObjectId, target);
}
session.Player.TargetAttack(record);
}
Expand Down
Loading