Files
wwdpublic/Content.Client/Actions/ActionsSystem.cs
SimpleStation14 151b5e1007 Mirror: Fix SCRAM implant not working while cuffed. Incidentally fix freedom implant working while dead/crit (#266)
## Mirror of PR #25978: [Fix SCRAM implant not working while cuffed.
Incidentally fix freedom implant working while
dead/crit](https://github.com/space-wizards/space-station-14/pull/25978)
from <img src="https://avatars.githubusercontent.com/u/10567778?v=4"
alt="space-wizards" width="22"/>
[space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14)

###### `22e9d6562f21bdd4f0962d6e3b6fcdd81bb4c253`

PR opened by <img
src="https://avatars.githubusercontent.com/u/32041239?v=4"
width="16"/><a href="https://github.com/nikthechampiongr">
nikthechampiongr</a> at 2024-03-10 19:33:05 UTC

---

PR changed 13 files with 142 additions and 3 deletions.

The PR had the following labels:
- Status: Needs Review


---

<details open="true"><summary><h1>Original Body</h1></summary>

> fixes #25662 
> 
> <!-- Please read these guidelines before opening your PR:
https://docs.spacestation14.io/en/getting-started/pr-guideline -->
> <!-- The text between the arrows are comments - they will not be
visible on your PR. -->
> 
> ## About the PR
> <!-- What did you change in this PR? -->
> This pr makes it so you can use the SCRAM implant while cuffed. Also
fixes the freedom implant working while you are in crit, or even dead.
> 
> ## Why / Balance
> <!-- Why was it changed? Link any discussions or issues here. Please
discuss how this would affect game balance. -->
> The implant is made so you can escape horrible situations. It should
as such work while in crit.
> 
> ## Technical details
> <!-- If this is a code change, summarize at high level how your new
code works. This makes it easier to review. -->
> ActionBlockers now has a specific check to see if someone is
conscious. It is also used in the Interaction check now. Actions can now
use that check if the bool is set. The bool is now set for the freedom
and SCRAM! implant actions. Additionally the SCRAM! action now ignores
whether the user can interact in order to be able to use it while
cuffed.
> 
> ## Media
> <!-- 
> PRs which make ingame changes (adding clothing, items, new features,
etc) are required to have media attached that showcase the changes.
> Small fixes/refactors are exempt.
> Any media may be used in SS14 progress reports, with clear credit
given.
> 
> If you're unsure whether your PR will require media, ask a maintainer.
> 
> Check the box below to confirm that you have in fact seen this (put an
X in the brackets, like [X]):
> -->
> 
> - [x] I have added screenshots/videos to this PR showcasing its
changes ingame, **or** this PR does not require an ingame showcase
> 
> ## Breaking changes
> <!--
> List any breaking changes, including namespace, public
class/method/field changes, prototype renames; and provide instructions
for fixing them. This will be pasted in #codebase-changes.
> -->
> 
> **Changelog**
> <!--
> Make players aware of new features and changes that could affect how
they play the game by adding a Changelog entry. Please read the
Changelog guidelines located at:
https://docs.spacestation14.io/en/getting-started/pr-guideline#changelog
> -->
> 
> <!--
> Make sure to take this Changelog template out of the comment block in
order for it to show up.
> 🆑
> - add: Added fun!
> - remove: Removed fun!
> - tweak: Changed fun!
> - fix: Fixed fun!
> -->
> 🆑
> fix: You can now use the SCRAM! implant while cuffed.
> fix: The freedom implant can no longer be used while in crit, or dead.


</details>

Co-authored-by: SimpleStation14 <Unknown>
2024-05-12 00:40:56 -04:00

383 lines
14 KiB
C#

using System.IO;
using System.Linq;
using Content.Shared.Actions;
using Content.Shared.Mapping;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.ContentPack;
using Robust.Shared.GameStates;
using Robust.Shared.Input.Binding;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Client.Actions
{
[UsedImplicitly]
public sealed class ActionsSystem : SharedActionsSystem
{
public delegate void OnActionReplaced(EntityUid actionId);
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
public event Action<EntityUid>? OnActionAdded;
public event Action<EntityUid>? OnActionRemoved;
public event Action? ActionsUpdated;
public event Action<ActionsComponent>? LinkActions;
public event Action? UnlinkActions;
public event Action? ClearAssignments;
public event Action<List<SlotAssignment>>? AssignSlot;
private readonly List<EntityUid> _removed = new();
private readonly List<(EntityUid, BaseActionComponent?)> _added = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActionsComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<ActionsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleComponentState);
SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState);
SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState);
SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState);
}
private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
{
if (args.Current is not InstantActionComponentState state)
return;
BaseHandleState<InstantActionComponent>(uid, component, state);
}
private void OnEntityTargetHandleState(EntityUid uid, EntityTargetActionComponent component, ref ComponentHandleState args)
{
if (args.Current is not EntityTargetActionComponentState state)
return;
component.Whitelist = state.Whitelist;
component.CanTargetSelf = state.CanTargetSelf;
BaseHandleState<EntityTargetActionComponent>(uid, component, state);
}
private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent component, ref ComponentHandleState args)
{
if (args.Current is not WorldTargetActionComponentState state)
return;
BaseHandleState<WorldTargetActionComponent>(uid, component, state);
}
private void BaseHandleState<T>(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent
{
// TODO ACTIONS use auto comp states
component.Icon = state.Icon;
component.IconOn = state.IconOn;
component.IconColor = state.IconColor;
component.Keywords.Clear();
component.Keywords.UnionWith(state.Keywords);
component.Enabled = state.Enabled;
component.Toggled = state.Toggled;
component.Cooldown = state.Cooldown;
component.UseDelay = state.UseDelay;
component.Charges = state.Charges;
component.MaxCharges = state.MaxCharges;
component.RenewCharges = state.RenewCharges;
component.Container = EnsureEntity<T>(state.Container, uid);
component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid);
component.CheckCanInteract = state.CheckCanInteract;
component.CheckConsciousness = state.CheckConsciousness;
component.ClientExclusive = state.ClientExclusive;
component.Priority = state.Priority;
component.AttachedEntity = EnsureEntity<T>(state.AttachedEntity, uid);
component.RaiseOnUser = state.RaiseOnUser;
component.AutoPopulate = state.AutoPopulate;
component.Temporary = state.Temporary;
component.ItemIconStyle = state.ItemIconStyle;
component.Sound = state.Sound;
UpdateAction(uid, component);
}
protected override void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null)
{
if (!ResolveActionData(actionId, ref action))
return;
base.UpdateAction(actionId, action);
if (_playerManager.LocalEntity != action.AttachedEntity)
return;
ActionsUpdated?.Invoke();
}
private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
{
if (args.Current is not ActionsComponentState state)
return;
_added.Clear();
_removed.Clear();
var stateEnts = EnsureEntitySet<ActionsComponent>(state.Actions, uid);
foreach (var act in component.Actions)
{
if (!stateEnts.Contains(act) && !IsClientSide(act))
_removed.Add(act);
}
component.Actions.ExceptWith(_removed);
foreach (var actionId in stateEnts)
{
if (!actionId.IsValid())
continue;
if (!component.Actions.Add(actionId))
continue;
TryGetActionData(actionId, out var action);
_added.Add((actionId, action));
}
if (_playerManager.LocalEntity != uid)
return;
foreach (var action in _removed)
{
OnActionRemoved?.Invoke(action);
}
_added.Sort(ActionComparer);
foreach (var action in _added)
{
OnActionAdded?.Invoke(action.Item1);
}
ActionsUpdated?.Invoke();
}
public static int ActionComparer((EntityUid, BaseActionComponent?) a, (EntityUid, BaseActionComponent?) b)
{
var priorityA = a.Item2?.Priority ?? 0;
var priorityB = b.Item2?.Priority ?? 0;
if (priorityA != priorityB)
return priorityA - priorityB;
priorityA = a.Item2?.Container?.Id ?? 0;
priorityB = b.Item2?.Container?.Id ?? 0;
return priorityA - priorityB;
}
protected override void ActionAdded(EntityUid performer, EntityUid actionId, ActionsComponent comp,
BaseActionComponent action)
{
if (_playerManager.LocalEntity != performer)
return;
OnActionAdded?.Invoke(actionId);
}
protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
{
if (_playerManager.LocalEntity != performer)
return;
OnActionRemoved?.Invoke(actionId);
}
public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()
{
if (_playerManager.LocalEntity is not { } user)
return Enumerable.Empty<(EntityUid, BaseActionComponent)>();
return GetActions(user);
}
private void OnPlayerAttached(EntityUid uid, ActionsComponent component, LocalPlayerAttachedEvent args)
{
LinkAllActions(component);
}
private void OnPlayerDetached(EntityUid uid, ActionsComponent component, LocalPlayerDetachedEvent? args = null)
{
UnlinkAllActions();
}
public void UnlinkAllActions()
{
UnlinkActions?.Invoke();
}
public void LinkAllActions(ActionsComponent? actions = null)
{
if (_playerManager.LocalEntity is not { } user ||
!Resolve(user, ref actions, false))
{
return;
}
LinkActions?.Invoke(actions);
}
public override void Shutdown()
{
base.Shutdown();
CommandBinds.Unregister<ActionsSystem>();
}
public void TriggerAction(EntityUid actionId, BaseActionComponent action)
{
if (_playerManager.LocalEntity is not { } user ||
!TryComp(user, out ActionsComponent? actions))
{
return;
}
if (action is not InstantActionComponent instantAction)
return;
if (action.ClientExclusive)
{
if (instantAction.Event != null)
instantAction.Event.Performer = user;
PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime);
}
else
{
var request = new RequestPerformActionEvent(GetNetEntity(actionId));
EntityManager.RaisePredictiveEvent(request);
}
}
/// <summary>
/// Load actions and their toolbar assignments from a file.
/// </summary>
public void LoadActionAssignments(string path, bool userData)
{
if (_playerManager.LocalEntity is not { } user)
return;
var file = new ResPath(path).ToRootedPath();
TextReader reader = userData
? _resources.UserData.OpenText(file)
: _resources.ContentFileReadText(file);
var yamlStream = new YamlStream();
yamlStream.Load(reader);
if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
return;
ClearAssignments?.Invoke();
var assignments = new List<SlotAssignment>();
foreach (var entry in sequence.Sequence)
{
if (entry is not MappingDataNode map)
continue;
if (!map.TryGet("action", out var actionNode))
continue;
var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
var actionId = Spawn(null);
AddComp(actionId, action);
AddActionDirect(user, actionId);
if (map.TryGet<ValueDataNode>("name", out var nameNode))
_metaData.SetEntityName(actionId, nameNode.Value);
if (!map.TryGet("assignments", out var assignmentNode))
continue;
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode, notNullableOverride: true);
foreach (var index in nodeAssignments)
{
var assignment = new SlotAssignment(index.Hotbar, index.Slot, actionId);
assignments.Add(assignment);
}
}
AssignSlot?.Invoke(assignments);
}
/// <summary>
/// Load actions and their toolbar assignments from a file.
/// DeltaV - Load from an existing yaml stream instead
/// </summary>
public void LoadActionAssignments(YamlStream stream)
{
if (_playerManager.LocalEntity is not { } user)
return;
if (stream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
return;
ClearAssignments?.Invoke();
var assignments = new List<SlotAssignment>();
var existingActions = GetClientActions();
var existingActionsList = existingActions.ToList();
foreach (var entry in sequence.Sequence)
{
if (entry is not MappingDataNode map)
continue;
if (!map.TryGet("action", out var actionNode))
continue;
if (!map.TryGet<ValueDataNode>("name", out var nameNode))
continue;
var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
// Prevent spawning actions multiple times
var existing = existingActionsList.FirstOrNull(a =>
Name(a.Id) == nameNode.Value);
EntityUid actionId;
if (existing == null)
{
actionId = Spawn(null);
AddComp(actionId, action);
_metaData.SetEntityName(actionId, nameNode.Value);
DirtyEntity(actionId);
AddActionDirect(user, actionId);
}
else
{
actionId = existing.Value.Id;
}
if (!map.TryGet("assignments", out var assignmentNode))
continue;
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode, notNullableOverride: true);
foreach (var index in nodeAssignments)
{
var assignment = new SlotAssignment(index.Hotbar, index.Slot, actionId);
assignments.Add(assignment);
}
}
AssignSlot?.Invoke(assignments);
}
public record struct SlotAssignment(byte Hotbar, byte Slot, EntityUid ActionId);
}
}