Refactor disarms (#36546)

* Refactor disarms

- Move client stuff to shared
- Cleanup a bunch of stuff
- Ref events
- Fix the swing sound mispredict (I noticed it on target dummies).

* Revert this change

* minor review

* Rebiew

---------

Co-authored-by: Milon <milonpl.git@proton.me>
This commit is contained in:
metalgearsloth
2025-04-19 11:38:22 +10:00
committed by GitHub
parent 1dbc40f051
commit 8ab0e59db6
10 changed files with 214 additions and 234 deletions

View File

@@ -107,28 +107,28 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
{
coordinates = TransformSystem.ToCoordinates(_map.GetMap(mousePos.MapId), mousePos);
}
// If the gun has AltFireComponent, it can be used to attack.
if (TryComp<GunComponent>(weaponUid, out var gun) && gun.UseKey)
{
if (!TryComp<AltFireMeleeComponent>(weaponUid, out var altFireComponent) || altDown != BoundKeyState.Down)
return;
switch(altFireComponent.AttackType)
{
case AltFireAttackType.Light:
ClientLightAttack(entity, mousePos, coordinates, weaponUid, weapon);
break;
case AltFireAttackType.Heavy:
ClientHeavyAttack(entity, coordinates, weaponUid, weapon);
break;
case AltFireAttackType.Disarm:
ClientDisarm(entity, mousePos, coordinates);
break;
}
return;
}
@@ -166,35 +166,6 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
_color.RaiseEffect(Color.Red, targets, Filter.Local());
}
protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
{
if (!base.DoDisarm(user, ev, meleeUid, component, session))
return false;
if (!TryComp<CombatModeComponent>(user, out var combatMode) ||
combatMode.CanDisarm != true)
{
return false;
}
var target = GetEntity(ev.Target);
// They need to either have hands...
if (!HasComp<HandsComponent>(target!.Value))
{
// or just be able to be shoved over.
if (TryComp<StatusEffectsComponent>(target, out var status) && status.AllowedEffects.Contains("KnockedDown"))
return true;
if (Timing.IsFirstTimePredicted && HasComp<MobStateComponent>(target.Value))
PopupSystem.PopupEntity(Loc.GetString("disarm-action-disarmable", ("targetName", target.Value)), target.Value);
return false;
}
return true;
}
/// <summary>
/// Raises a heavy attack event with the relevant attacked entities.
/// This is to avoid lag effecting the client's perspective too much.
@@ -222,7 +193,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
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)));
}
private void ClientDisarm(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates)
{
EntityUid? target = null;
@@ -232,7 +203,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
RaisePredictiveEvent(new DisarmAttackEvent(GetNetEntity(target), GetNetCoordinates(coordinates)));
}
private void ClientLightAttack(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates, EntityUid weaponUid, MeleeWeaponComponent meleeComponent)
{
var attackerPos = TransformSystem.GetMapCoordinates(attacker);

View File

@@ -1,16 +0,0 @@
namespace Content.Server.CombatMode.Disarm
{
/// <summary>
/// Applies a malus to disarm attempts against this item.
/// </summary>
[RegisterComponent]
public sealed partial class DisarmMalusComponent : Component
{
/// <summary>
/// So, disarm chances are a % chance represented as a value between 0 and 1.
/// This default would be a 30% penalty to that.
/// </summary>
[DataField("malus")]
public float Malus = 0.3f;
}
}

View File

@@ -98,7 +98,7 @@ namespace Content.Server.Hands.Systems
}
}
private void OnDisarmed(EntityUid uid, HandsComponent component, DisarmedEvent args)
private void OnDisarmed(EntityUid uid, HandsComponent component, ref DisarmedEvent args)
{
if (args.Handled)
return;

View File

@@ -1,26 +1,13 @@
using Content.Server.Chat.Systems;
using Content.Server.CombatMode.Disarm;
using Content.Server.Movement.Systems;
using Content.Shared.Actions.Events;
using Content.Shared.Administration.Components;
using Content.Shared.CombatMode;
using Content.Shared.Damage.Events;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Effects;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Speech.Components;
using Content.Shared.StatusEffect;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
using System.Linq;
using System.Numerics;
@@ -28,12 +15,9 @@ namespace Content.Server.Weapons.Melee;
public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly DamageExamineSystem _damageExamine = default!;
[Dependency] private readonly LagCompensationSystem _lag = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
public override void Initialize()
@@ -85,106 +69,6 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
return true;
}
protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
{
if (!base.DoDisarm(user, ev, meleeUid, component, session))
return false;
if (!TryComp<CombatModeComponent>(user, out var combatMode) ||
combatMode.CanDisarm != true)
{
return false;
}
var target = GetEntity(ev.Target!.Value);
if (_mobState.IsIncapacitated(target))
{
return false;
}
if (!TryComp<HandsComponent>(target, out var targetHandsComponent))
{
if (!TryComp<StatusEffectsComponent>(target, out var status) || !status.AllowedEffects.Contains("KnockedDown"))
return false;
}
if (!InRange(user, target, component.Range, session))
{
return false;
}
EntityUid? inTargetHand = null;
if (targetHandsComponent?.ActiveHand is { IsEmpty: false })
{
inTargetHand = targetHandsComponent.ActiveHand.HeldEntity!.Value;
}
Interaction.DoContactInteraction(user, target);
var attemptEvent = new DisarmAttemptEvent(target, user, inTargetHand);
if (inTargetHand != null)
{
RaiseLocalEvent(inTargetHand.Value, attemptEvent);
}
RaiseLocalEvent(target, attemptEvent);
if (attemptEvent.Cancelled)
return false;
var chance = CalculateDisarmChance(user, target, inTargetHand, combatMode);
if (_random.Prob(chance))
{
// Yknow something tells me this comment is hilariously out of date...
// Don't play a sound as the swing is already predicted.
// Also don't play popups because most disarms will miss.
return false;
}
AdminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(user):user} used disarm on {ToPrettyString(target):target}");
var eventArgs = new DisarmedEvent { Target = target, Source = user, PushProbability = 1 - chance };
RaiseLocalEvent(target, eventArgs);
if (!eventArgs.Handled)
{
return false;
}
_audio.PlayPvs(combatMode.DisarmSuccessSound, user, AudioParams.Default.WithVariation(0.025f).WithVolume(5f));
AdminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(user):user} used disarm on {ToPrettyString(target):target}");
var targetEnt = Identity.Entity(target, EntityManager);
var userEnt = Identity.Entity(user, EntityManager);
var msgOther = Loc.GetString(
eventArgs.PopupPrefix + "popup-message-other-clients",
("performerName", userEnt),
("targetName", targetEnt));
var msgUser = Loc.GetString(eventArgs.PopupPrefix + "popup-message-cursor", ("targetName", targetEnt));
var filterOther = Filter.PvsExcept(user, entityManager: EntityManager);
PopupSystem.PopupEntity(msgOther, user, filterOther, true);
PopupSystem.PopupEntity(msgUser, target, user);
if (eventArgs.IsStunned)
{
PopupSystem.PopupEntity(Loc.GetString("stunned-component-disarm-success-others", ("source", userEnt), ("target", targetEnt)), targetEnt, Filter.PvsExcept(user), true, PopupType.LargeCaution);
PopupSystem.PopupCursor(Loc.GetString("stunned-component-disarm-success", ("target", targetEnt)), user, PopupType.Large);
AdminLogger.Add(LogType.DisarmedKnockdown, LogImpact.Medium, $"{ToPrettyString(user):user} knocked down {ToPrettyString(target):target}");
}
return true;
}
protected override bool InRange(EntityUid user, EntityUid target, float range, ICommonSession? session)
{
EntityCoordinates targetCoordinates;
@@ -205,24 +89,6 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
_color.RaiseEffect(Color.Red, targets, filter);
}
private float CalculateDisarmChance(EntityUid disarmer, EntityUid disarmed, EntityUid? inTargetHand, CombatModeComponent disarmerComp)
{
if (HasComp<DisarmProneComponent>(disarmer))
return 1.0f;
if (HasComp<DisarmProneComponent>(disarmed))
return 0.0f;
var chance = disarmerComp.BaseDisarmFailChance;
if (inTargetHand != null && TryComp<DisarmMalusComponent>(inTargetHand, out var malus))
{
chance += malus.Malus;
}
return Math.Clamp(chance, 0f, 1f);
}
public override void DoLunge(EntityUid user, EntityUid weapon, Angle angle, Vector2 localPos, string? animation, bool predicted = true)
{
Filter filter;

View File

@@ -1,15 +1,21 @@
namespace Content.Shared.Actions.Events;
public sealed class DisarmAttemptEvent : CancellableEntityEventArgs
/// <summary>
/// Raised directed on the target OR their actively held entity.
/// </summary>
[ByRefEvent]
public record struct DisarmAttemptEvent
{
public readonly EntityUid TargetUid;
public readonly EntityUid DisarmerUid;
public readonly EntityUid? TargetItemInHandUid;
public bool Cancelled;
public DisarmAttemptEvent(EntityUid targetUid, EntityUid disarmerUid, EntityUid? targetItemInHandUid = null)
{
TargetUid = targetUid;
DisarmerUid = disarmerUid;
TargetItemInHandUid = targetItemInHandUid;
}
}
}

View File

@@ -0,0 +1,17 @@
using Robust.Shared.GameStates;
namespace Content.Shared.CombatMode;
/// <summary>
/// Applies a malus to disarm attempts against this item.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class DisarmMalusComponent : Component
{
/// <summary>
/// So, disarm chances are a % chance represented as a value between 0 and 1.
/// This default would be a 30% penalty to that.
/// </summary>
[DataField]
public float Malus = 0.3f;
}

View File

@@ -1,31 +1,33 @@
namespace Content.Shared.CombatMode
namespace Content.Shared.CombatMode;
[ByRefEvent]
public record struct DisarmedEvent(EntityUid Target, EntityUid Source, float PushProb)
{
public sealed class DisarmedEvent : HandledEntityEventArgs
{
/// <summary>
/// The entity being disarmed.
/// </summary>
public EntityUid Target { get; init; }
/// <summary>
/// The entity being disarmed.
/// </summary>
public readonly EntityUid Target = Target;
/// <summary>
/// The entity performing the disarm.
/// </summary>
public EntityUid Source { get; init; }
/// <summary>
/// The entity performing the disarm.
/// </summary>
public readonly EntityUid Source = Source;
/// <summary>
/// Probability for push/knockdown.
/// </summary>
public float PushProbability { get; init; }
/// <summary>
/// Probability for push/knockdown.
/// </summary>
public readonly float PushProbability = PushProb;
/// <summary>
/// Prefix for the popup message that will be displayed on a successful push.
/// Should be set before returning.
/// </summary>
public string PopupPrefix { get; set; } = "";
/// <summary>
/// Prefix for the popup message that will be displayed on a successful push.
/// Should be set before returning.
/// </summary>
public string PopupPrefix = "";
/// <summary>
/// Whether the entity was successfully stunned from a shove.
/// </summary>
public bool IsStunned { get; set; }
}
/// <summary>
/// Whether the entity was successfully stunned from a shove.
/// </summary>
public bool IsStunned;
public bool Handled;
}

View File

@@ -723,8 +723,8 @@ namespace Content.Shared.Cuffs
// if combat mode is on, shove the person.
if (_combatMode.IsInCombatMode(user) && target != user && user != null)
{
var eventArgs = new DisarmedEvent { Target = target, Source = user.Value, PushProbability = 1};
RaiseLocalEvent(target, eventArgs);
var eventArgs = new DisarmedEvent(target, user.Value, 1f);
RaiseLocalEvent(target, ref eventArgs);
shoved = true;
}

View File

@@ -119,7 +119,7 @@ public sealed partial class StaminaSystem : EntitySystem
Dirty(uid, component);
}
private void OnDisarmed(EntityUid uid, StaminaComponent component, DisarmedEvent args)
private void OnDisarmed(EntityUid uid, StaminaComponent component, ref DisarmedEvent args)
{
if (args.Handled)
return;

View File

@@ -2,6 +2,8 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions.Events;
using Content.Shared.Administration.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.CombatMode;
using Content.Shared.Damage;
@@ -10,22 +12,30 @@ using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.StatusEffect;
using Content.Shared.Weapons.Melee.Components;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using ItemToggleMeleeWeaponComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleMeleeWeaponComponent;
@@ -33,20 +43,24 @@ namespace Content.Shared.Weapons.Melee;
public abstract class SharedMeleeWeaponSystem : EntitySystem
{
[Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!;
[Dependency] protected readonly ActionBlockerSystem Blocker = default!;
[Dependency] protected readonly SharedCombatModeSystem CombatMode = default!;
[Dependency] protected readonly DamageableSystem Damageable = default!;
[Dependency] protected readonly SharedInteractionSystem Interaction = default!;
[Dependency] protected readonly IMapManager MapManager = default!;
[Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly MeleeSoundSystem _meleeSound = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly StaminaSystem _stamina = default!;
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] protected readonly IMapManager MapManager = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!;
[Dependency] protected readonly ActionBlockerSystem Blocker = default!;
[Dependency] protected readonly DamageableSystem Damageable = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly MeleeSoundSystem _meleeSound = default!;
[Dependency] protected readonly MobStateSystem MobState = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] protected readonly SharedCombatModeSystem CombatMode = default!;
[Dependency] protected readonly SharedInteractionSystem Interaction = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
[Dependency] private readonly StaminaSystem _stamina = default!;
private const int AttackMask = (int) (CollisionGroup.MobMask | CollisionGroup.Opaque);
@@ -783,7 +797,25 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
return highestDamageType;
}
protected virtual bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
private float CalculateDisarmChance(EntityUid disarmer, EntityUid disarmed, EntityUid? inTargetHand, CombatModeComponent disarmerComp)
{
if (HasComp<DisarmProneComponent>(disarmer))
return 1.0f;
if (HasComp<DisarmProneComponent>(disarmed))
return 0.0f;
var chance = disarmerComp.BaseDisarmFailChance;
if (inTargetHand != null && TryComp<DisarmMalusComponent>(inTargetHand, out var malus))
{
chance += malus.Malus;
}
return Math.Clamp(chance, 0f, 1f);
}
private bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
{
var target = GetEntity(ev.Target);
@@ -793,8 +825,110 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
return false;
}
// Play a sound to give instant feedback; same with playing the animations
_meleeSound.PlaySwingSound(user, meleeUid, component);
if (MobState.IsIncapacitated(target.Value))
{
return false;
}
if (!TryComp<CombatModeComponent>(user, out var combatMode) ||
combatMode.CanDisarm != true)
{
return false;
}
// Need hands or to be able to be shoved over.
if (!TryComp<HandsComponent>(target, out var targetHandsComponent))
{
if (!TryComp<StatusEffectsComponent>(target, out var status) ||
!status.AllowedEffects.Contains("KnockedDown"))
{
// Notify disarmable
if (HasComp<MobStateComponent>(target.Value))
PopupSystem.PopupClient(Loc.GetString("disarm-action-disarmable", ("targetName", target.Value)), target.Value);
return false;
}
}
if (!InRange(user, target.Value, component.Range, session))
{
return false;
}
EntityUid? inTargetHand = null;
if (targetHandsComponent?.ActiveHand is { IsEmpty: false })
{
inTargetHand = targetHandsComponent.ActiveHand.HeldEntity!.Value;
}
var attemptEvent = new DisarmAttemptEvent(target.Value, user, inTargetHand);
if (inTargetHand != null)
{
RaiseLocalEvent(inTargetHand.Value, ref attemptEvent);
}
RaiseLocalEvent(target.Value, ref attemptEvent);
if (attemptEvent.Cancelled)
return false;
var chance = CalculateDisarmChance(user, target.Value, inTargetHand, combatMode);
// At this point we diverge
if (_netMan.IsClient)
{
// Play a sound to give instant feedback; same with playing the animations
_meleeSound.PlaySwingSound(user, meleeUid, component);
return true;
}
if (_random.Prob(chance))
{
return false;
}
var eventArgs = new DisarmedEvent(target.Value, user, 1 - chance);
RaiseLocalEvent(target.Value, ref eventArgs);
// Nothing handled it so abort.
if (!eventArgs.Handled)
{
return false;
}
Interaction.DoContactInteraction(user, target);
AdminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(user):user} used disarm on {ToPrettyString(target):target}");
AdminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(user):user} used disarm on {ToPrettyString(target):target}");
_audio.PlayPvs(combatMode.DisarmSuccessSound, target.Value, AudioParams.Default.WithVariation(0.025f).WithVolume(5f));
var targetEnt = Identity.Entity(target.Value, EntityManager);
var userEnt = Identity.Entity(user, EntityManager);
var msgOther = Loc.GetString(
eventArgs.PopupPrefix + "popup-message-other-clients",
("performerName", userEnt),
("targetName", targetEnt));
var msgUser = Loc.GetString(eventArgs.PopupPrefix + "popup-message-cursor", ("targetName", targetEnt));
var filterOther = Filter.PvsExcept(user, entityManager: EntityManager);
PopupSystem.PopupEntity(msgOther, user, filterOther, true);
PopupSystem.PopupEntity(msgUser, target.Value, user);
if (eventArgs.IsStunned)
{
PopupSystem.PopupEntity(Loc.GetString("stunned-component-disarm-success-others", ("source", userEnt), ("target", targetEnt)), targetEnt, Filter.PvsExcept(user), true, PopupType.LargeCaution);
PopupSystem.PopupCursor(Loc.GetString("stunned-component-disarm-success", ("target", targetEnt)), user, PopupType.Large);
AdminLogger.Add(LogType.DisarmedKnockdown, LogImpact.Medium, $"{ToPrettyString(user):user} knocked down {ToPrettyString(target):target}");
}
return true;
}