[Stable -> Master] Hotfix: Fix melee wideswing damage exploit (#46299) (#46312)

Hotfix: Fix melee wideswing damage exploit (#46299)

* Initial commit

* TrimExcess to Take
This commit is contained in:
SlamBamActionman 2026-10-04 12:11:29 +02:00 • committed by GitHub
commit 64381ef101
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 46 additions and 56 deletions

View file

@ -159,10 +159,10 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range, overlapCheck: false);
}
protected override void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform)
protected override void DoDamageEffect(HashSet<EntityUid> targets, EntityUid? user, TransformComponent targetXform)
{
// Server never sends the event to us for predictiveeevent.
_color.RaiseEffect(Color.Red, targets, Filter.Local());
_color.RaiseEffect(Color.Red, targets.ToList(), Filter.Local());
}
/// <summary>
@ -189,8 +189,9 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
// This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes.
// Server will validate it with InRangeUnobstructed.
var entities = GetNetEntityList(ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user).ToList());
RaisePredictiveEvent(new HeavyAttackEvent(GetNetEntity(meleeUid), entities.GetRange(0, Math.Min(MaxTargets, entities.Count)), GetNetCoordinates(coordinates)));
var entities = GetNetEntitySet(ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user));
entities = entities.Take(Math.Min(MaxTargets, entities.Count)).ToHashSet();
RaisePredictiveEvent(new HeavyAttackEvent(GetNetEntity(meleeUid), entities, GetNetCoordinates(coordinates)));
}
private void ClientDisarm(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates)

View file

@ -63,7 +63,7 @@ public sealed partial class SolutionInjectOnCollideSystem : EntitySystem
private void DoInjection(Entity<BaseSolutionInjectOnEventComponent> injectorEntity, EntityUid target, EntityUid? source = null)
{
TryInjectTargets(injectorEntity, [target], source);
TryInjectTargets(injectorEntity, new HashSet<EntityUid>() { target }, source);
}
/// <summary>
@ -79,7 +79,7 @@ public sealed partial class SolutionInjectOnCollideSystem : EntitySystem
/// </list>
/// </remarks>
/// <returns>true if at least one target was successfully injected, otherwise false</returns>
private bool TryInjectTargets(Entity<BaseSolutionInjectOnEventComponent> injector, IReadOnlyList<EntityUid> targets, EntityUid? user = null)
private bool TryInjectTargets(Entity<BaseSolutionInjectOnEventComponent> injector, IReadOnlySet<EntityUid> targets, EntityUid? user = null)
{
// Make sure we have at least one target
if (targets.Count == 0)

View file

@ -68,10 +68,10 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
return Interaction.InRangeUnobstructed(user, target, range);
}
protected override void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform)
protected override void DoDamageEffect(HashSet<EntityUid> targets, EntityUid? user, TransformComponent targetXform)
{
var filter = Filter.Pvs(targetXform.Coordinates, entityMan: EntityManager).RemoveWhereAttachedEntity(o => o == user);
_color.RaiseEffect(Color.Red, targets, filter);
_color.RaiseEffect(Color.Red, targets.ToList(), filter);
}
public override void DoLunge(EntityUid user, EntityUid weapon, Angle angle, Vector2 localPos, string? animation, bool predicted = true)

View file

@ -99,10 +99,10 @@ public sealed partial class InjectorSystem : EntitySystem
private void OnAttack(Entity<InjectorComponent> injector, ref MeleeHitEvent args)
{
if (args.HitEntities is [])
if (args.HitEntities.Count == 0)
return;
TryMobsDoAfter(injector, args.User, args.HitEntities[0]);
TryMobsDoAfter(injector, args.User, args.HitEntities.First());
}
/// <summary>

View file

@ -92,7 +92,7 @@ public abstract partial class SharedFlashSystem : EntitySystem
Flash(target, args.User, ent.Owner, ent.Comp.MeleeDuration, ent.Comp.SlowTo, melee: true, stunDuration: ent.Comp.MeleeStunDuration);
}
EntityUid? firstTarget = args.HitEntities.Count > 0 ? args.HitEntities[0] : null; // Just pick the first hit entity.
EntityUid? firstTarget = args.HitEntities.Count > 0 ? args.HitEntities.First() : null; // Just pick the first hit entity.
var ev = new AfterFlashActivatedEvent(firstTarget, args.User);
RaiseLocalEvent(ent, ref ev);
}

View file

@ -1,3 +1,4 @@
using System.Linq;
using Content.Shared.Trigger.Components.Triggers;
using Content.Shared.Weapons.Melee.Events;
@ -30,7 +31,7 @@ public sealed partial class TriggerOnMeleeTriggerSystem : TriggerOnXSystem
if (args.HitEntities.Count == 0)
target = ent.Comp.TargetIsUser ? null : args.User;
else
target = ent.Comp.TargetIsUser ? args.HitEntities[0] : args.User;
target = ent.Comp.TargetIsUser ? args.HitEntities.First() : args.User;
Trigger.Trigger(ent.Owner, target, ent.Comp.KeyOut);
}
@ -42,7 +43,7 @@ public sealed partial class TriggerOnMeleeTriggerSystem : TriggerOnXSystem
if (!ent.Comp.TriggerEveryHit)
{
var target = ent.Comp.TargetIsUser ? args.HitEntities[0] : args.User;
var target = ent.Comp.TargetIsUser ? args.HitEntities.First() : args.User;
Trigger.Trigger(ent.Owner, target, ent.Comp.KeyOut);
return;
}

View file

@ -14,9 +14,9 @@ public sealed class HeavyAttackEvent : AttackEvent
/// <summary>
/// As what the client swung at will not match server we'll have them tell us what they hit so we can verify.
/// </summary>
public List<NetEntity> Entities;
public HashSet<NetEntity> Entities;
public HeavyAttackEvent(NetEntity weapon, List<NetEntity> entities, NetCoordinates coordinates) : base(coordinates)
public HeavyAttackEvent(NetEntity weapon, HashSet<NetEntity> entities, NetCoordinates coordinates) : base(coordinates)
{
Weapon = weapon;
Entities = entities;

View file

@ -31,9 +31,9 @@ public sealed class MeleeHitEvent : HandledEntityEventArgs
public DamageSpecifier BonusDamage = new();
/// <summary>
/// A list containing every hit entity. Can be zero.
/// A set containing every hit entity. Can be zero.
/// </summary>
public IReadOnlyList<EntityUid> HitEntities;
public IReadOnlySet<EntityUid> HitEntities;
/// <summary>
/// Used to define a new hit sound in case you want to override the default GenericHit.
@ -66,7 +66,7 @@ public sealed class MeleeHitEvent : HandledEntityEventArgs
/// </remarks>
public bool IsHit = true;
public MeleeHitEvent(List<EntityUid> hitEntities, EntityUid user, EntityUid weapon, DamageSpecifier baseDamage, Vector2? direction)
public MeleeHitEvent(HashSet<EntityUid> hitEntities, EntityUid user, EntityUid weapon, DamageSpecifier baseDamage, Vector2? direction)
{
HitEntities = hitEntities;
User = user;

View file

@ -527,7 +527,7 @@ public abstract partial class SharedMeleeWeaponSystem : EntitySystem
LogImpact.Low,
$"{ToPrettyString(user):actor} melee attacked (light) using {ToPrettyString(meleeUid):tool} and missed");
}
var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, null);
var missEvent = new MeleeHitEvent(new HashSet<EntityUid>(), user, meleeUid, damage, null);
RaiseLocalEvent(meleeUid, missEvent);
_meleeSound.PlaySwingSound(user, meleeUid, component);
return;
@ -536,15 +536,15 @@ public abstract partial class SharedMeleeWeaponSystem : EntitySystem
// Sawmill.Debug($"Melee damage is {damage.Total} out of {component.Damage.Total}");
// Raise event before doing damage so we can cancel damage if the event is handled
var hitEvent = new MeleeHitEvent(new List<EntityUid> { target.Value }, user, meleeUid, damage, null);
var hitEvent = new MeleeHitEvent(new HashSet<EntityUid> { target.Value }, user, meleeUid, damage, null);
RaiseLocalEvent(meleeUid, hitEvent);
if (hitEvent.Handled)
return;
var targets = new List<EntityUid>(1)
var targets = new HashSet<EntityUid>(1)
{
target.Value
target.Value,
};
var weapon = GetEntity(ev.Weapon);
@ -601,7 +601,7 @@ public abstract partial class SharedMeleeWeaponSystem : EntitySystem
}
}
protected abstract void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform);
protected abstract void DoDamageEffect(HashSet<EntityUid> targets, EntityUid? user, TransformComponent targetXform);
private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
{
@ -620,7 +620,7 @@ public abstract partial class SharedMeleeWeaponSystem : EntitySystem
var damage = GetDamage(meleeUid, user, component);
var resistanceBypass = GetResistanceBypass(meleeUid, user, component);
var entities = GetEntityList(ev.Entities);
var entities = GetEntitySet(ev.Entities);
if (entities.Count == 0)
{
@ -636,7 +636,7 @@ public abstract partial class SharedMeleeWeaponSystem : EntitySystem
LogImpact.Low,
$"{ToPrettyString(user):actor} melee attacked (heavy) using {ToPrettyString(meleeUid):tool} and missed");
}
var missEvent = new MeleeHitEvent(new List<EntityUid>(), user, meleeUid, damage, direction);
var missEvent = new MeleeHitEvent(new HashSet<EntityUid>(), user, meleeUid, damage, direction);
RaiseLocalEvent(meleeUid, missEvent);
// immediate audio feedback
@ -648,35 +648,21 @@ public abstract partial class SharedMeleeWeaponSystem : EntitySystem
// Naughty input
if (entities.Count > MaxTargets)
{
entities.RemoveRange(MaxTargets, entities.Count - MaxTargets);
entities = entities.Take(MaxTargets).ToHashSet();
}
// Validate client
for (var i = entities.Count - 1; i >= 0; i--)
{
var entity = entities[i];
entities.RemoveWhere(entity => TerminatingOrDeleted(entity) ||
!ArcRaySuccessful(entity,
userPos,
direction.ToWorldAngle(),
component.Angle,
distance,
userXform.MapID,
user,
session));
if (TerminatingOrDeleted(entity))
{
entities.RemoveAt(i);
continue;
}
if (!ArcRaySuccessful(entity,
userPos,
direction.ToWorldAngle(),
component.Angle,
distance,
userXform.MapID,
user,
session))
{
// Bad input
entities.RemoveAt(i);
}
}
var targets = new List<EntityUid>();
var targets = new HashSet<EntityUid>();
foreach (var entity in entities)
{
if (entity == user ||
@ -710,17 +696,17 @@ public abstract partial class SharedMeleeWeaponSystem : EntitySystem
}
var appliedDamage = new DamageSpecifier();
var filteredTargets = new HashSet<EntityUid>();
for (var i = targets.Count - 1; i >= 0; i--)
foreach (var entity in targets)
{
var entity = targets[i];
// We raise an attack attempt here as well,
// primarily because this was an untargeted wideswing: if a subscriber to that event cared about
// the potential target (such as for pacifism), they need to be made aware of the target here.
// In that case, just continue.
if (!Blocker.CanAttack(user, entity, (weapon, component)))
{
targets.RemoveAt(i);
filteredTargets.Add(entity);
continue;
}
@ -755,9 +741,11 @@ public abstract partial class SharedMeleeWeaponSystem : EntitySystem
}
if (TerminatingOrDeleted(entity))
targets.RemoveAt(i);
filteredTargets.Add(entity);
}
targets.ExceptWith(filteredTargets);
if (entities.Count != 0)
{
var target = entities.First();
@ -768,12 +756,12 @@ public abstract partial class SharedMeleeWeaponSystem : EntitySystem
{
if (appliedDamage.GetTotal() > FixedPoint2.Zero)
{
DoDamageEffect(targets, user, Transform(targets[0]));
DoDamageEffect(targets, user, Transform(targets.First()));
ResetUndamagedSwingsCount((meleeUid, component));
}
else
{
UndamagedAttack((meleeUid, component), targets[0], user);
UndamagedAttack((meleeUid, component), targets.First(), user);
}
}