diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
index 91ac67beda..a74dc48e6f 100644
--- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
+++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using Content.Client.Gameplay;
using Content.Shared.CombatMode;
using Content.Shared.Hands.Components;
@@ -142,7 +143,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, _transform, EntityManager);
}
- EntityManager.RaisePredictiveEvent(new HeavyAttackEvent(weaponUid, coordinates));
+ ClientHeavyAttack(entity, coordinates, weaponUid, weapon);
}
return;
@@ -244,6 +245,31 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
return true;
}
+ ///
+ /// Raises a heavy attack event with the relevant attacked entities.
+ /// This is to avoid lag effecting the client's perspective too much.
+ ///
+ private void ClientHeavyAttack(EntityUid user, EntityCoordinates coordinates, EntityUid meleeUid, MeleeWeaponComponent component)
+ {
+ // Only run on first prediction to avoid the potential raycast entities changing.
+ if (!TryComp(user, out var userXform) || !Timing.IsFirstTimePredicted)
+ return;
+
+ var targetMap = coordinates.ToMap(EntityManager, _transform);
+
+ if (targetMap.MapId != userXform.MapID)
+ return;
+
+ var userPos = _transform.GetWorldPosition(userXform);
+ var direction = targetMap.Position - userPos;
+ var distance = Math.Min(component.Range, direction.Length);
+
+ // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes.
+ // Server will validate it with InRangeUnobstructed.
+ var entities = ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user);
+ RaisePredictiveEvent(new HeavyAttackEvent(meleeUid, entities.ToList(), coordinates));
+ }
+
protected override void Popup(string message, EntityUid? uid, EntityUid? user)
{
if (!Timing.IsFirstTimePredicted || uid == null)
diff --git a/Content.Server/Movement/Systems/LagCompensationSystem.cs b/Content.Server/Movement/Systems/LagCompensationSystem.cs
index 9b30372696..0fbec2d492 100644
--- a/Content.Server/Movement/Systems/LagCompensationSystem.cs
+++ b/Content.Server/Movement/Systems/LagCompensationSystem.cs
@@ -1,6 +1,7 @@
using Content.Server.Movement.Components;
using Robust.Server.Player;
using Robust.Shared.Map;
+using Robust.Shared.Players;
using Robust.Shared.Timing;
namespace Content.Server.Movement.Systems;
@@ -64,13 +65,13 @@ public sealed class LagCompensationSystem : EntitySystem
component.Positions.Enqueue((_timing.CurTime, args.NewPosition, args.NewRotation));
}
- public (EntityCoordinates Coordinates, Angle Angle) GetCoordinatesAngle(EntityUid uid, IPlayerSession pSession,
+ public (EntityCoordinates Coordinates, Angle Angle) GetCoordinatesAngle(EntityUid uid, ICommonSession? pSession,
TransformComponent? xform = null)
{
if (!Resolve(uid, ref xform))
return (EntityCoordinates.Invalid, Angle.Zero);
- if (!TryComp(uid, out var lag) || lag.Positions.Count == 0)
+ if (pSession == null || !TryComp(uid, out var lag) || lag.Positions.Count == 0)
return (xform.Coordinates, xform.LocalRotation);
var angle = Angle.Zero;
@@ -102,15 +103,15 @@ public sealed class LagCompensationSystem : EntitySystem
return (coordinates, angle);
}
- public Angle GetAngle(EntityUid uid, IPlayerSession pSession, TransformComponent? xform = null)
+ public Angle GetAngle(EntityUid uid, ICommonSession? session, TransformComponent? xform = null)
{
- var (_, angle) = GetCoordinatesAngle(uid, pSession, xform);
+ var (_, angle) = GetCoordinatesAngle(uid, session, xform);
return angle;
}
- public EntityCoordinates GetCoordinates(EntityUid uid, IPlayerSession pSession, TransformComponent? xform = null)
+ public EntityCoordinates GetCoordinates(EntityUid uid, ICommonSession? session, TransformComponent? xform = null)
{
- var (coordinates, _) = GetCoordinatesAngle(uid, pSession, xform);
+ var (coordinates, _) = GetCoordinatesAngle(uid, session, xform);
return coordinates;
}
}
diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
index 0f0a9ad5b0..0fcaaa6f4f 100644
--- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
+++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
@@ -25,6 +25,7 @@ using Content.Shared.Weapons.Melee.Events;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Map;
+using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Random;
@@ -85,6 +86,29 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
args.Verbs.Add(verb);
}
+ protected override bool ArcRaySuccessful(EntityUid targetUid, Vector2 position, Angle angle, Angle arcWidth, float range, MapId mapId,
+ EntityUid ignore, ICommonSession? session)
+ {
+ // Originally the client didn't predict damage effects so you'd intuit some level of how far
+ // in the future you'd need to predict, but then there was a lot of complaining like "why would you add artifical delay" as if ping is a choice.
+ // Now damage effects are predicted but for wide attacks it differs significantly from client and server so your game could be lying to you on hits.
+ // This isn't fair in the slightest because it makes ping a huge advantage and this would be a hidden system.
+ // Now the client tells us what they hit and we validate if it's plausible.
+
+ // Even if the client is sending entities they shouldn't be able to hit:
+ // A) Wide-damage is split anyway
+ // B) We run the same validation we do for click attacks.
+
+ // Could also check the arc though future effort + if they're aimbotting it's not really going to make a difference.
+
+ // (This runs lagcomp internally and is what clickattacks use)
+ if (!Interaction.InRangeUnobstructed(ignore, targetUid, range + 0.1f))
+ return false;
+
+ // TODO: Check arc though due to the aforementioned aimbot + damage split comments it's less important.
+ return true;
+ }
+
private DamageSpecifier? GetDamage(MeleeWeaponComponent component)
{
return component.Damage.Total > FixedPoint2.Zero ? component.Damage : null;
diff --git a/Content.Shared/Weapons/Melee/Events/HeavyAttackEvent.cs b/Content.Shared/Weapons/Melee/Events/HeavyAttackEvent.cs
index e5dadc7aa7..47d5d7f6c9 100644
--- a/Content.Shared/Weapons/Melee/Events/HeavyAttackEvent.cs
+++ b/Content.Shared/Weapons/Melee/Events/HeavyAttackEvent.cs
@@ -11,8 +11,14 @@ public sealed class HeavyAttackEvent : AttackEvent
{
public readonly EntityUid Weapon;
- public HeavyAttackEvent(EntityUid weapon, EntityCoordinates coordinates) : base(coordinates)
+ ///
+ /// As what the client swung at will not match server we'll have them tell us what they hit so we can verify.
+ ///
+ public List Entities;
+
+ public HeavyAttackEvent(EntityUid weapon, List entities, EntityCoordinates coordinates) : base(coordinates)
{
Weapon = weapon;
+ Entities = entities;
}
}
diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
index ef9be4d9cf..5bf5dc8f17 100644
--- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
+++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
@@ -41,7 +41,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
[Dependency] protected readonly SharedInteractionSystem Interaction = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
- [Dependency] protected readonly SharedTransformSystem _transform = default!;
+ [Dependency] protected readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StaminaSystem _stamina = default!;
protected ISawmill Sawmill = default!;
@@ -65,10 +65,10 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
SubscribeLocalEvent(OnMeleeDropped);
SubscribeLocalEvent(OnMeleeSelected);
+ SubscribeAllEvent(OnHeavyAttack);
SubscribeAllEvent(OnLightAttack);
SubscribeAllEvent(OnStartHeavyAttack);
SubscribeAllEvent(OnStopHeavyAttack);
- SubscribeAllEvent(OnHeavyAttack);
SubscribeAllEvent(OnDisarmAttack);
SubscribeAllEvent(OnStopAttack);
@@ -513,29 +513,23 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
protected abstract void DoDamageEffect(List targets, EntityUid? user, TransformComponent targetXform);
- protected virtual void DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
+ private void DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
{
// TODO: This is copy-paste as fuck with DoPreciseAttack
if (!TryComp(user, out var userXform))
- {
return;
- }
var targetMap = ev.Coordinates.ToMap(EntityManager, _transform);
if (targetMap.MapId != userXform.MapID)
- {
return;
- }
var userPos = _transform.GetWorldPosition(userXform);
var direction = targetMap.Position - userPos;
var distance = Math.Min(component.Range, direction.Length);
var damage = component.Damage * GetModifier(component, false);
-
- // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes.
- var entities = ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user);
+ var entities = ev.Entities;
if (entities.Count == 0)
{
@@ -546,6 +540,19 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
return;
}
+ // Validate client
+ for (var i = entities.Count - 1; i >= 0; i--)
+ {
+ if (ArcRaySuccessful(entities[i], userPos, direction.ToWorldAngle(), component.Angle, distance,
+ userXform.MapID, user, session))
+ {
+ continue;
+ }
+
+ // Bad input
+ entities.RemoveAt(i);
+ }
+
var targets = new List();
var damageQuery = GetEntityQuery();
@@ -633,7 +640,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
}
}
- private HashSet ArcRayCast(Vector2 position, Angle angle, Angle arcWidth, float range, MapId mapId, EntityUid ignore)
+ protected HashSet ArcRayCast(Vector2 position, Angle angle, Angle arcWidth, float range, MapId mapId, EntityUid ignore)
{
// TODO: This is pretty sucky.
var widthRad = arcWidth;
@@ -659,6 +666,13 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
return resSet;
}
+ protected virtual bool ArcRaySuccessful(EntityUid targetUid, Vector2 position, Angle angle, Angle arcWidth, float range,
+ MapId mapId, EntityUid ignore, ICommonSession? session)
+ {
+ // Only matters for server.
+ return true;
+ }
+
private void PlayHitSound(EntityUid target, EntityUid? user, string? type, SoundSpecifier? hitSoundOverride, SoundSpecifier? hitSound)
{
var playedSound = false;