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;