Revert "Revert "Cleanup ExecutionSystem (#24382)" (#25555)"

This reverts commit bb0776c496.

# Conflicts:
#	Content.Server/Projectiles/ProjectileSystem.cs
#	Content.Shared/Projectiles/SharedProjectileSystem.cs
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/flare_gun.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml
This commit is contained in:
Remuchi
2024-03-27 19:42:57 +07:00
parent c04b962141
commit 3c9c149b81
28 changed files with 678 additions and 114 deletions

View File

@@ -0,0 +1,9 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Execution;
[Serializable, NetSerializable]
public sealed partial class ExecutionDoAfterEvent : SimpleDoAfterEvent
{
}

View File

@@ -0,0 +1,26 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Execution;
/// <summary>
/// Added to entities that can be used to execute another target.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ExecutionComponent : Component
{
/// <summary>
/// How long the execution duration lasts.
/// </summary>
[DataField, AutoNetworkedField]
public float DoAfterDuration = 5f;
[DataField, AutoNetworkedField]
public float DamageModifier = 9f;
// Not networked because this is transient inside of a tick.
/// <summary>
/// True if it is currently executing for handlers.
/// </summary>
[DataField]
public bool Executing = true;
}

View File

@@ -0,0 +1,244 @@
using Content.Shared.Weapons.Ranged.Systems;
using Content.Shared.ActionBlocker;
using Content.Shared.CombatMode;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Player;
namespace Content.Shared.Execution;
/// <summary>
/// Verb for violently murdering cuffed creatures.
/// </summary>
public sealed class ExecutionSystem : EntitySystem
{
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly SharedGunSystem _gunSystem = default!;
[Dependency] private readonly SharedCombatModeSystem _combatSystem = default!;
[Dependency] private readonly SharedMeleeWeaponSystem _meleeSystem = default!;
// TODO: Still needs more cleaning up.
private const string DefaultInternalMeleeExecutionMessage = "execution-popup-melee-initial-internal";
private const string DefaultExternalMeleeExecutionMessage = "execution-popup-melee-initial-external";
private const string DefaultCompleteInternalMeleeExecutionMessage = "execution-popup-melee-complete-internal";
private const string DefaultCompleteExternalMeleeExecutionMessage = "execution-popup-melee-complete-external";
private const string DefaultInternalGunExecutionMessage = "execution-popup-gun-initial-internal";
private const string DefaultExternalGunExecutionMessage = "execution-popup-gun-initial-external";
private const string DefaultCompleteInternalGunExecutionMessage = "execution-popup-gun-complete-internal";
private const string DefaultCompleteExternalGunExecutionMessage = "execution-popup-gun-complete-external";
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExecutionComponent, GetVerbsEvent<UtilityVerb>>(OnGetInteractionsVerbs);
SubscribeLocalEvent<ExecutionComponent, ExecutionDoAfterEvent>(OnExecutionDoAfter);
SubscribeLocalEvent<ExecutionComponent, GetMeleeDamageEvent>(OnGetMeleeDamage);
}
private void OnGetInteractionsVerbs(EntityUid uid, ExecutionComponent comp, GetVerbsEvent<UtilityVerb> args)
{
if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract)
return;
var attacker = args.User;
var weapon = args.Using.Value;
var victim = args.Target;
if (!CanExecuteWithAny(victim, attacker))
return;
UtilityVerb verb = new()
{
Act = () => TryStartExecutionDoAfter(weapon, victim, attacker, comp),
Impact = LogImpact.High,
Text = Loc.GetString("execution-verb-name"),
Message = Loc.GetString("execution-verb-message"),
};
args.Verbs.Add(verb);
}
private void TryStartExecutionDoAfter(
EntityUid weapon,
EntityUid victim,
EntityUid attacker,
ExecutionComponent comp)
{
if (!CanExecuteWithAny(victim, attacker))
return;
// TODO: This should just be on the weapons as a single execution message.
var defaultExecutionInternal = DefaultInternalMeleeExecutionMessage;
var defaultExecutionExternal = DefaultExternalMeleeExecutionMessage;
if (HasComp<GunComponent>(weapon))
{
defaultExecutionExternal = DefaultInternalGunExecutionMessage;
defaultExecutionInternal = DefaultExternalGunExecutionMessage;
}
var internalMsg = defaultExecutionInternal;
var externalMsg = defaultExecutionExternal;
ShowExecutionInternalPopup(internalMsg, attacker, victim, weapon);
ShowExecutionExternalPopup(externalMsg, attacker, victim, weapon);
var doAfter = new DoAfterArgs(EntityManager, attacker, comp.DoAfterDuration, new ExecutionDoAfterEvent(),
weapon, target: victim, used: weapon)
{
BreakOnMove = true,
BreakOnDamage = true,
NeedHand = true
};
_doAfterSystem.TryStartDoAfter(doAfter);
}
private bool CanExecuteWithAny(EntityUid victim, EntityUid attacker)
{
// Use suicide.
if (victim == attacker)
return false;
// No point executing someone if they can't take damage
if (!TryComp<DamageableComponent>(victim, out _))
return false;
// You can't execute something that cannot die
if (!TryComp<MobStateComponent>(victim, out var mobState))
return false;
// You're not allowed to execute dead people (no fun allowed)
if (_mobStateSystem.IsDead(victim, mobState))
return false;
// You must be able to attack people to execute
if (!_actionBlockerSystem.CanAttack(attacker, victim))
return false;
// The victim must be incapacitated to be executed
if (victim != attacker && _actionBlockerSystem.CanInteract(victim, null))
return false;
// All checks passed
return true;
}
private void OnExecutionDoAfter(EntityUid uid, ExecutionComponent component, ExecutionDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Used == null || args.Target == null)
return;
var attacker = args.User;
var victim = args.Target.Value;
var weapon = args.Used.Value;
if (!CanExecuteWithAny(victim, attacker))
return;
// This is needed so the melee system does not stop it.
var prev = _combatSystem.IsInCombatMode(attacker);
_combatSystem.SetInCombatMode(attacker, true);
component.Executing = true;
string? internalMsg = null;
string? externalMsg = null;
if (TryComp(uid, out MeleeWeaponComponent? melee))
{
_meleeSystem.AttemptLightAttack(attacker, weapon, melee, victim);
internalMsg = DefaultCompleteInternalMeleeExecutionMessage;
externalMsg = DefaultCompleteExternalMeleeExecutionMessage;
}
else if (TryComp(uid, out GunComponent? gun))
{
// TODO: This should just be an event or something instead to get this.
// TODO: Handle clumsy.
if (!_gunSystem.AttemptDirectShoot(args.User, uid, args.Target.Value, gun))
{
internalMsg = null;
externalMsg = null;
}
else
{
internalMsg = DefaultCompleteInternalGunExecutionMessage;
externalMsg = DefaultCompleteExternalGunExecutionMessage;
}
args.Handled = true;
}
_combatSystem.SetInCombatMode(attacker, prev);
component.Executing = false;
args.Handled = true;
if (internalMsg != null && externalMsg != null)
{
ShowExecutionInternalPopup(internalMsg, attacker, victim, uid);
ShowExecutionExternalPopup(externalMsg, attacker, victim, uid);
}
}
private void OnGetMeleeDamage(EntityUid uid, ExecutionComponent comp, ref GetMeleeDamageEvent args)
{
if (!TryComp<MeleeWeaponComponent>(uid, out var melee) ||
!TryComp<ExecutionComponent>(uid, out var execComp) ||
!execComp.Executing)
{
return;
}
var bonus = melee.Damage * execComp.DamageModifier - melee.Damage;
args.Damage += bonus;
}
private void ShowExecutionInternalPopup(
string locString,
EntityUid attacker,
EntityUid victim,
EntityUid weapon,
bool predict = true)
{
if (predict)
{
_popupSystem.PopupClient(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
attacker,
attacker,
PopupType.Medium
);
}
else
{
_popupSystem.PopupEntity(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
attacker,
Filter.Entities(attacker),
true,
PopupType.Medium
);
}
}
private void ShowExecutionExternalPopup(string locString, EntityUid attacker, EntityUid victim, EntityUid weapon)
{
_popupSystem.PopupEntity(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
attacker,
Filter.PvsExcept(attacker),
true,
PopupType.MediumCaution
);
}
}

View File

@@ -303,3 +303,9 @@ public record struct ProjectileReflectAttemptEvent(EntityUid ProjUid, Projectile
/// </summary>
[ByRefEvent]
public record struct ProjectileHitEvent(DamageSpecifier Damage, EntityUid Target, EntityUid? Shooter = null);
/// <summary>
/// Raised after a projectile has dealt it's damage.
/// </summary>
[ByRefEvent]
public record struct AfterProjectileHitEvent(DamageSpecifier Damage, EntityUid Target);

View File

@@ -19,7 +19,7 @@ public record struct ShotAttemptedEvent
public bool Cancelled { get; private set; }
/// </summary>
/// <summary>
/// Prevent the gun from shooting
/// </summary>
public void Cancel()
@@ -27,7 +27,7 @@ public record struct ShotAttemptedEvent
Cancelled = true;
}
/// </summary>
/// <summary>
/// Allow the gun to shoot again, only use if you know what you are doing
/// </summary>
public void Uncancel()

View File

@@ -21,6 +21,7 @@ using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
@@ -140,7 +141,7 @@ public abstract partial class SharedGunSystem : EntitySystem
gun.ShootCoordinates = GetCoordinates(msg.Coordinates);
gun.Target = GetEntity(msg.Target);
Log.Debug($"Set shoot coordinates to {gun.ShootCoordinates}");
AttemptShoot(user.Value, ent, gun);
AttemptShootInternal(user.Value, ent, gun);
}
private void OnStopShootRequest(RequestStopShootEvent ev, EntitySessionEventArgs args)
@@ -204,13 +205,38 @@ public abstract partial class SharedGunSystem : EntitySystem
Dirty(uid, gun);
}
/// <summary>
/// Attempts to shoot the specified target directly.
/// This may bypass projectiles firing etc.
/// </summary>
public bool AttemptDirectShoot(EntityUid user, EntityUid gunUid, EntityUid target, GunComponent gun)
{
// Unique name so people don't think it's "shoot towards" and not "I will teleport a bullet into them".
gun.ShootCoordinates = Transform(target).Coordinates;
if (!TryTakeAmmo(user, gunUid, gun, out _, out _, out var args))
{
gun.ShootCoordinates = null;
return false;
}
var result = ShootDirect(gunUid, gun, target, args.Ammo, user: user);
gun.ShootCoordinates = null;
return result;
}
protected virtual bool ShootDirect(EntityUid gunUid, GunComponent gun, EntityUid target, List<(EntityUid? Entity, IShootable Shootable)> ammo, EntityUid user)
{
return false;
}
/// <summary>
/// Attempts to shoot at the target coordinates. Resets the shot counter after every shot.
/// </summary>
public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates)
{
gun.ShootCoordinates = toCoordinates;
AttemptShoot(user, gunUid, gun);
AttemptShootInternal(user, gunUid, gun);
gun.ShotCounter = 0;
}
@@ -221,20 +247,35 @@ public abstract partial class SharedGunSystem : EntitySystem
{
var coordinates = new EntityCoordinates(gunUid, new Vector2(0, -1));
gun.ShootCoordinates = coordinates;
AttemptShoot(gunUid, gunUid, gun);
AttemptShootInternal(gunUid, gunUid, gun);
gun.ShotCounter = 0;
}
private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
private void AttemptShootInternal(EntityUid user, EntityUid gunUid, GunComponent gun)
{
if (!TryTakeAmmo(user, gunUid, gun, out var fromCoordinates, out var toCoordinates, out var args))
return;
Shoot(gunUid, gun, args.Ammo, fromCoordinates, toCoordinates, out var userImpulse, user: user);
if (userImpulse && TryComp<PhysicsComponent>(user, out var userPhysics))
{
if (_gravity.IsWeightless(user, userPhysics))
CauseImpulse(fromCoordinates, toCoordinates, user, userPhysics);
}
}
/// <summary>
/// Validates if a gun can currently shoot.
/// </summary>
[Pure]
private bool CanShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
{
if (gun.FireRateModified <= 0f ||
!_actionBlockerSystem.CanAttack(user))
return;
var toCoordinates = gun.ShootCoordinates;
if (toCoordinates == null)
return;
{
return false;
}
var curTime = Timing.CurTime;
@@ -246,17 +287,42 @@ public abstract partial class SharedGunSystem : EntitySystem
};
RaiseLocalEvent(gunUid, ref prevention);
if (prevention.Cancelled)
return;
return false;
RaiseLocalEvent(user, ref prevention);
if (prevention.Cancelled)
return;
return false;
// Need to do this to play the clicking sound for empty automatic weapons
// but not play anything for burst fire.
if (gun.NextFire > curTime)
return;
return false;
return true;
}
/// <summary>
/// Tries to return ammo prepped for shooting if a gun is available to shoot.
/// </summary>
private bool TryTakeAmmo(
EntityUid user,
EntityUid gunUid, GunComponent gun,
out EntityCoordinates fromCoordinates,
out EntityCoordinates toCoordinates,
[NotNullWhen(true)] out TakeAmmoEvent? args)
{
toCoordinates = EntityCoordinates.Invalid;
fromCoordinates = EntityCoordinates.Invalid;
args = null;
if (!CanShoot(user, gunUid, gun))
return false;
if (gun.ShootCoordinates == null)
return false;
toCoordinates = gun.ShootCoordinates.Value;
var curTime = Timing.CurTime;
var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified);
// First shot
@@ -308,10 +374,11 @@ public abstract partial class SharedGunSystem : EntitySystem
}
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
return;
return false;
}
var fromCoordinates = Transform(user).Coordinates;
fromCoordinates = Transform(user).Coordinates;
// Remove ammo
var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user);
@@ -346,24 +413,18 @@ public abstract partial class SharedGunSystem : EntitySystem
// May cause prediction issues? Needs more tweaking
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
return;
return false;
}
return;
return false;
}
// Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent).
Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems);
var shotEv = new GunShotEvent(user, ev.Ammo);
RaiseLocalEvent(gunUid, ref shotEv);
if (userImpulse && TryComp<PhysicsComponent>(user, out var userPhysics))
{
if (_gravity.IsWeightless(user, userPhysics))
CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
}
Dirty(gunUid, gun);
args = ev;
return true;
}
public void Shoot(