From 49ae383f0645fb7bf754d6fcafa8860fdb9668f0 Mon Sep 17 00:00:00 2001
From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Date: Fri, 25 Feb 2022 18:55:18 +1300
Subject: [PATCH] Revert "Actions Rework" (#6888)
---
Content.Client/Actions/ActionEvents.cs | 11 -
Content.Client/Actions/ActionsSystem.cs | 594 +---------------
.../Actions/Assignments/ActionAssignment.cs | 90 +++
.../Actions/Assignments/ActionAssignments.cs | 233 ++++++-
.../Actions/ClientActionsComponent.cs | 252 +++++++
.../Actions/UI/ActionAlertTooltip.cs | 4 +-
Content.Client/Actions/UI/ActionMenu.cs | 201 ++++--
Content.Client/Actions/UI/ActionMenuItem.cs | 170 +----
Content.Client/Actions/UI/ActionSlot.cs | 429 ++++++++----
Content.Client/Actions/UI/ActionsUI.cs | 220 ++++--
Content.Client/Clothing/MagbootsSystem.cs | 3 +-
Content.Client/DragDrop/DragDropSystem.cs | 7 +-
Content.Client/Entry/EntryPoint.cs | 4 +-
Content.Client/Entry/IgnoredComponents.cs | 1 -
Content.Client/Hands/ShowHandItemOverlay.cs | 31 +-
Content.Client/IoC/ClientContentIoC.cs | 1 +
.../Outline/InteractionOutlineSystem.cs | 72 +-
Content.Client/Outline/TargetOutlineSystem.cs | 179 -----
.../Components/Mobs/ActionsComponentTests.cs | 389 +++++++++++
Content.Server/Act/IDisarmedAct.cs | 26 +-
Content.Server/Actions/Actions/CombatMode.cs | 29 +
.../Actions/Actions/DebugInstant.cs | 35 +
.../Actions/Actions/DebugTargetEntity.cs | 27 +
.../Actions/Actions/DebugTargetPoint.cs | 26 +
Content.Server/Actions/Actions/DebugToggle.cs | 48 ++
.../Actions/Actions/DisarmAction.cs | 126 ++++
Content.Server/Actions/Actions/GhostBoo.cs | 49 ++
.../Actions/Actions/GuardianToggleAction.cs | 38 ++
Content.Server/Actions/Actions/PAIMidi.cs | 36 +
.../Actions/Actions/ScreamAction.cs | 71 ++
Content.Server/Actions/ActionsSystem.cs | 40 --
.../Actions/Commands/CooldownAction.cs | 64 ++
.../Actions/Commands/GrantAction.cs | 53 ++
.../Actions/Commands/RevokeAction.cs | 54 ++
.../Actions/ServerActionsComponent.cs | 215 ++++++
.../Actions/Spells/GiveItemSpell.cs | 77 +++
.../Atmos/Components/GasTankComponent.cs | 37 +-
.../Atmos/EntitySystems/GasTankSystem.cs | 22 +-
.../Chemistry/ReagentEffects/DoAction.cs | 34 +
.../Chemistry/ReagentEffects/Scream.cs | 15 -
.../Clothing/Components/MagbootsComponent.cs | 25 +-
Content.Server/Clothing/MagbootsSystem.cs | 7 +-
Content.Server/CombatMode/CombatModeSystem.cs | 74 +-
Content.Server/Entry/EntryPoint.cs | 1 +
.../Ghost/Components/GhostComponent.cs | 23 +-
Content.Server/Ghost/GhostSystem.cs | 35 +-
.../Guardian/GuardianHostComponent.cs | 17 +-
Content.Server/Guardian/GuardianSystem.cs | 30 +-
.../Hands/Components/HandsComponent.cs | 37 +-
Content.Server/Hands/Systems/HandsSystem.cs | 26 -
Content.Server/IoC/ServerContentIoC.cs | 1 +
.../Components/HandheldLightComponent.cs | 14 +
.../UnpoweredFlashlightComponent.cs | 7 +-
.../EntitySystems/HandheldLightSystem.cs | 43 +-
.../UnpoweredFlashlightSystem.cs | 27 +-
Content.Server/PAI/PAISystem.cs | 17 -
.../Speech/Components/VocalComponent.cs | 42 --
Content.Server/Speech/VocalSystem.cs | 86 ---
Content.Server/Stunnable/StunSystem.cs | 4 +-
.../UserInterface/ActivatableUISystem.cs | 21 +-
.../UserInterface/OpenUiActionEvent.cs | 23 -
Content.Server/Wieldable/WieldableSystem.cs | 4 +-
Content.Shared.Database/LogType.cs | 1 -
Content.Shared/Actions/ActionEvents.cs | 61 --
Content.Shared/Actions/ActionManager.cs | 70 ++
Content.Shared/Actions/ActionType.cs | 41 ++
.../Actions/ActionTypes/ActionType.cs | 268 --------
.../Actions/ActionTypes/InstantAction.cs | 41 --
.../Actions/ActionTypes/TargetedAction.cs | 153 -----
Content.Shared/Actions/ActionsComponent.cs | 46 --
.../Actions/Behaviors/IActionBehavior.cs | 46 ++
.../Actions/Behaviors/IInstantAction.cs | 25 +
.../Actions/Behaviors/ITargetEntityAction.cs | 30 +
.../Behaviors/ITargetEntityItemAction.cs | 32 +
.../Actions/Behaviors/ITargetPointAction.cs | 32 +
.../Behaviors/ITargetPointItemAction.cs | 33 +
.../Actions/Behaviors/IToggleAction.cs | 41 ++
.../Behaviors/Item/IInstantItemAction.cs | 26 +
.../Behaviors/Item/IItemActionBehavior.cs | 53 ++
.../Behaviors/Item/IToggleItemAction.cs | 42 ++
.../Components/ItemActionsComponent.cs | 203 ++++++
.../Components/SharedActionsComponent.cs | 641 ++++++++++++++++++
Content.Shared/Actions/IActionAttempt.cs | 226 ++++++
.../Actions/Prototypes/ActionPrototype.cs | 102 +++
.../Actions/Prototypes/BaseActionPrototype.cs | 169 +++++
.../Actions/Prototypes/ItemActionPrototype.cs | 125 ++++
Content.Shared/Actions/SharedActionSystem.cs | 74 ++
Content.Shared/Actions/SharedActionsSystem.cs | 459 -------------
.../Clothing/SharedMagbootsComponent.cs | 12 +-
.../Clothing/SharedMagbootsSystem.cs | 30 -
.../CombatMode/SharedCombatModeComponent.cs | 52 +-
.../CombatMode/SharedCombatModeSystem.cs | 32 +-
Content.Shared/Hands/HandEvents.cs | 24 +-
Content.Shared/Hands/SharedHandsSystem.cs | 19 +-
.../Component/SharedHandheldLightComponent.cs | 6 +-
Content.Shared/PAI/PAIComponent.cs | 5 +-
.../Toggleable/ToggleActionEvent.cs | 8 -
Content.Shared/Whitelist/EntityWhitelist.cs | 2 -
.../en-US/actions/actions/combat-mode.ftl | 8 +-
.../en-US/actions/actions/disarm-action.ftl | 5 +-
.../Locale/en-US/actions/actions/ghost.ftl | 2 -
.../en-US/actions/actions/internals.ftl | 2 -
.../Locale/en-US/actions/actions/light.ftl | 2 -
.../Locale/en-US/actions/actions/mapping.ftl | 1 -
.../Locale/en-US/actions/actions/pai.ftl | 2 -
.../Locale/en-US/actions/actions/vocal.ftl | 1 -
.../Locale/en-US/actions/ui/actionslot.ftl | 2 -
Resources/Locale/en-US/guardian/guardian.ftl | 6 +-
Resources/Locale/en-US/magboot/actions.ftl | 2 -
Resources/Prototypes/Actions/actions.yml | 147 ++++
.../Prototypes/Actions/guardian_actions.yml | 12 +
Resources/Prototypes/Actions/item_actions.yml | 125 ++++
Resources/Prototypes/Actions/magboots.yml | 13 +
Resources/Prototypes/Actions/spells.yml | 14 +
.../Clothing/Head/base_clothinghead.yml | 11 +-
.../Entities/Clothing/Head/hardhats.yml | 9 +-
.../Entities/Clothing/Shoes/magboots.yml | 10 +-
.../Prototypes/Entities/Mobs/NPCs/animals.yml | 11 +-
.../Entities/Mobs/NPCs/simplemob.yml | 2 +
.../Entities/Mobs/Player/admin_ghost.yml | 3 +
.../Prototypes/Entities/Mobs/Player/dwarf.yml | 4 +
.../Entities/Mobs/Player/guardian.yml | 2 +
.../Prototypes/Entities/Mobs/Player/human.yml | 4 +
.../Entities/Mobs/Player/observer.yml | 3 +
.../Prototypes/Entities/Mobs/Player/slime.yml | 4 +
.../Prototypes/Entities/Mobs/Player/vox.yml | 6 +-
.../Entities/Mobs/Species/human.yml | 1 -
.../Entities/Objects/Devices/pda.yml | 6 -
.../Prototypes/Entities/Objects/Fun/pai.yml | 9 +-
.../Entities/Objects/Misc/fluff_lights.yml | 9 +-
.../Entities/Objects/Tools/flashlights.yml | 9 +-
.../Entities/Objects/Tools/gas_tanks.yml | 9 +-
.../Entities/Objects/Tools/lantern.yml | 9 +-
Resources/Prototypes/Reagents/pyrotechnic.yml | 3 +-
Resources/Prototypes/Reagents/toxins.yml | 9 +-
135 files changed, 5165 insertions(+), 3119 deletions(-)
delete mode 100644 Content.Client/Actions/ActionEvents.cs
create mode 100644 Content.Client/Actions/Assignments/ActionAssignment.cs
create mode 100644 Content.Client/Actions/ClientActionsComponent.cs
delete mode 100644 Content.Client/Outline/TargetOutlineSystem.cs
create mode 100644 Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs
create mode 100644 Content.Server/Actions/Actions/CombatMode.cs
create mode 100644 Content.Server/Actions/Actions/DebugInstant.cs
create mode 100644 Content.Server/Actions/Actions/DebugTargetEntity.cs
create mode 100644 Content.Server/Actions/Actions/DebugTargetPoint.cs
create mode 100644 Content.Server/Actions/Actions/DebugToggle.cs
create mode 100644 Content.Server/Actions/Actions/DisarmAction.cs
create mode 100644 Content.Server/Actions/Actions/GhostBoo.cs
create mode 100644 Content.Server/Actions/Actions/GuardianToggleAction.cs
create mode 100644 Content.Server/Actions/Actions/PAIMidi.cs
create mode 100644 Content.Server/Actions/Actions/ScreamAction.cs
delete mode 100644 Content.Server/Actions/ActionsSystem.cs
create mode 100644 Content.Server/Actions/Commands/CooldownAction.cs
create mode 100644 Content.Server/Actions/Commands/GrantAction.cs
create mode 100644 Content.Server/Actions/Commands/RevokeAction.cs
create mode 100644 Content.Server/Actions/ServerActionsComponent.cs
create mode 100644 Content.Server/Actions/Spells/GiveItemSpell.cs
create mode 100644 Content.Server/Chemistry/ReagentEffects/DoAction.cs
delete mode 100644 Content.Server/Chemistry/ReagentEffects/Scream.cs
delete mode 100644 Content.Server/Speech/Components/VocalComponent.cs
delete mode 100644 Content.Server/Speech/VocalSystem.cs
delete mode 100644 Content.Server/UserInterface/OpenUiActionEvent.cs
delete mode 100644 Content.Shared/Actions/ActionEvents.cs
create mode 100644 Content.Shared/Actions/ActionManager.cs
create mode 100644 Content.Shared/Actions/ActionType.cs
delete mode 100644 Content.Shared/Actions/ActionTypes/ActionType.cs
delete mode 100644 Content.Shared/Actions/ActionTypes/InstantAction.cs
delete mode 100644 Content.Shared/Actions/ActionTypes/TargetedAction.cs
delete mode 100644 Content.Shared/Actions/ActionsComponent.cs
create mode 100644 Content.Shared/Actions/Behaviors/IActionBehavior.cs
create mode 100644 Content.Shared/Actions/Behaviors/IInstantAction.cs
create mode 100644 Content.Shared/Actions/Behaviors/ITargetEntityAction.cs
create mode 100644 Content.Shared/Actions/Behaviors/ITargetEntityItemAction.cs
create mode 100644 Content.Shared/Actions/Behaviors/ITargetPointAction.cs
create mode 100644 Content.Shared/Actions/Behaviors/ITargetPointItemAction.cs
create mode 100644 Content.Shared/Actions/Behaviors/IToggleAction.cs
create mode 100644 Content.Shared/Actions/Behaviors/Item/IInstantItemAction.cs
create mode 100644 Content.Shared/Actions/Behaviors/Item/IItemActionBehavior.cs
create mode 100644 Content.Shared/Actions/Behaviors/Item/IToggleItemAction.cs
create mode 100644 Content.Shared/Actions/Components/ItemActionsComponent.cs
create mode 100644 Content.Shared/Actions/Components/SharedActionsComponent.cs
create mode 100644 Content.Shared/Actions/IActionAttempt.cs
create mode 100644 Content.Shared/Actions/Prototypes/ActionPrototype.cs
create mode 100644 Content.Shared/Actions/Prototypes/BaseActionPrototype.cs
create mode 100644 Content.Shared/Actions/Prototypes/ItemActionPrototype.cs
create mode 100644 Content.Shared/Actions/SharedActionSystem.cs
delete mode 100644 Content.Shared/Actions/SharedActionsSystem.cs
delete mode 100644 Content.Shared/Clothing/SharedMagbootsSystem.cs
delete mode 100644 Content.Shared/Toggleable/ToggleActionEvent.cs
delete mode 100644 Resources/Locale/en-US/actions/actions/ghost.ftl
delete mode 100644 Resources/Locale/en-US/actions/actions/internals.ftl
delete mode 100644 Resources/Locale/en-US/actions/actions/light.ftl
delete mode 100644 Resources/Locale/en-US/actions/actions/mapping.ftl
delete mode 100644 Resources/Locale/en-US/actions/actions/pai.ftl
delete mode 100644 Resources/Locale/en-US/actions/actions/vocal.ftl
delete mode 100644 Resources/Locale/en-US/actions/ui/actionslot.ftl
delete mode 100644 Resources/Locale/en-US/magboot/actions.ftl
create mode 100644 Resources/Prototypes/Actions/actions.yml
create mode 100644 Resources/Prototypes/Actions/guardian_actions.yml
create mode 100644 Resources/Prototypes/Actions/item_actions.yml
create mode 100644 Resources/Prototypes/Actions/magboots.yml
create mode 100644 Resources/Prototypes/Actions/spells.yml
diff --git a/Content.Client/Actions/ActionEvents.cs b/Content.Client/Actions/ActionEvents.cs
deleted file mode 100644
index 36739b0dbe..0000000000
--- a/Content.Client/Actions/ActionEvents.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Content.Shared.Actions.ActionTypes;
-
-namespace Content.Client.Actions;
-
-///
-/// This event is raised when a user clicks on an empty action slot. Enables other systems to fill this slow.
-///
-public sealed class FillActionSlotEvent : EntityEventArgs
-{
- public ActionType? Action;
-}
diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs
index 4b972cf9ca..b5201fcd62 100644
--- a/Content.Client/Actions/ActionsSystem.cs
+++ b/Content.Client/Actions/ActionsSystem.cs
@@ -1,64 +1,20 @@
-using Content.Client.Actions.Assignments;
-using Content.Client.Actions.UI;
using Content.Client.Construction;
using Content.Client.DragDrop;
-using Content.Client.Hands;
-using Content.Client.Items.Managers;
-using Content.Client.Outline;
-using Content.Client.Popups;
-using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
using Content.Shared.Input;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
using Robust.Client.Player;
-using Robust.Client.UserInterface;
-using Robust.Client.Utility;
-using Robust.Shared.Audio;
-using Robust.Shared.ContentPack;
-using Robust.Shared.GameStates;
+using Robust.Shared.GameObjects;
using Robust.Shared.Input;
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.Utility;
-using System.IO;
-using System.Linq;
-using YamlDotNet.RepresentationModel;
+using Robust.Shared.IoC;
namespace Content.Client.Actions
{
[UsedImplicitly]
- public sealed class ActionsSystem : SharedActionsSystem
+ public sealed class ActionsSystem : EntitySystem
{
-
[Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly IUserInterfaceManager _uiManager = default!;
- [Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
- [Dependency] private readonly ISerializationManager _serializationManager = default!;
- [Dependency] private readonly IResourceManager _resourceManager = default!;
- [Dependency] private readonly IOverlayManager _overlayMan = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
- [Dependency] private readonly InteractionOutlineSystem _interactionOutline = default!;
- [Dependency] private readonly TargetOutlineSystem _targetOutline = default!;
-
- // TODO Redo assignments, including allowing permanent user configurable slot assignments.
- ///
- /// Current assignments for all hotbars / slots for this entity.
- ///
- public ActionAssignments Assignments = new(Hotbars, Slots);
-
- public const byte Hotbars = 9;
- public const byte Slots = 10;
-
- public bool UIDirty;
-
- public ActionsUI? Ui;
- private EntityUid? _highlightedEntity;
public override void Initialize()
{
@@ -108,193 +64,12 @@ namespace Content.Client.Actions
HandleChangeHotbarKeybind(8))
// when selecting a target, we intercept clicks in the game world, treating them as our target selection. We want to
// take priority before any other systems handle the click.
- .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true),
+ .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse),
typeof(ConstructionSystem), typeof(DragDropSystem))
- .BindBefore(EngineKeyFunctions.UIRightClick, new PointerInputCmdHandler(TargetingCancel, outsidePrediction: true))
.Register();
- SubscribeLocalEvent(OnPlayerAttached);
- SubscribeLocalEvent(OnPlayerDetached);
- SubscribeLocalEvent(HandleState);
- }
-
- protected override void Dirty(ActionType action)
- {
- base.Dirty(action);
- if (action.AttachedEntity == _playerManager.LocalPlayer?.ControlledEntity)
- UIDirty = true;
- }
-
- private void HandleState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
- {
- if (args.Current is not ActionsComponentState state)
- return;
-
- var serverActions = new SortedSet(state.Actions);
-
- foreach (var act in component.Actions.ToList())
- {
- if (act.ClientExclusive)
- continue;
-
- if (!serverActions.TryGetValue(act, out var serverAct))
- {
- component.Actions.Remove(act);
- Assignments.Remove(act);
- continue;
- }
-
- act.CopyFrom(serverAct);
- serverActions.Remove(serverAct);
-
- if (act is EntityTargetAction entAct)
- {
- entAct.Whitelist?.UpdateRegistrations();
- }
- }
-
- // Anything that remains is a new action
- foreach (var newAct in serverActions)
- {
- if (newAct is EntityTargetAction entAct)
- entAct.Whitelist?.UpdateRegistrations();
-
- // We create a new action, not just sorting a reference to the state's action.
- component.Actions.Add((ActionType) newAct.Clone());
- }
-
- UIDirty = true;
- }
-
- ///
- /// Highlights the item slot (inventory or hand) that contains this item
- ///
- ///
- public void HighlightItemSlot(EntityUid item)
- {
- StopHighlightingItemSlot();
-
- _highlightedEntity = item;
- _itemSlotManager.HighlightEntity(item);
- }
-
- ///
- /// Stops highlighting any item slots we are currently highlighting.
- /// H
- public void StopHighlightingItemSlot()
- {
- if (_highlightedEntity == null)
- return;
-
- _itemSlotManager.UnHighlightEntity(_highlightedEntity.Value);
- _highlightedEntity = null;
- }
-
- protected override void AddActionInternal(ActionsComponent comp, ActionType action)
- {
- // Sometimes the client receives actions from the server, before predicting that newly added components will add
- // their own shared actions. Just in case those systems ever decided to directly access action properties (e.g.,
- // action.Toggled), we will remove duplicates:
- if (comp.Actions.TryGetValue(action, out var existing))
- {
- comp.Actions.Remove(existing);
- Assignments.Replace(existing, action);
- }
-
- comp.Actions.Add(action);
- }
-
- public override void AddActions(EntityUid uid, IEnumerable actions, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
- {
- if (!Resolve(uid, ref comp, false))
- return;
-
- base.AddActions(uid, actions, provider, comp, dirty);
- UIDirty = true;
- }
-
- public override void RemoveActions(EntityUid uid, IEnumerable actions, ActionsComponent? comp = null, bool dirty = true)
- {
- if (!Resolve(uid, ref comp, false))
- return;
-
- base.RemoveActions(uid, actions, comp, dirty);
-
- foreach (var act in actions)
- {
- Assignments.Remove(act);
- }
-
- UIDirty = true;
- }
-
- public override void FrameUpdate(float frameTime)
- {
- // avoid updating GUI when doing predictions & resetting state.
- if (UIDirty)
- {
- UIDirty = false;
- UpdateUI();
- }
- }
-
- ///
- /// Updates the displayed hotbar (and menu) based on current state of actions.
- ///
- public void UpdateUI()
- {
- if (Ui == null)
- return;
-
- foreach (var action in Ui.Component.Actions)
- {
- if (action.AutoPopulate && !Assignments.Assignments.ContainsKey(action))
- Assignments.AutoPopulate(action, Ui.SelectedHotbar, false);
- }
-
- // get rid of actions that are no longer available to the user
- foreach (var (action, index) in Assignments.Assignments.ToList())
- {
- if (index.Count == 0)
- {
- Assignments.Assignments.Remove(action);
- continue;
- }
-
- if (Ui.Component.Actions.Contains(action))
- continue;
-
- Assignments.ClearSlot(index[0].Hotbar, index[0].Slot, false);
- }
-
- Assignments.PreventAutoPopulate.RemoveWhere(action => !Ui.Component.Actions.Contains(action));
-
- Ui.UpdateUI();
- }
-
- public void HandleHotbarKeybind(byte slot, in PointerInputCmdHandler.PointerInputCmdArgs args)
- {
- Ui?.HandleHotbarKeybind(slot, args);
- }
-
- public void HandleChangeHotbarKeybind(byte hotbar, in PointerInputCmdHandler.PointerInputCmdArgs args)
- {
- Ui?.HandleChangeHotbarKeybind(hotbar, args);
- }
-
- private void OnPlayerDetached(EntityUid uid, ActionsComponent component, PlayerDetachedEvent args)
- {
- if (Ui == null) return;
- _uiManager.StateRoot.RemoveChild(Ui);
- Ui = null;
- }
-
- private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args)
- {
- Assignments = new(Hotbars, Slots);
- Ui = new ActionsUI(this, component);
- _uiManager.StateRoot.AddChild(Ui);
- UIDirty = true;
+ SubscribeLocalEvent((_, component, _) => component.PlayerAttached());
+ SubscribeLocalEvent((_, component, _) => component.PlayerDetached());
}
public override void Shutdown()
@@ -310,9 +85,9 @@ namespace Content.Client.Actions
{
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
if (playerEntity == null ||
- !EntityManager.TryGetComponent(playerEntity.Value, out var actionsComponent)) return false;
+ !EntityManager.TryGetComponent(playerEntity.Value, out var actionsComponent)) return false;
- HandleHotbarKeybind(slot, args);
+ actionsComponent.HandleHotbarKeybind(slot, args);
return true;
}, false);
}
@@ -323,363 +98,28 @@ namespace Content.Client.Actions
return new((in PointerInputCmdHandler.PointerInputCmdArgs args) =>
{
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
- if (!EntityManager.TryGetComponent(playerEntity, out var actionsComponent)) return false;
+ if (!EntityManager.TryGetComponent(playerEntity, out var actionsComponent)) return false;
- HandleChangeHotbarKeybind(hotbar, args);
+ actionsComponent.HandleChangeHotbarKeybind(hotbar, args);
return true;
},
false);
}
- private void ToggleActionsMenu()
- {
- Ui?.ToggleActionsMenu();
- }
-
- ///
- /// A action slot was pressed. This either performs the action or toggles the targeting mode.
- ///
- internal void OnSlotPressed(ActionSlot slot)
- {
- if (Ui == null)
- return;
-
- if (slot.Action == null || _playerManager.LocalPlayer?.ControlledEntity is not EntityUid user)
- return;
-
- if (slot.Action.Provider != null && Deleted(slot.Action.Provider))
- return;
-
- if (slot.Action is not InstantAction instantAction)
- {
- // for target actions, we go into "select target" mode, we don't
- // message the server until we actually pick our target.
-
- // if we're clicking the same thing we're already targeting for, then we simply cancel
- // targeting
- Ui.ToggleTargeting(slot);
- return;
- }
-
- if (slot.Action.ClientExclusive)
- {
- if (instantAction.Event != null)
- instantAction.Event.Performer = user;
-
- PerformAction(Ui.Component, instantAction, instantAction.Event, GameTiming.CurTime);
- }
- else
- {
- var request = new RequestPerformActionEvent(instantAction);
- EntityManager.RaisePredictiveEvent(request);
- }
- }
-
- private bool TargetingCancel(in PointerInputCmdHandler.PointerInputCmdArgs args)
- {
- if (!GameTiming.IsFirstTimePredicted)
- return false;
-
- // only do something for actual target-based actions
- if (Ui?.SelectingTargetFor?.Action == null)
- return false;
-
- Ui.StopTargeting();
- return true;
- }
-
- ///
- /// If the user clicked somewhere, and they are currently targeting an action, try and perform it.
- ///
private bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
- if (!GameTiming.IsFirstTimePredicted)
- return false;
+ var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
+ if (!EntityManager.TryGetComponent(playerEntity, out var actionsComponent)) return false;
- // only do something for actual target-based actions
- if (Ui?.SelectingTargetFor?.Action is not TargetedAction action)
- return false;
-
- if (_playerManager.LocalPlayer?.ControlledEntity is not EntityUid user)
- return false;
-
- if (!TryComp(user, out ActionsComponent? comp))
- return false;
-
- // Is the action currently valid?
- if (!action.Enabled
- || action.Charges != null && action.Charges == 0
- || action.Cooldown.HasValue && action.Cooldown.Value.End > GameTiming.CurTime)
- {
- // The user is targeting with this action, but it is not valid. Maybe mark this click as
- // handled and prevent further interactions.
- return !action.InteractOnMiss;
- }
-
- switch (action)
- {
- case WorldTargetAction mapTarget:
- return TryTargetWorld(args, mapTarget, user, comp) || !action.InteractOnMiss;
-
- case EntityTargetAction entTarget:
- return TargetEntity(args, entTarget, user, comp) || !action.InteractOnMiss;
-
- default:
- Logger.Error($"Unknown targeting action: {action.GetType()}");
- return false;
- }
+ return actionsComponent.TargetingOnUse(args);
}
- private bool TryTargetWorld(in PointerInputCmdHandler.PointerInputCmdArgs args, WorldTargetAction action, EntityUid user, ActionsComponent actionComp)
+ private void ToggleActionsMenu()
{
- var coords = args.Coordinates.ToMap(EntityManager);
+ var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
+ if (!EntityManager.TryGetComponent(playerEntity, out var actionsComponent)) return;
- if (!ValidateWorldTarget(user, coords, action))
- {
- // Invalid target.
- if (action.DeselectOnMiss)
- Ui?.StopTargeting();
-
- return false;
- }
-
- if (action.ClientExclusive)
- {
- if (action.Event != null)
- {
- action.Event.Target = coords;
- action.Event.Performer = user;
- }
-
- PerformAction(actionComp, action, action.Event, GameTiming.CurTime);
- }
- else
- EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, coords));
-
- if (!action.Repeat)
- Ui?.StopTargeting();
-
- return true;
- }
-
- private bool TargetEntity(in PointerInputCmdHandler.PointerInputCmdArgs args, EntityTargetAction action, EntityUid user, ActionsComponent actionComp)
- {
- if (!ValidateEntityTarget(user, args.EntityUid, action))
- {
- if (action.DeselectOnMiss)
- Ui?.StopTargeting();
-
- return false;
- }
-
- if (action.ClientExclusive)
- {
- if (action.Event != null)
- {
- action.Event.Target = args.EntityUid;
- action.Event.Performer = user;
- }
-
- PerformAction(actionComp, action, action.Event, GameTiming.CurTime);
- }
- else
- EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, args.EntityUid));
-
- if (!action.Repeat)
- Ui?.StopTargeting();
-
- return true;
- }
-
- ///
- /// Execute convenience functionality for actions (pop-ups, sound, speech)
- ///
- protected override bool PerformBasicActions(EntityUid user, ActionType action)
- {
- var performedAction = action.Sound != null
- || !string.IsNullOrWhiteSpace(action.UserPopup)
- || !string.IsNullOrWhiteSpace(action.Popup);
-
- if (!GameTiming.IsFirstTimePredicted)
- return performedAction;
-
- if (!string.IsNullOrWhiteSpace(action.UserPopup))
- {
- var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
- ? Loc.GetString(action.UserPopup)
- : Loc.GetString(action.UserPopup + action.PopupToggleSuffix);
-
- _popupSystem.PopupEntity(msg, user);
- }
- else if (!string.IsNullOrWhiteSpace(action.Popup))
- {
- var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
- ? Loc.GetString(action.Popup)
- : Loc.GetString(action.Popup + action.PopupToggleSuffix);
-
- _popupSystem.PopupEntity(msg, user);
- }
-
- if (action.Sound != null)
- SoundSystem.Play(Filter.Local(), action.Sound.GetSound(), user, action.AudioParams);
-
- return performedAction;
- }
-
- internal void StopTargeting()
- {
- _targetOutline.Disable();
- _interactionOutline.SetEnabled(true);
-
- if (!_overlayMan.TryGetOverlay(out var handOverlay) || handOverlay == null)
- return;
-
- handOverlay.IconOverride = null;
- handOverlay.EntityOverride = null;
- }
-
- internal void StartTargeting(TargetedAction action)
- {
- // override "held-item" overlay
- if (action.TargetingIndicator
- && _overlayMan.TryGetOverlay(out var handOverlay)
- && handOverlay != null)
- {
- if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Provider != null)
- {
- handOverlay.EntityOverride = action.Provider;
- }
- else if (action.Toggled && action.IconOn != null)
- handOverlay.IconOverride = action.IconOn.Frame0();
- else if (action.Icon != null)
- handOverlay.IconOverride = action.Icon.Frame0();
- }
-
- // TODO: allow world-targets to check valid positions. E.g., maybe:
- // - Draw a red/green ghost entity
- // - Add a yes/no checkmark where the HandItemOverlay usually is
-
- // Highlight valid entity targets
- if (action is not EntityTargetAction entityAction)
- return;
-
- Func? predicate = null;
-
- if (!entityAction.CanTargetSelf)
- predicate = e => e != entityAction.AttachedEntity;
-
- var range = entityAction.CheckCanAccess ? action.Range : -1;
-
- _interactionOutline.SetEnabled(false);
- _targetOutline.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, null);
- }
-
- internal void TryFillSlot(ActionSlot slot, byte hotbar, byte index)
- {
- if (Ui == null)
- return;
-
- var fillEvent = new FillActionSlotEvent();
- RaiseLocalEvent(Ui.Component.Owner, fillEvent, broadcast: true);
-
- if (fillEvent.Action == null)
- return;
-
- fillEvent.Action.ClientExclusive = true;
- fillEvent.Action.Temporary = true;
-
- Ui.Component.Actions.Add(fillEvent.Action);
- Assignments.AssignSlot(hotbar, index, fillEvent.Action);
-
- Ui.UpdateUI();
- }
-
- public void SaveActionAssignments(string path)
- {
- // Disabled until YamlMappingFix's sandbox issues are resolved.
-
- /*
- // Currently only tested with temporary innate actions (i.e., mapping actions). No guarantee it works with
- // other actions. If its meant to be used for full game state saving/loading, the entity that provides
- // actions needs to keep the same uid.
-
- var sequence = new SequenceDataNode();
-
- foreach (var (action, assigns) in Assignments.Assignments)
- {
- var slot = new MappingDataNode();
- slot.Add("action", _serializationManager.WriteValue(action));
- slot.Add("assignments", _serializationManager.WriteValue(assigns));
- sequence.Add(slot);
- }
-
- using var writer = _resourceManager.UserData.OpenWriteText(new ResourcePath(path).ToRootedPath());
- var stream = new YamlStream { new(sequence.ToSequenceNode()) };
- stream.Save(new YamlMappingFix(new Emitter(writer)), false);
- */
- }
-
- ///
- /// Load actions and their toolbar assignments from a file.
- ///
- public void LoadActionAssignments(string path, bool userData)
- {
- if (Ui == null)
- return;
-
- var file = new ResourcePath(path).ToRootedPath();
- TextReader reader = userData
- ? _resourceManager.UserData.OpenText(file)
- : _resourceManager.ContentFileReadText(file);
-
- var yamlStream = new YamlStream();
- yamlStream.Load(reader);
-
- if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
- return;
-
- foreach (var (action, assigns) in Assignments.Assignments)
- {
- foreach (var index in assigns)
- {
- Assignments.ClearSlot(index.Hotbar, index.Slot, true);
- }
- }
-
- foreach (var entry in sequence.Sequence)
- {
- if (entry is not MappingDataNode map)
- continue;
-
- if (!map.TryGet("action", out var actionNode))
- continue;
-
- var action = _serializationManager.ReadValueCast(typeof(ActionType), actionNode);
- if (action == null)
- continue;
-
- if (Ui.Component.Actions.TryGetValue(action, out var existingAction))
- {
- existingAction.CopyFrom(action);
- action = existingAction;
- }
- else
- Ui.Component.Actions.Add(action);
-
- if (!map.TryGet("assignments", out var assignmentNode))
- continue;
-
- var assignments = _serializationManager.ReadValueCast>(typeof(List<(byte Hotbar, byte Slot)>), assignmentNode);
- if (assignments == null)
- continue;
-
- foreach (var index in assignments)
- {
- Assignments.AssignSlot(index.Hotbar, index.Slot, action);
- }
- }
-
- UIDirty = true;
+ actionsComponent.ToggleActionsMenu();
}
}
}
diff --git a/Content.Client/Actions/Assignments/ActionAssignment.cs b/Content.Client/Actions/Assignments/ActionAssignment.cs
new file mode 100644
index 0000000000..91e1e7725c
--- /dev/null
+++ b/Content.Client/Actions/Assignments/ActionAssignment.cs
@@ -0,0 +1,90 @@
+using System;
+using Content.Shared.Actions;
+using Robust.Shared.GameObjects;
+
+namespace Content.Client.Actions.Assignments
+{
+ public struct ActionAssignment : IEquatable
+ {
+ private readonly ActionType _actionType;
+ private readonly ItemActionType _itemActionType;
+ private readonly EntityUid _item;
+ public Assignment Assignment { get; private init; }
+
+ private ActionAssignment(Assignment assignment, ActionType actionType, ItemActionType itemActionType, EntityUid item)
+ {
+ Assignment = assignment;
+ _actionType = actionType;
+ _itemActionType = itemActionType;
+ _item = item;
+ }
+
+ /// the action type, if our Assignment is Assignment.Action
+ /// true only if our Assignment is Assignment.Action
+ public bool TryGetAction(out ActionType actionType)
+ {
+ actionType = _actionType;
+ return Assignment == Assignment.Action;
+ }
+
+ /// the item action type, if our Assignment is Assignment.ItemActionWithoutItem
+ /// true only if our Assignment is Assignment.ItemActionWithoutItem
+ public bool TryGetItemActionWithoutItem(out ItemActionType itemActionType)
+ {
+ itemActionType = _itemActionType;
+ return Assignment == Assignment.ItemActionWithoutItem;
+ }
+
+ /// the item action type, if our Assignment is Assignment.ItemActionWithItem
+ /// the item UID providing the action, if our Assignment is Assignment.ItemActionWithItem
+ /// true only if our Assignment is Assignment.ItemActionWithItem
+ public bool TryGetItemActionWithItem(out ItemActionType itemActionType, out EntityUid item)
+ {
+ itemActionType = _itemActionType;
+ item = _item;
+ return Assignment == Assignment.ItemActionWithItem;
+ }
+
+ public static ActionAssignment For(ActionType actionType)
+ {
+ return new(Assignment.Action, actionType, default, default);
+ }
+
+ public static ActionAssignment For(ItemActionType actionType)
+ {
+ return new(Assignment.ItemActionWithoutItem, default, actionType, default);
+ }
+
+ public static ActionAssignment For(ItemActionType actionType, EntityUid item)
+ {
+ return new(Assignment.ItemActionWithItem, default, actionType, item);
+ }
+
+ public bool Equals(ActionAssignment other)
+ {
+ return _actionType == other._actionType && _itemActionType == other._itemActionType && Equals(_item, other._item);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is ActionAssignment other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(_actionType, _itemActionType, _item);
+ }
+
+ public override string ToString()
+ {
+ return $"{nameof(_actionType)}: {_actionType}, {nameof(_itemActionType)}: {_itemActionType}, {nameof(_item)}: {_item}, {nameof(Assignment)}: {Assignment}";
+ }
+ }
+
+ public enum Assignment : byte
+ {
+ Action,
+ ItemActionWithoutItem,
+ ItemActionWithItem
+ }
+}
diff --git a/Content.Client/Actions/Assignments/ActionAssignments.cs b/Content.Client/Actions/Assignments/ActionAssignments.cs
index 5c9c178eb1..a7e3e7d8aa 100644
--- a/Content.Client/Actions/Assignments/ActionAssignments.cs
+++ b/Content.Client/Actions/Assignments/ActionAssignments.cs
@@ -1,5 +1,7 @@
using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
+using Content.Shared.Actions.Components;
+using Robust.Shared.GameObjects;
+using System.Collections.Generic;
using System.Linq;
namespace Content.Client.Actions.Assignments
@@ -7,7 +9,6 @@ namespace Content.Client.Actions.Assignments
///
/// Tracks and manages the hotbar assignments for actions.
///
- [DataDefinition]
public sealed class ActionAssignments
{
// the slots and assignments fields hold client's assignments (what action goes in what slot),
@@ -18,7 +19,7 @@ namespace Content.Client.Actions.Assignments
/// x = hotbar number, y = slot of that hotbar (index 0 corresponds to the one labeled "1",
/// index 9 corresponds to the one labeled "0"). Essentially the inverse of _assignments.
///
- private readonly ActionType?[,] _slots;
+ private readonly ActionAssignment?[,] _slots;
///
/// Hotbar and slot assignment for each action type (slot index 0 corresponds to the one labeled "1",
@@ -27,14 +28,14 @@ namespace Content.Client.Actions.Assignments
/// it can still be assigned to a slot. Essentially the inverse of _slots.
/// There will be no entry if there is no assignment (no empty lists in this dict)
///
- [DataField("assignments")]
- public readonly Dictionary> Assignments = new();
+ private readonly Dictionary> _assignments;
///
/// Actions which have been manually cleared by the user, thus should not
/// auto-populate.
///
- public readonly SortedSet PreventAutoPopulate = new();
+ private readonly HashSet _preventAutoPopulate = new();
+ private readonly Dictionary> _preventAutoPopulateItem = new();
private readonly byte _numHotbars;
private readonly byte _numSlots;
@@ -43,25 +44,105 @@ namespace Content.Client.Actions.Assignments
{
_numHotbars = numHotbars;
_numSlots = numSlots;
- _slots = new ActionType?[numHotbars, numSlots];
+ _assignments = new Dictionary>();
+ _slots = new ActionAssignment?[numHotbars, numSlots];
}
- public bool Remove(ActionType action) => Replace(action, null);
-
- internal bool Replace(ActionType action, ActionType? newAction)
+ ///
+ /// Updates the assignments based on the current states of all the actions.
+ /// Newly-granted actions or item actions which don't have an assignment will be assigned a slot
+ /// automatically (unless they've been manually cleared). Item-based actions
+ /// which no longer have an associated state will be decoupled from their item.
+ ///
+ public void Reconcile(byte currentHotbar, IReadOnlyDictionary actionStates,
+ IReadOnlyDictionary> itemActionStates,
+ bool actionMenuLocked)
{
- if (!Assignments.Remove(action, out var assigns))
- return false;
-
- if (newAction != null)
- Assignments[newAction] = assigns;
-
- foreach (var (bar, slot) in assigns)
+ // if we've been granted any actions which have no assignment to any hotbar, we must auto-populate them
+ // into the hotbar so the user knows about them.
+ // We fill their current hotbar first, rolling over to the next open slot on the next hotbar.
+ foreach (var actionState in actionStates)
{
- _slots[bar, slot] = newAction;
+ var assignment = ActionAssignment.For(actionState.Key);
+ if (actionState.Value.Enabled && !_assignments.ContainsKey(assignment))
+ {
+ AutoPopulate(assignment, currentHotbar, false);
+ }
}
- return true;
+ foreach (var (item, itemStates) in itemActionStates)
+ {
+ foreach (var itemActionState in itemStates)
+ {
+ // unlike regular actions, we DO actually show user their new item action even when it's disabled.
+ // this allows them to instantly see when an action may be possible that is provided by an item but
+ // something is preventing it
+ // Note that we are checking if there is an explicit assignment for this item action + item,
+ // we will determine during auto-population if we should tie the item to an existing "item action only"
+ // assignment
+ var assignment = ActionAssignment.For(itemActionState.Key, item);
+ if (!_assignments.ContainsKey(assignment))
+ {
+ AutoPopulate(assignment, currentHotbar, false);
+ }
+ }
+ }
+
+ // We need to figure out which current item action assignments we had
+ // which once had an associated item but have been revoked (based on our newly provided action states)
+ // so we can dissociate them from the item. If the provided action states do not
+ // have a state for this action type + item, we can assume that the action has been revoked for that item.
+ var assignmentsWithoutItem = new List>>();
+ foreach (var assignmentEntry in _assignments)
+ {
+ if (!assignmentEntry.Key.TryGetItemActionWithItem(out var actionType, out var item))
+ {
+ continue;
+ }
+
+ // we have this assignment currently tied to an item,
+ // check if it no longer has an associated item in our dict of states
+ if (itemActionStates.TryGetValue(item, out var states))
+ {
+ if (states.ContainsKey(actionType))
+ {
+ // we have a state for this item + action type so we won't
+ // remove the item from the assignment
+ continue;
+ }
+ }
+
+ assignmentsWithoutItem.Add(assignmentEntry);
+ }
+
+ // reassign without the item for each assignment we found that no longer has an associated item
+ foreach (var (assignment, slots) in assignmentsWithoutItem)
+ {
+ foreach (var (hotbar, slot) in slots)
+ {
+ if (!assignment.TryGetItemActionWithItem(out var actionType, out _))
+ {
+ continue;
+ }
+
+ if (actionMenuLocked)
+ {
+ AssignSlot(hotbar, slot, ActionAssignment.For(actionType));
+ }
+ else
+ {
+ ClearSlot(hotbar, slot, false);
+ }
+ }
+ }
+
+ // Additionally, we must find items which have no action states at all in our newly provided states so
+ // we can assume their item was unequipped and reset them to allow auto-population.
+ var itemsWithoutState = _preventAutoPopulateItem.Keys.Where(item => !itemActionStates.ContainsKey(item));
+ foreach (var toRemove in itemsWithoutState)
+ {
+ _preventAutoPopulateItem.Remove(toRemove);
+ }
}
///
@@ -70,18 +151,18 @@ namespace Content.Client.Actions.Assignments
/// hotbar whose slot is being assigned
/// slot of the hotbar to assign to (0 = the slot labeled 1, 9 = the slot labeled 0)
/// action to assign to the slot
- public void AssignSlot(byte hotbar, byte slot, ActionType actionType)
+ public void AssignSlot(byte hotbar, byte slot, ActionAssignment actionType)
{
ClearSlot(hotbar, slot, false);
_slots[hotbar, slot] = actionType;
- if (Assignments.TryGetValue(actionType, out var slotList))
+ if (_assignments.TryGetValue(actionType, out var slotList))
{
slotList.Add((hotbar, slot));
}
else
{
var newList = new List<(byte Hotbar, byte Slot)> { (hotbar, slot) };
- Assignments[actionType] = newList;
+ _assignments[actionType] = newList;
}
}
@@ -102,21 +183,40 @@ namespace Content.Client.Actions.Assignments
// (keeping in mind something can be assigned multiple slots)
var currentAction = _slots[hotbar, slot];
- if (currentAction == null)
+ if (!currentAction.HasValue)
+ {
return;
+ }
if (preventAutoPopulate)
- PreventAutoPopulate.Add(currentAction);
+ {
+ var assignment = currentAction.Value;
- var assignmentList = Assignments[currentAction];
+ if (assignment.TryGetAction(out var actionType))
+ {
+ _preventAutoPopulate.Add(actionType);
+ }
+ else if (assignment.TryGetItemActionWithItem(out var itemActionType, out var item))
+ {
+ if (!_preventAutoPopulateItem.TryGetValue(item, out var actionTypes))
+ {
+ actionTypes = new HashSet();
+ _preventAutoPopulateItem[item] = actionTypes;
+ }
+
+ actionTypes.Add(itemActionType);
+ }
+ }
+
+ var assignmentList = _assignments[currentAction.Value];
assignmentList = assignmentList.Where(a => a.Hotbar != hotbar || a.Slot != slot).ToList();
if (!assignmentList.Any())
{
- Assignments.Remove(currentAction);
+ _assignments.Remove(currentAction.Value);
}
else
{
- Assignments[currentAction] = assignmentList;
+ _assignments[currentAction.Value] = assignmentList;
}
_slots[hotbar, slot] = null;
@@ -131,10 +231,45 @@ namespace Content.Client.Actions.Assignments
/// regardless of whether this assignment has been prevented from auto population
/// via ClearSlot's preventAutoPopulate parameter. If false, will have no effect
/// if this assignment has been prevented from auto population.
- public void AutoPopulate(ActionType toAssign, byte currentHotbar, bool force = true)
+ public void AutoPopulate(ActionAssignment toAssign, byte currentHotbar, bool force = true)
{
- if (!force && PreventAutoPopulate.Contains(toAssign))
+ if (ShouldPreventAutoPopulate(toAssign, force))
+ {
return;
+ }
+
+ // if the assignment to make is an item action with an associated item,
+ // then first look for currently assigned item actions without an item, to replace with this
+ // assignment
+ if (toAssign.TryGetItemActionWithItem(out var actionType, out var _))
+ {
+ if (_assignments.TryGetValue(ActionAssignment.For(actionType),
+ out var possibilities))
+ {
+ // use the closest assignment to current hotbar
+ byte hotbar = 0;
+ byte slot = 0;
+ var minCost = int.MaxValue;
+ foreach (var possibility in possibilities)
+ {
+ var cost = possibility.Slot + _numSlots * (currentHotbar >= possibility.Hotbar
+ ? currentHotbar - possibility.Hotbar
+ : _numHotbars - currentHotbar + possibility.Hotbar);
+ if (cost < minCost)
+ {
+ hotbar = possibility.Hotbar;
+ slot = possibility.Slot;
+ minCost = cost;
+ }
+ }
+
+ if (minCost != int.MaxValue)
+ {
+ AssignSlot(hotbar, slot, toAssign);
+ return;
+ }
+ }
+ }
for (byte hotbarOffset = 0; hotbarOffset < _numHotbars; hotbarOffset++)
{
@@ -142,10 +277,21 @@ namespace Content.Client.Actions.Assignments
{
var hotbar = (byte) ((currentHotbar + hotbarOffset) % _numHotbars);
var slotAssignment = _slots[hotbar, slot];
+ if (slotAssignment.HasValue)
+ {
+ // if the assignment in this slot is an item action without an associated item,
+ // then tie it to the current item if we are trying to auto populate an item action.
+ if (toAssign.Assignment == Assignment.ItemActionWithItem &&
+ slotAssignment.Value.Assignment == Assignment.ItemActionWithoutItem)
+ {
+ AssignSlot(hotbar, slot, toAssign);
+ return;
+ }
- if (slotAssignment != null)
continue;
+ }
+ // slot's empty, assign
AssignSlot(hotbar, slot, toAssign);
return;
}
@@ -153,15 +299,36 @@ namespace Content.Client.Actions.Assignments
// there was no empty slot
}
+ private bool ShouldPreventAutoPopulate(ActionAssignment assignment, bool force)
+ {
+ if (force)
+ {
+ return false;
+ }
+
+ if (assignment.TryGetAction(out var actionType))
+ {
+ return _preventAutoPopulate.Contains(actionType);
+ }
+
+ if (assignment.TryGetItemActionWithItem(out var itemActionType, out var item))
+ {
+ return _preventAutoPopulateItem.TryGetValue(item,
+ out var itemActionTypes) && itemActionTypes.Contains(itemActionType);
+ }
+
+ return false;
+ }
+
///
/// Gets the assignment to the indicated slot if there is one.
///
- public ActionType? this[in byte hotbar, in byte slot] => _slots[hotbar, slot];
+ public ActionAssignment? this[in byte hotbar, in byte slot] => _slots[hotbar, slot];
/// true if we have the assignment assigned to some slot
- public bool HasAssignment(ActionType assignment)
+ public bool HasAssignment(ActionAssignment assignment)
{
- return Assignments.ContainsKey(assignment);
+ return _assignments.ContainsKey(assignment);
}
}
}
diff --git a/Content.Client/Actions/ClientActionsComponent.cs b/Content.Client/Actions/ClientActionsComponent.cs
new file mode 100644
index 0000000000..1032a63ae4
--- /dev/null
+++ b/Content.Client/Actions/ClientActionsComponent.cs
@@ -0,0 +1,252 @@
+using Content.Client.Actions.Assignments;
+using Content.Client.Actions.UI;
+using Content.Client.Items.Managers;
+using Content.Shared.Actions.Components;
+using Content.Shared.Actions.Prototypes;
+using Robust.Client.GameObjects;
+using Robust.Client.Player;
+using Robust.Client.UserInterface;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Client.Actions
+{
+ ///
+ [RegisterComponent]
+ [ComponentReference(typeof(SharedActionsComponent))]
+ public sealed class ClientActionsComponent : SharedActionsComponent
+ {
+ public const byte Hotbars = 9;
+ public const byte Slots = 10;
+
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
+
+ private ActionsUI? _ui;
+ private EntityUid _highlightedEntity;
+
+ ///
+ /// Current assignments for all hotbars / slots for this entity.
+ ///
+ public ActionAssignments Assignments { get; } = new(Hotbars, Slots);
+
+ ///
+ /// Allows calculating if we need to act due to this component being controlled by the current mob
+ ///
+ [ViewVariables]
+ private bool CurrentlyControlled => _playerManager.LocalPlayer != null && _playerManager.LocalPlayer.ControlledEntity == Owner;
+
+ protected override void Shutdown()
+ {
+ base.Shutdown();
+ PlayerDetached();
+ }
+
+ public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
+ {
+ base.HandleComponentState(curState, nextState);
+
+ if (curState is not ActionComponentState)
+ {
+ return;
+ }
+
+ UpdateUI();
+ }
+
+ public void PlayerAttached()
+ {
+ if (!CurrentlyControlled || _ui != null)
+ {
+ return;
+ }
+
+ _ui = new ActionsUI(this);
+ IoCManager.Resolve().StateRoot.AddChild(_ui);
+ UpdateUI();
+ }
+
+ public void PlayerDetached()
+ {
+ if (_ui == null) return;
+ IoCManager.Resolve().StateRoot.RemoveChild(_ui);
+ _ui = null;
+ }
+
+ public void HandleHotbarKeybind(byte slot, in PointerInputCmdHandler.PointerInputCmdArgs args)
+ {
+ _ui?.HandleHotbarKeybind(slot, args);
+ }
+
+ public void HandleChangeHotbarKeybind(byte hotbar, in PointerInputCmdHandler.PointerInputCmdArgs args)
+ {
+ _ui?.HandleChangeHotbarKeybind(hotbar, args);
+ }
+
+ ///
+ /// Updates the displayed hotbar (and menu) based on current state of actions.
+ ///
+ private void UpdateUI()
+ {
+ if (!CurrentlyControlled || _ui == null)
+ {
+ return;
+ }
+
+ Assignments.Reconcile(_ui.SelectedHotbar, ActionStates(), ItemActionStates(), _ui.Locked);
+
+ _ui.UpdateUI();
+ }
+
+ public void AttemptAction(ActionSlot slot)
+ {
+
+ var attempt = slot.ActionAttempt();
+ if (attempt == null) return;
+
+ switch (attempt.Action.BehaviorType)
+ {
+ case BehaviorType.Instant:
+ // for instant actions, we immediately tell the server we're doing it
+#pragma warning disable 618
+ SendNetworkMessage(attempt.PerformInstantActionMessage());
+#pragma warning restore 618
+ break;
+ case BehaviorType.Toggle:
+ // for toggle actions, we immediately tell the server we're toggling it.
+ if (attempt.TryGetActionState(this, out var actionState))
+ {
+ // TODO: At the moment we always predict that the toggle will work clientside,
+ // even if it sometimes may not (it will be reset by the server if wrong).
+ attempt.ToggleAction(this, !actionState.ToggledOn);
+ slot.ToggledOn = !actionState.ToggledOn;
+#pragma warning disable 618
+ SendNetworkMessage(attempt.PerformToggleActionMessage(!actionState.ToggledOn));
+#pragma warning restore 618
+ }
+ else
+ {
+ Logger.ErrorS("action", "attempted to toggle action {0} which has" +
+ " unknown state", attempt);
+ }
+
+ break;
+ case BehaviorType.TargetPoint:
+ case BehaviorType.TargetEntity:
+ // for target actions, we go into "select target" mode, we don't
+ // message the server until we actually pick our target.
+
+ // if we're clicking the same thing we're already targeting for, then we simply cancel
+ // targeting
+ _ui?.ToggleTargeting(slot);
+ break;
+ case BehaviorType.None:
+ break;
+ default:
+ Logger.ErrorS("action", "unhandled action press for action {0}",
+ attempt);
+ break;
+ }
+ }
+
+ ///
+ /// Handles clicks when selecting the target for an action. Only has an effect when currently
+ /// selecting a target.
+ ///
+ public bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
+ {
+ // not currently predicted
+ if (EntitySystem.Get().Predicted) return false;
+
+ // only do something for actual target-based actions
+ if (_ui?.SelectingTargetFor?.Action == null ||
+ (!_ui.SelectingTargetFor.Action.IsTargetAction)) return false;
+
+ // do nothing if we know it's on cooldown
+ if (_ui.SelectingTargetFor.IsOnCooldown) return false;
+
+ var attempt = _ui.SelectingTargetFor.ActionAttempt();
+ if (attempt == null)
+ {
+ _ui.StopTargeting();
+ return false;
+ }
+
+ switch (_ui.SelectingTargetFor.Action.BehaviorType)
+ {
+ case BehaviorType.TargetPoint:
+ {
+ // send our action to the server, we chose our target
+#pragma warning disable 618
+ SendNetworkMessage(attempt.PerformTargetPointActionMessage(args));
+#pragma warning restore 618
+ if (!attempt.Action.Repeat)
+ {
+ _ui.StopTargeting();
+ }
+ return true;
+ }
+ // target the currently hovered entity, if there is one
+ case BehaviorType.TargetEntity when args.EntityUid != EntityUid.Invalid:
+ {
+ // send our action to the server, we chose our target
+#pragma warning disable 618
+ SendNetworkMessage(attempt.PerformTargetEntityActionMessage(args));
+#pragma warning restore 618
+ if (!attempt.Action.Repeat)
+ {
+ _ui.StopTargeting();
+ }
+ return true;
+ }
+ // we are supposed to target an entity but we didn't click it
+ case BehaviorType.TargetEntity when args.EntityUid == EntityUid.Invalid:
+ {
+ if (attempt.Action.DeselectWhenEntityNotClicked)
+ _ui.StopTargeting();
+ return false;
+ }
+ default:
+ _ui.StopTargeting();
+ return false;
+ }
+ }
+
+ protected override void AfterActionChanged()
+ {
+ UpdateUI();
+ }
+
+ ///
+ /// Highlights the item slot (inventory or hand) that contains this item
+ ///
+ ///
+ public void HighlightItemSlot(EntityUid item)
+ {
+ StopHighlightingItemSlots();
+
+ _highlightedEntity = item;
+ _itemSlotManager.HighlightEntity(item);
+ }
+
+ ///
+ /// Stops highlighting any item slots we are currently highlighting.
+ /// H
+ public void StopHighlightingItemSlots()
+ {
+ if (_highlightedEntity == default)
+ return;
+
+ _itemSlotManager.UnHighlightEntity(_highlightedEntity);
+ _highlightedEntity = default;
+ }
+
+ public void ToggleActionsMenu()
+ {
+ _ui?.ToggleActionsMenu();
+ }
+ }
+}
diff --git a/Content.Client/Actions/UI/ActionAlertTooltip.cs b/Content.Client/Actions/UI/ActionAlertTooltip.cs
index 319c682299..392e67873a 100644
--- a/Content.Client/Actions/UI/ActionAlertTooltip.cs
+++ b/Content.Client/Actions/UI/ActionAlertTooltip.cs
@@ -1,7 +1,5 @@
using System;
using Content.Client.Stylesheets;
-using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
@@ -91,7 +89,7 @@ namespace Content.Client.Actions.UI
{
var duration = Cooldown.Value.End - Cooldown.Value.Start;
_cooldownLabel.SetMessage(FormattedMessage.FromMarkup(
- $"[color=#a10505]{(int) duration.TotalSeconds} sec cooldown ({(int) timeLeft.TotalSeconds + 1} sec remaining)[/color]"));
+ $"[color=#a10505]{duration.Seconds} sec cooldown ({timeLeft.Seconds + 1} sec remaining)[/color]"));
_cooldownLabel.Visible = true;
}
else
diff --git a/Content.Client/Actions/UI/ActionMenu.cs b/Content.Client/Actions/UI/ActionMenu.cs
index edb341a644..b3140a32b5 100644
--- a/Content.Client/Actions/UI/ActionMenu.cs
+++ b/Content.Client/Actions/UI/ActionMenu.cs
@@ -1,16 +1,22 @@
+using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
+using Content.Client.Actions.Assignments;
using Content.Client.DragDrop;
using Content.Client.HUD;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
+using Content.Shared.Actions.Prototypes;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.Utility;
using Robust.Shared.Input;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Log;
using Robust.Shared.Timing;
using static Robust.Client.UserInterface.Controls.BaseButton;
using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -23,35 +29,28 @@ namespace Content.Client.Actions.UI
///
public sealed class ActionMenu : DefaultWindow
{
- // Pre-defined global filters that can be used to select actions based on their properties (as opposed to their
- // own yaml-defined filters).
- // TODO LOC STRINGs
- private const string AllFilter = "all";
- private const string ItemFilter = "item";
- private const string InnateFilter = "innate";
- private const string EnabledFilter = "enabled";
- private const string InstantFilter = "instant";
- private const string TargetedFilter = "targeted";
-
- private readonly string[] _filters =
- {
- AllFilter,
- ItemFilter,
- InnateFilter,
- EnabledFilter,
- InstantFilter,
- TargetedFilter
- };
-
+ private const string ItemTag = "item";
+ private const string NotItemTag = "not item";
+ private const string InstantActionTag = "instant";
+ private const string ToggleActionTag = "toggle";
+ private const string TargetActionTag = "target";
+ private const string AllActionsTag = "all";
+ private const string GrantedActionsTag = "granted";
private const int MinSearchLength = 3;
private static readonly Regex NonAlphanumeric = new Regex(@"\W", RegexOptions.Compiled);
private static readonly Regex Whitespace = new Regex(@"\s+", RegexOptions.Compiled);
+ private static readonly BaseActionPrototype[] EmptyActionList = Array.Empty();
///
/// Is an action currently being dragged from this window?
///
public bool IsDragging => _dragDropHelper.IsDragging;
+ // parallel list of actions currently selectable in itemList
+ private BaseActionPrototype[] _actionList = new BaseActionPrototype[0];
+
+ private readonly ActionManager _actionManager;
+ private readonly ClientActionsComponent _actionsComponent;
private readonly ActionsUI _actionsUI;
private readonly LineEdit _searchBar;
private readonly MultiselectOptionButton _filterButton;
@@ -61,16 +60,17 @@ namespace Content.Client.Actions.UI
private readonly TextureRect _dragShadow;
private readonly IGameHud _gameHud;
private readonly DragDropHelper _dragDropHelper;
- private readonly IEntityManager _entMan;
- public ActionMenu(ActionsUI actionsUI)
+
+ public ActionMenu(ClientActionsComponent actionsComponent, ActionsUI actionsUI)
{
+ _actionsComponent = actionsComponent;
_actionsUI = actionsUI;
+ _actionManager = IoCManager.Resolve();
_gameHud = IoCManager.Resolve();
- _entMan = IoCManager.Resolve();
Title = Loc.GetString("ui-actionmenu-title");
- MinSize = (320, 300);
+ MinSize = (300, 300);
Contents.AddChild(new BoxContainer
{
@@ -115,14 +115,27 @@ namespace Content.Client.Actions.UI
}
});
- foreach (var tag in _filters)
+ // populate filters from search tags
+ var filterTags = new List();
+ foreach (var action in _actionManager.EnumerateActions())
+ {
+ filterTags.AddRange(action.Filters);
+ }
+
+ // special one to filter to only include item actions
+ filterTags.Add(ItemTag);
+ filterTags.Add(NotItemTag);
+ filterTags.Add(InstantActionTag);
+ filterTags.Add(ToggleActionTag);
+ filterTags.Add(TargetActionTag);
+ filterTags.Add(AllActionsTag);
+ filterTags.Add(GrantedActionsTag);
+
+ foreach (var tag in filterTags.Distinct().OrderBy(tag => tag))
{
_filterButton.AddItem( CultureInfo.CurrentCulture.TextInfo.ToTitleCase(tag), tag);
}
- // default to showing all actions.
- _filterButton.SelectKey(AllFilter);
-
UpdateFilterLabel();
_dragShadow = new TextureRect
@@ -144,6 +157,13 @@ namespace Content.Client.Actions.UI
_searchBar.OnTextChanged += OnSearchTextChanged;
_filterButton.OnItemSelected += OnFilterItemSelected;
_gameHud.ActionsButtonDown = true;
+ foreach (var actionMenuControl in _resultsGrid.Children)
+ {
+ var actionMenuItem = (ActionMenuItem) actionMenuControl;
+ actionMenuItem.OnButtonDown += OnItemButtonDown;
+ actionMenuItem.OnButtonUp += OnItemButtonUp;
+ actionMenuItem.OnPressed += OnItemPressed;
+ }
}
protected override void ExitedTree()
@@ -179,7 +199,7 @@ namespace Content.Client.Actions.UI
private bool OnBeginActionDrag()
{
- _dragShadow.Texture = _dragDropHelper.Dragged?.Action?.Icon?.Frame0();
+ _dragShadow.Texture = _dragDropHelper.Dragged!.Action.Icon.Frame0();
// don't make visible until frameupdate, otherwise it'll flicker
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled.Position - (32, 32));
return true;
@@ -224,7 +244,47 @@ namespace Content.Client.Actions.UI
return;
}
- _actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, _dragDropHelper.Dragged.Action);
+ // drag and drop
+ switch (_dragDropHelper.Dragged.Action)
+ {
+ // assign the dragged action to the target slot
+ case ActionPrototype actionPrototype:
+ _actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, ActionAssignment.For(actionPrototype.ActionType));
+ break;
+ case ItemActionPrototype itemActionPrototype:
+ // the action menu doesn't show us if the action has an associated item,
+ // so when we perform the assignment, we should check if we currently have an unassigned state
+ // for this item and assign it tied to that item if so, otherwise assign it "itemless"
+
+ // this is not particularly efficient but we don't maintain an index from
+ // item action type to its action states, and this method should be pretty infrequent so it's probably fine
+ var assigned = false;
+ foreach (var (item, itemStates) in _actionsComponent.ItemActionStates())
+ {
+ foreach (var (actionType, _) in itemStates)
+ {
+ if (actionType != itemActionPrototype.ActionType) continue;
+ var assignment = ActionAssignment.For(actionType, item);
+ if (_actionsComponent.Assignments.HasAssignment(assignment)) continue;
+ // no assignment for this state, assign tied to the item
+ assigned = true;
+ _actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, assignment);
+ break;
+ }
+
+ if (assigned)
+ {
+ break;
+ }
+ }
+
+ if (!assigned)
+ {
+ _actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, ActionAssignment.For(itemActionPrototype.ActionType));
+ }
+ break;
+ }
+
_actionsUI.UpdateUI();
}
@@ -240,8 +300,19 @@ namespace Content.Client.Actions.UI
private void OnItemPressed(ButtonEventArgs args)
{
if (args.Button is not ActionMenuItem actionMenuItem) return;
+ switch (actionMenuItem.Action)
+ {
+ case ActionPrototype actionPrototype:
+ _actionsComponent.Assignments.AutoPopulate(ActionAssignment.For(actionPrototype.ActionType), _actionsUI.SelectedHotbar);
+ break;
+ case ItemActionPrototype itemActionPrototype:
+ _actionsComponent.Assignments.AutoPopulate(ActionAssignment.For(itemActionPrototype.ActionType), _actionsUI.SelectedHotbar);
+ break;
+ default:
+ Logger.ErrorS("action", "unexpected action prototype {0}", actionMenuItem.Action);
+ break;
+ }
- _actionsUI.System.Assignments.AutoPopulate(actionMenuItem.Action, _actionsUI.SelectedHotbar);
_actionsUI.UpdateUI();
}
@@ -270,7 +341,7 @@ namespace Content.Client.Actions.UI
return;
}
- var matchingActions = _actionsUI.Component.Actions
+ var matchingActions = _actionManager.EnumerateActions()
.Where(a => MatchesSearchCriteria(a, search, _filterButton.SelectedKeys));
PopulateActions(matchingActions);
@@ -290,7 +361,7 @@ namespace Content.Client.Actions.UI
}
}
- private bool MatchesSearchCriteria(ActionType action, string standardizedSearch,
+ private bool MatchesSearchCriteria(BaseActionPrototype action, string standardizedSearch,
IReadOnlyList selectedFilterTags)
{
// check filter tag match first - each action must contain all filter tags currently selected.
@@ -306,32 +377,56 @@ namespace Content.Client.Actions.UI
return true;
}
+ if (Standardize(ActionTypeString(action)).Contains(standardizedSearch))
+ {
+ return true;
+ }
+
+ // allows matching by typing spaces between the enum case changes, like "xeno spit" if the
+ // actiontype is "XenoSpit"
+ if (Standardize(ActionTypeString(action), true).Contains(standardizedSearch))
+ {
+ return true;
+ }
+
if (Standardize(action.Name.ToString()).Contains(standardizedSearch))
{
return true;
}
- // search by provider name
- if (action.Provider == null || action.Provider == _actionsUI.Component.Owner)
- return false;
+ return false;
- var name = _entMan.GetComponent(action.Provider.Value).EntityName;
- return Standardize(name).Contains(standardizedSearch);
}
- private bool ActionMatchesFilterTag(ActionType action, string tag)
+ private string ActionTypeString(BaseActionPrototype baseActionPrototype)
+ {
+ if (baseActionPrototype is ActionPrototype actionPrototype)
+ {
+ return actionPrototype.ActionType.ToString();
+ }
+ if (baseActionPrototype is ItemActionPrototype itemActionPrototype)
+ {
+ return itemActionPrototype.ActionType.ToString();
+ }
+ throw new InvalidOperationException();
+ }
+
+ private bool ActionMatchesFilterTag(BaseActionPrototype action, string tag)
{
return tag switch
{
- EnabledFilter => action.Enabled,
- ItemFilter => action.Provider != null && action.Provider != _actionsUI.Component.Owner,
- InnateFilter => action.Provider == null || action.Provider == _actionsUI.Component.Owner,
- InstantFilter => action is InstantAction,
- TargetedFilter => action is TargetedAction,
- _ => true
+ AllActionsTag => true,
+ GrantedActionsTag => _actionsComponent.IsGranted(action),
+ ItemTag => action is ItemActionPrototype,
+ NotItemTag => action is ActionPrototype,
+ InstantActionTag => action.BehaviorType == BehaviorType.Instant,
+ TargetActionTag => action.IsTargetAction,
+ ToggleActionTag => action.BehaviorType == BehaviorType.Toggle,
+ _ => action.Filters.Contains(tag)
};
}
+
///
/// Standardized form is all lowercase, no non-alphanumeric characters (converted to whitespace),
/// trimmed, 1 space max per whitespace gap,
@@ -377,15 +472,16 @@ namespace Content.Client.Actions.UI
return newText.ToString();
}
- private void PopulateActions(IEnumerable actions)
+ private void PopulateActions(IEnumerable actions)
{
ClearList();
- foreach (var action in actions)
+ _actionList = actions.ToArray();
+ foreach (var action in _actionList.OrderBy(act => act.Name.ToString()))
{
- var actionItem = new ActionMenuItem(_actionsUI, action, OnItemFocusExited);
+ var actionItem = new ActionMenuItem(action, OnItemFocusExited);
_resultsGrid.Children.Add(actionItem);
- actionItem.SetActionState(action.Enabled);
+ actionItem.SetActionState(_actionsComponent.IsGranted(action));
actionItem.OnButtonDown += OnItemButtonDown;
actionItem.OnButtonUp += OnItemButtonUp;
actionItem.OnPressed += OnItemPressed;
@@ -400,6 +496,7 @@ namespace Content.Client.Actions.UI
((ActionMenuItem) actionItem).OnPressed -= OnItemPressed;
}
_resultsGrid.Children.Clear();
+ _actionList = EmptyActionList;
}
///
@@ -411,10 +508,8 @@ namespace Content.Client.Actions.UI
foreach (var actionItem in _resultsGrid.Children)
{
var actionMenuItem = ((ActionMenuItem) actionItem);
- actionMenuItem.SetActionState(actionMenuItem.Action.Enabled);
+ actionMenuItem.SetActionState(_actionsComponent.IsGranted(actionMenuItem.Action));
}
-
- SearchAndDisplay();
}
protected override void FrameUpdate(FrameEventArgs args)
diff --git a/Content.Client/Actions/UI/ActionMenuItem.cs b/Content.Client/Actions/UI/ActionMenuItem.cs
index d82801d304..03293e03f5 100644
--- a/Content.Client/Actions/UI/ActionMenuItem.cs
+++ b/Content.Client/Actions/UI/ActionMenuItem.cs
@@ -1,20 +1,12 @@
-using System;
+using System;
using Content.Client.Stylesheets;
-using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
+using Content.Shared.Actions.Prototypes;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
-using Robust.Shared.Utility;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Actions.UI
{
- // TODO merge this with action-slot when it gets XAMLd
- // this has way too much overlap, especially now that they both have the item-sprite icons.
-
///
/// An individual action visible in the action menu.
///
@@ -24,165 +16,28 @@ namespace Content.Client.Actions.UI
// quickly explore what each action is
private const float CustomTooltipDelay = 0.2f;
- private readonly TextureRect _bigActionIcon;
- private readonly TextureRect _smallActionIcon;
- private readonly SpriteView _smallItemSpriteView;
- private readonly SpriteView _bigItemSpriteView;
-
- public ActionType Action;
+ public BaseActionPrototype Action { get; private set; }
private Action _onControlFocusExited;
- private readonly ActionsUI _actionsUI;
-
- public ActionMenuItem(ActionsUI actionsUI, ActionType action, Action onControlFocusExited)
+ public ActionMenuItem(BaseActionPrototype action, Action onControlFocusExited)
{
- _actionsUI = actionsUI;
- Action = action;
_onControlFocusExited = onControlFocusExited;
+ Action = action;
MinSize = (64, 64);
VerticalAlignment = VAlignment.Top;
- _bigActionIcon = new TextureRect
+ AddChild(new TextureRect
{
HorizontalExpand = true,
VerticalExpand = true,
Stretch = TextureRect.StretchMode.Scale,
- Visible = false
- };
- _bigItemSpriteView = new SpriteView
- {
- HorizontalExpand = true,
- VerticalExpand = true,
- Scale = (2, 2),
- Visible = false,
- OverrideDirection = Direction.South,
- };
- _smallActionIcon = new TextureRect
- {
- HorizontalAlignment = HAlignment.Right,
- VerticalAlignment = VAlignment.Bottom,
- Stretch = TextureRect.StretchMode.Scale,
- Visible = false
- };
- _smallItemSpriteView = new SpriteView
- {
- HorizontalAlignment = HAlignment.Right,
- VerticalAlignment = VAlignment.Bottom,
- Visible = false,
- OverrideDirection = Direction.South,
- };
-
- // padding to the left of the small icon
- var paddingBoxItemIcon = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- HorizontalExpand = true,
- VerticalExpand = true,
- MinSize = (64, 64)
- };
- paddingBoxItemIcon.AddChild(new Control()
- {
- MinSize = (32, 32),
+ Texture = action.Icon.Frame0()
});
- paddingBoxItemIcon.AddChild(new Control
- {
- Children =
- {
- _smallActionIcon,
- _smallItemSpriteView
- }
- });
- AddChild(_bigActionIcon);
- AddChild(_bigItemSpriteView);
- AddChild(paddingBoxItemIcon);
TooltipDelay = CustomTooltipDelay;
TooltipSupplier = SupplyTooltip;
- UpdateIcons();
- }
-
-
- public void UpdateIcons()
- {
- UpdateItemIcon();
-
- if (Action == null)
- {
- SetActionIcon(null);
- return;
- }
-
- if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null)
- SetActionIcon(Action.IconOn.Frame0());
- else
- SetActionIcon(Action.Icon?.Frame0());
- }
-
- private void SetActionIcon(Texture? texture)
- {
- if (texture == null || Action == null)
- {
- _bigActionIcon.Texture = null;
- _bigActionIcon.Visible = false;
- _smallActionIcon.Texture = null;
- _smallActionIcon.Visible = false;
- }
- else if (Action.Provider != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem)
- {
- _smallActionIcon.Texture = texture;
- _smallActionIcon.Modulate = Action.IconColor;
- _smallActionIcon.Visible = true;
- _bigActionIcon.Texture = null;
- _bigActionIcon.Visible = false;
- }
- else
- {
- _bigActionIcon.Texture = texture;
- _bigActionIcon.Modulate = Action.IconColor;
- _bigActionIcon.Visible = true;
- _smallActionIcon.Texture = null;
- _smallActionIcon.Visible = false;
- }
- }
-
- private void UpdateItemIcon()
- {
- if (Action?.Provider == null || !IoCManager.Resolve().TryGetComponent(Action.Provider.Value, out SpriteComponent sprite))
- {
- _bigItemSpriteView.Visible = false;
- _bigItemSpriteView.Sprite = null;
- _smallItemSpriteView.Visible = false;
- _smallItemSpriteView.Sprite = null;
- }
- else
- {
- switch (Action.ItemIconStyle)
- {
- case ItemActionIconStyle.BigItem:
- _bigItemSpriteView.Visible = true;
- _bigItemSpriteView.Sprite = sprite;
- _smallItemSpriteView.Visible = false;
- _smallItemSpriteView.Sprite = null;
- break;
- case ItemActionIconStyle.BigAction:
-
- _bigItemSpriteView.Visible = false;
- _bigItemSpriteView.Sprite = null;
- _smallItemSpriteView.Visible = true;
- _smallItemSpriteView.Sprite = sprite;
- break;
-
- case ItemActionIconStyle.NoItem:
-
- _bigItemSpriteView.Visible = false;
- _bigItemSpriteView.Sprite = null;
- _smallItemSpriteView.Visible = false;
- _smallItemSpriteView.Sprite = null;
- break;
- }
- }
}
protected override void ControlFocusExited()
@@ -193,15 +48,7 @@ namespace Content.Client.Actions.UI
private Control SupplyTooltip(Control? sender)
{
- var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Name));
- var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description));
-
- var tooltip = new ActionAlertTooltip(name, decr);
-
- if (Action.Enabled && (Action.Charges == null || Action.Charges != 0))
- tooltip.Cooldown = Action.Cooldown;
-
- return tooltip;
+ return new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires);
}
///
@@ -224,5 +71,6 @@ namespace Content.Client.Actions.UI
}
}
}
+
}
}
diff --git a/Content.Client/Actions/UI/ActionSlot.cs b/Content.Client/Actions/UI/ActionSlot.cs
index 635db14ba8..77f0ac4c07 100644
--- a/Content.Client/Actions/UI/ActionSlot.cs
+++ b/Content.Client/Actions/UI/ActionSlot.cs
@@ -1,14 +1,20 @@
-using System;
+using System;
using Content.Client.Cooldown;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
+using Content.Shared.Actions.Components;
+using Content.Shared.Actions.Prototypes;
+using Content.Shared.Inventory;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
+using Robust.Shared.GameObjects;
using Robust.Shared.Input;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -31,7 +37,48 @@ namespace Content.Client.Actions.UI
///
/// Current action in this slot.
///
- public ActionType? Action { get; private set; }
+ public BaseActionPrototype? Action { get; private set; }
+
+ ///
+ /// true if there is an action assigned to the slot
+ ///
+ public bool HasAssignment => Action != null;
+
+ private bool HasToggleSprite => Action != null && Action.IconOn != SpriteSpecifier.Invalid;
+
+ ///
+ /// Only applicable when an action is in this slot.
+ /// True if the action is currently shown as enabled, false if action disabled.
+ ///
+ public bool ActionEnabled { get; private set; }
+
+ ///
+ /// Is there an action in the slot that can currently be used?
+ /// Target-basedActions on cooldown can still be selected / deselected if they've been configured as such
+ ///
+ public bool CanUseAction => Action != null && ActionEnabled &&
+ (!IsOnCooldown || (Action.IsTargetAction && !Action.DeselectOnCooldown));
+
+ ///
+ /// Item the action is provided by, only valid if Action is an ItemActionPrototype. May be null
+ /// if the item action is not yet tied to an item.
+ ///
+ public EntityUid? Item { get; private set; }
+
+ ///
+ /// Whether the action in this slot should be shown as toggled on. Separate from Depressed.
+ ///
+ public bool ToggledOn
+ {
+ get => _toggledOn;
+ set
+ {
+ if (_toggledOn == value) return;
+ _toggledOn = value;
+ UpdateIcons();
+ DrawModeChanged();
+ }
+ }
///
/// 1-10 corresponding to the number label on the slot (10 is labeled as 0)
@@ -39,6 +86,25 @@ namespace Content.Client.Actions.UI
private byte SlotNumber => (byte) (SlotIndex + 1);
public byte SlotIndex { get; }
+ ///
+ /// Current cooldown displayed in this slot. Set to null to show no cooldown.
+ ///
+ public (TimeSpan Start, TimeSpan End)? Cooldown
+ {
+ get => _cooldown;
+ set
+ {
+ _cooldown = value;
+ if (SuppliedTooltip is ActionAlertTooltip actionAlertTooltip)
+ {
+ actionAlertTooltip.Cooldown = value;
+ }
+ }
+ }
+ private (TimeSpan Start, TimeSpan End)? _cooldown;
+
+ public bool IsOnCooldown => Cooldown.HasValue && _gameTiming.CurTime < Cooldown.Value.End;
+
private readonly IGameTiming _gameTiming;
private readonly RichTextLabel _number;
private readonly TextureRect _bigActionIcon;
@@ -48,6 +114,8 @@ namespace Content.Client.Actions.UI
private readonly CooldownGraphic _cooldownGraphic;
private readonly ActionsUI _actionsUI;
private readonly ActionMenu _actionMenu;
+ private readonly ClientActionsComponent _actionsComponent;
+ private bool _toggledOn;
// whether button is currently pressed down by mouse or keybind down.
private bool _depressed;
private bool _beingHovered;
@@ -56,8 +124,9 @@ namespace Content.Client.Actions.UI
/// Creates an action slot for the specified number
///
/// slot index this corresponds to, 0-9 (0 labeled as 1, 8, labeled "9", 9 labeled as "0".
- public ActionSlot(ActionsUI actionsUI, ActionMenu actionMenu, byte slotIndex)
+ public ActionSlot(ActionsUI actionsUI, ActionMenu actionMenu, ClientActionsComponent actionsComponent, byte slotIndex)
{
+ _actionsComponent = actionsComponent;
_actionsUI = actionsUI;
_actionMenu = actionMenu;
_gameTiming = IoCManager.Resolve();
@@ -101,8 +170,7 @@ namespace Content.Client.Actions.UI
{
HorizontalAlignment = HAlignment.Right,
VerticalAlignment = VAlignment.Bottom,
- Visible = false,
- OverrideDirection = Direction.South,
+ Visible = false
};
_cooldownGraphic = new CooldownGraphic {Progress = 0, Visible = false};
@@ -151,24 +219,24 @@ namespace Content.Client.Actions.UI
private Control? SupplyTooltip(Control sender)
{
- if (Action == null)
- return null;
+ return Action == null ? null :
+ new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires) {Cooldown = Cooldown};
+ }
- string? extra = null;
- if (Action.Charges != null)
+ ///
+ /// Action attempt for performing the action in the slot
+ ///
+ public IActionAttempt? ActionAttempt()
+ {
+ IActionAttempt? attempt = Action switch
{
- extra = Loc.GetString("ui-actionslot-charges", ("charges", Action.Charges));
- }
-
- var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Name));
- var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description));
-
- var tooltip = new ActionAlertTooltip(name, decr, extra);
-
- if (Action.Enabled && (Action.Charges == null || Action.Charges != 0))
- tooltip.Cooldown = Action.Cooldown;
-
- return tooltip;
+ ActionPrototype actionPrototype => new ActionAttempt(actionPrototype),
+ ItemActionPrototype itemActionPrototype =>
+ Item.HasValue && IoCManager.Resolve().TryGetComponent(Item, out var itemActions) ?
+ new ItemActionAttempt(itemActionPrototype, Item.Value, itemActions) : null,
+ _ => null
+ };
+ return attempt;
}
protected override void MouseEntered()
@@ -177,9 +245,9 @@ namespace Content.Client.Actions.UI
_beingHovered = true;
DrawModeChanged();
-
- if (Action?.Provider != null)
- _actionsUI.System.HighlightItemSlot(Action.Provider.Value);
+ if (Action is not ItemActionPrototype) return;
+ if (Item == null) return;
+ _actionsComponent.HighlightItemSlot(Item.Value);
}
protected override void MouseExited()
@@ -188,55 +256,32 @@ namespace Content.Client.Actions.UI
_beingHovered = false;
CancelPress();
DrawModeChanged();
- _actionsUI.System.StopHighlightingItemSlot();
+ _actionsComponent.StopHighlightingItemSlots();
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
{
base.KeyBindDown(args);
- if (Action == null)
+ if (args.Function == EngineKeyFunctions.UIRightClick)
{
- // No action for this slot. Maybe the user is trying to add a mapping action?
- _actionsUI.System.TryFillSlot(this, _actionsUI.SelectedHotbar, SlotIndex);
+ if (!_actionsUI.Locked && !_actionsUI.DragDropHelper.IsDragging && !_actionMenu.IsDragging)
+ {
+ _actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true);
+ _actionsUI.StopTargeting();
+ _actionsUI.UpdateUI();
+ }
return;
}
// only handle clicks, and can't do anything to this if no assignment
- if (args.Function == EngineKeyFunctions.UIClick)
- {
- // might turn into a drag or a full press if released
- Depress(true);
- _actionsUI.DragDropHelper.MouseDown(this);
- DrawModeChanged();
- return;
- }
-
- if (args.Function != EngineKeyFunctions.UIRightClick || _actionsUI.Locked)
+ if (args.Function != EngineKeyFunctions.UIClick || !HasAssignment)
return;
- if (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging)
- return;
-
- // user right clicked on an action slot, so we clear it.
- _actionsUI.System.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true);
-
- // If this was a temporary action, and it is no longer assigned to any slots, then we remove the action
- // altogether.
- if (Action.Temporary)
- {
- // Theres probably a better way to do this.....
- DebugTools.Assert(Action.ClientExclusive, "Temporary-actions must be client exclusive");
-
- if (!_actionsUI.System.Assignments.Assignments.TryGetValue(Action, out var index)
- || index.Count == 0)
- {
- _actionsUI.Component.Actions.Remove(Action);
- }
- }
-
- _actionsUI.StopTargeting();
- _actionsUI.UpdateUI();
+ // might turn into a drag or a full press if released
+ Depress(true);
+ _actionsUI.DragDropHelper.MouseDown(this);
+ DrawModeChanged();
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
@@ -254,21 +299,21 @@ namespace Content.Client.Actions.UI
{
// finish the drag, swap the 2 slots
var fromIdx = SlotIndex;
- var fromAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, fromIdx];
+ var fromAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, fromIdx];
var toIdx = targetSlot.SlotIndex;
- var toAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, toIdx];
+ var toAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, toIdx];
if (fromIdx == toIdx) return;
- if (fromAssignment == null) return;
+ if (!fromAssignment.HasValue) return;
- _actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment);
- if (toAssignment != null)
+ _actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment.Value);
+ if (toAssignment.HasValue)
{
- _actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment);
+ _actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment.Value);
}
else
{
- _actionsUI.System.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false);
+ _actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false);
}
_actionsUI.UpdateUI();
}
@@ -303,19 +348,66 @@ namespace Content.Client.Actions.UI
///
/// Press this button down. If it was depressed and now set to not depressed, will
- /// trigger the action.
+ /// trigger the action. Only has an effect if CanUseAction.
///
public void Depress(bool depress)
{
// action can still be toggled if it's allowed to stay selected
- if (Action == null || !Action.Enabled) return;
+ if (!CanUseAction) return;
+
if (_depressed && !depress)
{
// fire the action
- _actionsUI.System.OnSlotPressed(this);
+ // no left-click interaction with it on cooldown or revoked
+ _actionsComponent.AttemptAction(this);
}
_depressed = depress;
+ DrawModeChanged();
+ }
+
+ ///
+ /// Updates the action assigned to this slot.
+ ///
+ /// action to assign
+ /// whether action should initially appear enable or disabled
+ public void Assign(ActionPrototype action, bool actionEnabled)
+ {
+ // already assigned
+ if (Action != null && Action == action) return;
+
+ Action = action;
+ Item = default;
+ _depressed = false;
+ ToggledOn = false;
+ ActionEnabled = actionEnabled;
+ Cooldown = null;
+ HideTooltip();
+ UpdateIcons();
+ DrawModeChanged();
+ _number.SetMessage(SlotNumberLabel());
+ }
+
+ ///
+ /// Updates the item action assigned to this slot. The action will always be shown as disabled
+ /// until it is tied to a specific item.
+ ///
+ /// action to assign
+ public void Assign(ItemActionPrototype action)
+ {
+ // already assigned
+ if (Action != null && Action == action && Item == default) return;
+
+ Action = action;
+ Item = default;
+ _depressed = false;
+ ToggledOn = false;
+ ActionEnabled = false;
+ Cooldown = null;
+ HideTooltip();
+ UpdateIcons();
+ DrawModeChanged();
+ _number.SetMessage(SlotNumberLabel());
}
///
@@ -323,12 +415,18 @@ namespace Content.Client.Actions.UI
///
/// action to assign
/// item the action is provided by
- public void Assign(ActionType action)
+ /// whether action should initially appear enable or disabled
+ public void Assign(ItemActionPrototype action, EntityUid item, bool actionEnabled)
{
// already assigned
- if (Action != null && Action == action) return;
+ if (Action != null && Action == action && Item == item) return;
Action = action;
+ Item = item;
+ _depressed = false;
+ ToggledOn = false;
+ ActionEnabled = false;
+ Cooldown = null;
HideTooltip();
UpdateIcons();
DrawModeChanged();
@@ -340,9 +438,12 @@ namespace Content.Client.Actions.UI
///
public void Clear()
{
- if (Action == null) return;
+ if (!HasAssignment) return;
Action = null;
+ Item = default;
+ ToggledOn = false;
_depressed = false;
+ Cooldown = null;
HideTooltip();
UpdateIcons();
DrawModeChanged();
@@ -352,8 +453,12 @@ namespace Content.Client.Actions.UI
///
/// Display the action in this slot (if there is one) as enabled
///
- public void Enable()
+ public void EnableAction()
{
+ if (ActionEnabled || !HasAssignment) return;
+
+ ActionEnabled = true;
+ _depressed = false;
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
@@ -362,8 +467,11 @@ namespace Content.Client.Actions.UI
/// Display the action in this slot (if there is one) as disabled.
/// The slot is still clickable.
///
- public void Disable()
+ public void DisableAction()
{
+ if (!ActionEnabled || !HasAssignment) return;
+
+ ActionEnabled = false;
_depressed = false;
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
@@ -373,56 +481,70 @@ namespace Content.Client.Actions.UI
{
if (SlotNumber > 10) return FormattedMessage.FromMarkup("");
var number = Loc.GetString(SlotNumber == 10 ? "0" : SlotNumber.ToString());
- var color = (Action == null || Action.Enabled) ? EnabledColor : DisabledColor;
+ var color = (ActionEnabled || !HasAssignment) ? EnabledColor : DisabledColor;
return FormattedMessage.FromMarkup("[color=" + color + "]" + number + "[/color]");
}
- public void UpdateIcons()
+ private void UpdateIcons()
{
- UpdateItemIcon();
-
- if (Action == null)
+ if (!HasAssignment)
{
SetActionIcon(null);
+ SetItemIcon(null);
return;
}
- if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null)
+ if (HasToggleSprite && ToggledOn && Action != null)
+ {
SetActionIcon(Action.IconOn.Frame0());
+ }
+ else if (Action != null)
+ {
+ SetActionIcon(Action.Icon.Frame0());
+ }
+
+ if (Item != default)
+ {
+ SetItemIcon(IoCManager.Resolve().TryGetComponent(Item, out var spriteComponent) ? spriteComponent : null);
+ }
else
- SetActionIcon(Action.Icon?.Frame0());
+ {
+ SetItemIcon(null);
+ }
}
private void SetActionIcon(Texture? texture)
{
- if (texture == null || Action == null)
+ if (texture == null || !HasAssignment)
{
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
}
- else if (Action.Provider != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem)
- {
- _smallActionIcon.Texture = texture;
- _smallActionIcon.Modulate = Action.IconColor;
- _smallActionIcon.Visible = true;
- _bigActionIcon.Texture = null;
- _bigActionIcon.Visible = false;
- }
else
{
- _bigActionIcon.Texture = texture;
- _bigActionIcon.Modulate = Action.IconColor;
- _bigActionIcon.Visible = true;
- _smallActionIcon.Texture = null;
- _smallActionIcon.Visible = false;
+ if (Action is ItemActionPrototype {IconStyle: ItemActionIconStyle.BigItem})
+ {
+ _bigActionIcon.Texture = null;
+ _bigActionIcon.Visible = false;
+ _smallActionIcon.Texture = texture;
+ _smallActionIcon.Visible = true;
+ }
+ else
+ {
+ _bigActionIcon.Texture = texture;
+ _bigActionIcon.Visible = true;
+ _smallActionIcon.Texture = null;
+ _smallActionIcon.Visible = false;
+ }
+
}
}
- private void UpdateItemIcon()
+ private void SetItemIcon(ISpriteComponent? sprite)
{
- if (Action?.Provider == null || !IoCManager.Resolve().TryGetComponent(Action.Provider.Value, out SpriteComponent sprite))
+ if (sprite == null || !HasAssignment)
{
_bigItemSpriteView.Visible = false;
_bigItemSpriteView.Sprite = null;
@@ -431,48 +553,70 @@ namespace Content.Client.Actions.UI
}
else
{
- switch (Action.ItemIconStyle)
+ if (Action is ItemActionPrototype actionPrototype)
{
- case ItemActionIconStyle.BigItem:
- _bigItemSpriteView.Visible = true;
- _bigItemSpriteView.Sprite = sprite;
- _smallItemSpriteView.Visible = false;
- _smallItemSpriteView.Sprite = null;
- break;
- case ItemActionIconStyle.BigAction:
+ switch (actionPrototype.IconStyle)
+ {
+ case ItemActionIconStyle.BigItem:
+ {
+ _bigItemSpriteView.Visible = true;
+ _bigItemSpriteView.Sprite = sprite;
+ _smallItemSpriteView.Visible = false;
+ _smallItemSpriteView.Sprite = null;
+ break;
+ }
+ case ItemActionIconStyle.BigAction:
+ {
+ _bigItemSpriteView.Visible = false;
+ _bigItemSpriteView.Sprite = null;
+ _smallItemSpriteView.Visible = true;
+ _smallItemSpriteView.Sprite = sprite;
+ break;
+ }
+ case ItemActionIconStyle.NoItem:
+ {
+ _bigItemSpriteView.Visible = false;
+ _bigItemSpriteView.Sprite = null;
+ _smallItemSpriteView.Visible = false;
+ _smallItemSpriteView.Sprite = null;
+ break;
+ }
+ }
- _bigItemSpriteView.Visible = false;
- _bigItemSpriteView.Sprite = null;
- _smallItemSpriteView.Visible = true;
- _smallItemSpriteView.Sprite = sprite;
- break;
-
- case ItemActionIconStyle.NoItem:
-
- _bigItemSpriteView.Visible = false;
- _bigItemSpriteView.Sprite = null;
- _smallItemSpriteView.Visible = false;
- _smallItemSpriteView.Sprite = null;
- break;
}
+ else
+ {
+ _bigItemSpriteView.Visible = false;
+ _bigItemSpriteView.Sprite = null;
+ _smallItemSpriteView.Visible = false;
+ _smallItemSpriteView.Sprite = null;
+ }
+
}
}
- public void DrawModeChanged()
+
+ private void DrawModeChanged()
{
+
+ // show a hover only if the action is usable or another action is being dragged on top of this
+ if (_beingHovered)
+ {
+ if (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging ||
+ (HasAssignment && ActionEnabled && !IsOnCooldown))
+ {
+ SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
+ return;
+ }
+ }
+
// always show the normal empty button style if no action in this slot
- if (Action == null)
+ if (!HasAssignment)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
return;
}
- // show a hover only if the action is usable or another action is being dragged on top of this
- if (_beingHovered && (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging || Action.Enabled))
- {
- SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
- }
-
// it's only depress-able if it's usable, so if we're depressed
// show the depressed style
if (_depressed)
@@ -481,50 +625,47 @@ namespace Content.Client.Actions.UI
return;
}
+
// if it's toggled on, always show the toggled on style (currently same as depressed style)
- if (Action.Toggled || _actionsUI.SelectingTargetFor == this)
+ if (ToggledOn)
{
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
- SetOnlyStylePseudoClass(Action.IconOn != null ? ContainerButton.StylePseudoClassNormal :
+ SetOnlyStylePseudoClass(HasToggleSprite ? ContainerButton.StylePseudoClassNormal :
ContainerButton.StylePseudoClassPressed);
return;
}
- if (!Action.Enabled)
+
+ if (!ActionEnabled)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
return;
}
+
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
}
+
+
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
- if (Action == null || Action.Cooldown == null || !Action.Enabled)
+ if (!Cooldown.HasValue)
{
_cooldownGraphic.Visible = false;
_cooldownGraphic.Progress = 0;
return;
}
- var cooldown = Action.Cooldown.Value;
- var duration = cooldown.End - cooldown.Start;
+ var duration = Cooldown.Value.End - Cooldown.Value.Start;
var curTime = _gameTiming.CurTime;
var length = duration.TotalSeconds;
- var progress = (curTime - cooldown.Start).TotalSeconds / length;
- var ratio = (progress <= 1 ? (1 - progress) : (curTime - cooldown.End).TotalSeconds * -5);
+ var progress = (curTime - Cooldown.Value.Start).TotalSeconds / length;
+ var ratio = (progress <= 1 ? (1 - progress) : (curTime - Cooldown.Value.End).TotalSeconds * -5);
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
- if (ratio > -1f)
- _cooldownGraphic.Visible = true;
- else
- {
- _cooldownGraphic.Visible = false;
- Action.Cooldown = null;
- DrawModeChanged();
- }
+ _cooldownGraphic.Visible = ratio > -1f;
}
}
}
diff --git a/Content.Client/Actions/UI/ActionsUI.cs b/Content.Client/Actions/UI/ActionsUI.cs
index c9ea1649fa..260a6656d0 100644
--- a/Content.Client/Actions/UI/ActionsUI.cs
+++ b/Content.Client/Actions/UI/ActionsUI.cs
@@ -1,18 +1,23 @@
+using System.Collections.Generic;
+using Content.Client.Actions.Assignments;
using Content.Client.DragDrop;
using Content.Client.HUD;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
+using Content.Shared.Actions.Prototypes;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
+using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Log;
using Robust.Shared.Timing;
-using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Actions.UI
@@ -22,16 +27,13 @@ namespace Content.Client.Actions.UI
///
public sealed class ActionsUI : Container
{
- private const float DragDeadZone = 10f;
private const float CustomTooltipDelay = 0.4f;
- internal readonly ActionsSystem System;
+ private readonly ClientActionsComponent _actionsComponent;
+ private readonly ActionManager _actionManager;
+ private readonly IEntityManager _entityManager;
+ private readonly IGameTiming _gameTiming;
private readonly IGameHud _gameHud;
- ///
- /// The action component of the currently attached entity.
- ///
- public readonly ActionsComponent Component;
-
private readonly ActionSlot[] _slots;
private readonly GridContainer _slotContainer;
@@ -73,13 +75,15 @@ namespace Content.Client.Actions.UI
///
public IEnumerable Slots => _slots;
- public ActionsUI(ActionsSystem system, ActionsComponent component)
+ public ActionsUI(ClientActionsComponent actionsComponent)
{
SetValue(LayoutContainer.DebugProperty, true);
- System = system;
- Component = component;
+ _actionsComponent = actionsComponent;
+ _actionManager = IoCManager.Resolve();
+ _entityManager = IoCManager.Resolve();
+ _gameTiming = IoCManager.Resolve();
_gameHud = IoCManager.Resolve();
- _menu = new ActionMenu(this);
+ _menu = new ActionMenu(_actionsComponent, this);
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.End);
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Constrain);
@@ -192,7 +196,7 @@ namespace Content.Client.Actions.UI
_loadoutContainer.AddChild(nextHotbarIcon);
_loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 });
- _slots = new ActionSlot[ActionsSystem.Slots];
+ _slots = new ActionSlot[ClientActionsComponent.Slots];
_dragShadow = new TextureRect
{
@@ -203,14 +207,14 @@ namespace Content.Client.Actions.UI
};
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
- for (byte i = 0; i < ActionsSystem.Slots; i++)
+ for (byte i = 0; i < ClientActionsComponent.Slots; i++)
{
- var slot = new ActionSlot(this, _menu, i);
+ var slot = new ActionSlot(this, _menu, actionsComponent, i);
_slotContainer.AddChild(slot);
_slots[i] = slot;
}
- DragDropHelper = new DragDropHelper(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag, DragDeadZone);
+ DragDropHelper = new DragDropHelper(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
MinSize = (10, 400);
}
@@ -281,47 +285,157 @@ namespace Content.Client.Actions.UI
foreach (var actionSlot in Slots)
{
- var action = System.Assignments[SelectedHotbar, actionSlot.SlotIndex];
-
- if (action == null)
+ var assignedActionType = _actionsComponent.Assignments[SelectedHotbar, actionSlot.SlotIndex];
+ if (!assignedActionType.HasValue)
{
- if (SelectingTargetFor == actionSlot)
- StopTargeting(true);
actionSlot.Clear();
continue;
}
- // This shouldn't happen, but could possible occur if theres some bug. Really the fact that the
- // assignments aren't stored directly on the components or actions is just bad design and needs fixing.
- DebugTools.Assert(Component.Actions.TryGetValue(action, out var compAction) && compAction == action);
-
- UpdateActionSlot(action, actionSlot);
+ if (assignedActionType.Value.TryGetAction(out var actionType))
+ {
+ UpdateActionSlot(actionType, actionSlot, assignedActionType);
+ }
+ else if (assignedActionType.Value.TryGetItemActionWithoutItem(out var itemlessActionType))
+ {
+ UpdateActionSlot(itemlessActionType, actionSlot, assignedActionType);
+ }
+ else if (assignedActionType.Value.TryGetItemActionWithItem(out var itemActionType, out var item))
+ {
+ UpdateActionSlot(item, itemActionType, actionSlot, assignedActionType);
+ }
+ else
+ {
+ Logger.ErrorS("action", "unexpected Assignment type {0}",
+ assignedActionType.Value.Assignment);
+ actionSlot.Clear();
+ }
}
}
- private void UpdateActionSlot(ActionType action, ActionSlot actionSlot)
+ private void UpdateActionSlot(ActionType actionType, ActionSlot actionSlot, ActionAssignment? assignedActionType)
{
- actionSlot.Assign(action);
-
- if (!action.Enabled)
+ if (_actionManager.TryGet(actionType, out var action))
{
+ actionSlot.Assign(action, true);
+ }
+ else
+ {
+ Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
+ actionSlot.Clear();
+ return;
+ }
+
+ if (!_actionsComponent.TryGetActionState(actionType, out var actionState) || !actionState.Enabled)
+ {
+ // action is currently disabled
+
// just revoked an action we were trying to target with, stop targeting
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action)
{
StopTargeting();
}
- actionSlot.Disable();
+ actionSlot.DisableAction();
+ actionSlot.Cooldown = null;
}
else
{
- actionSlot.Enable();
+ // action is currently granted
+ actionSlot.EnableAction();
+ actionSlot.Cooldown = actionState.Cooldown;
+
+ // if we are targeting for this action and it's now on cooldown, stop targeting if we're supposed to
+ if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
+ actionState.IsOnCooldown(_gameTiming) && action.DeselectOnCooldown)
+ {
+ StopTargeting();
+ }
}
- actionSlot.UpdateIcons();
- actionSlot.DrawModeChanged();
+ // check if we need to toggle it
+ if (action.BehaviorType == BehaviorType.Toggle)
+ {
+ actionSlot.ToggledOn = actionState.ToggledOn;
+ }
}
+ private void UpdateActionSlot(ItemActionType itemlessActionType, ActionSlot actionSlot,
+ ActionAssignment? assignedActionType)
+ {
+ if (_actionManager.TryGet(itemlessActionType, out var action))
+ {
+ actionSlot.Assign(action);
+ }
+ else
+ {
+ Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
+ actionSlot.Clear();
+ }
+ actionSlot.Cooldown = null;
+ }
+
+ private void UpdateActionSlot(EntityUid item, ItemActionType itemActionType, ActionSlot actionSlot,
+ ActionAssignment? assignedActionType)
+ {
+ if (!_entityManager.EntityExists(item)) return;
+ if (_actionManager.TryGet(itemActionType, out var action))
+ {
+ actionSlot.Assign(action, item, true);
+ }
+ else
+ {
+ Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
+ actionSlot.Clear();
+ return;
+ }
+
+ if (!_actionsComponent.TryGetItemActionState(itemActionType, item, out var actionState))
+ {
+ // action is no longer tied to an item, this should never happen as we
+ // check this at the start of this method. But just to be safe
+ // we will restore our assignment here to the correct state
+ Logger.ErrorS("action", "coding error, expected actionType {0} to have" +
+ " a state but it didn't", assignedActionType);
+ _actionsComponent.Assignments.AssignSlot(SelectedHotbar, actionSlot.SlotIndex,
+ ActionAssignment.For(itemActionType));
+ actionSlot.Assign(action);
+ return;
+ }
+
+ if (!actionState.Enabled)
+ {
+ // just disabled an action we were trying to target with, stop targeting
+ if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action)
+ {
+ StopTargeting();
+ }
+
+ actionSlot.DisableAction();
+ }
+ else
+ {
+ // action is currently granted
+ actionSlot.EnableAction();
+
+ // if we are targeting with an action now on cooldown, stop targeting if we should
+ if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
+ SelectingTargetFor.Item == item &&
+ actionState.IsOnCooldown(_gameTiming) && action.DeselectOnCooldown)
+ {
+ StopTargeting();
+ }
+ }
+ actionSlot.Cooldown = actionState.Cooldown;
+
+ // check if we need to toggle it
+ if (action.BehaviorType == BehaviorType.Toggle)
+ {
+ actionSlot.ToggledOn = actionState.ToggledOn;
+ }
+ }
+
+
private void OnHotbarPaginate(GUIBoundKeyEventArgs args)
{
// rather than clicking the arrows themselves, the user can click the hbox so it's more
@@ -331,11 +445,11 @@ namespace Content.Client.Actions.UI
var rightness = args.RelativePosition.X / _loadoutContainer.Width;
if (rightness > 0.5)
{
- ChangeHotbar((byte) ((SelectedHotbar + 1) % ActionsSystem.Hotbars));
+ ChangeHotbar((byte) ((SelectedHotbar + 1) % ClientActionsComponent.Hotbars));
}
else
{
- var newBar = SelectedHotbar == 0 ? ActionsSystem.Hotbars - 1 : SelectedHotbar - 1;
+ var newBar = SelectedHotbar == 0 ? ClientActionsComponent.Hotbars - 1 : SelectedHotbar - 1;
ChangeHotbar((byte) newBar);
}
}
@@ -369,35 +483,29 @@ namespace Content.Client.Actions.UI
///
private void StartTargeting(ActionSlot actionSlot)
{
- if (actionSlot.Action == null)
- return;
-
// If we were targeting something else we should stop
StopTargeting();
SelectingTargetFor = actionSlot;
- if (actionSlot.Action is TargetedAction targetAction)
- System.StartTargeting(targetAction);
-
- UpdateUI();
+ // show it as toggled on to indicate we are currently selecting a target for it
+ if (!actionSlot.ToggledOn)
+ {
+ actionSlot.ToggledOn = true;
+ }
}
///
/// Switch out of targeting mode if currently selecting target for an action
///
- public void StopTargeting(bool updating = false)
+ public void StopTargeting()
{
- if (SelectingTargetFor == null)
- return;
-
+ if (SelectingTargetFor == null) return;
+ if (SelectingTargetFor.ToggledOn)
+ {
+ SelectingTargetFor.ToggledOn = false;
+ }
SelectingTargetFor = null;
- System.StopTargeting();
-
- // Sometimes targeting gets stopped mid-UI update.
- // in that case, don't need to do a nested UI refresh.
- if (!updating)
- UpdateUI();
}
private void OnToggleActionsMenu(BaseButton.ButtonEventArgs args)
@@ -405,6 +513,7 @@ namespace Content.Client.Actions.UI
ToggleActionsMenu();
}
+
private void OnToggleActionsMenuTopButton(bool open)
{
if (open == _menu.IsOpen) return;
@@ -434,7 +543,7 @@ namespace Content.Client.Actions.UI
// only initiate the drag if the slot has an action in it
if (Locked || DragDropHelper.Dragged?.Action == null) return false;
- _dragShadow.Texture = DragDropHelper.Dragged.Action.Icon?.Frame0();
+ _dragShadow.Texture = DragDropHelper.Dragged.Action.Icon.Frame0();
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled.Position - (32, 32));
DragDropHelper.Dragged.CancelPress();
return true;
@@ -465,7 +574,6 @@ namespace Content.Client.Actions.UI
{
var actionSlot = _slots[slot];
actionSlot.Depress(args.State == BoundKeyState.Down);
- actionSlot.DrawModeChanged();
}
///
diff --git a/Content.Client/Clothing/MagbootsSystem.cs b/Content.Client/Clothing/MagbootsSystem.cs
index 46905601c5..d16b70fc4b 100644
--- a/Content.Client/Clothing/MagbootsSystem.cs
+++ b/Content.Client/Clothing/MagbootsSystem.cs
@@ -1,9 +1,10 @@
using Content.Shared.Clothing;
using Content.Shared.Movement.EntitySystems;
+using Robust.Shared.GameObjects;
namespace Content.Client.Clothing
{
- public sealed class MagbootsSystem : SharedMagbootsSystem
+ public sealed class MagbootsSystem : EntitySystem
{
public override void Initialize()
{
diff --git a/Content.Client/DragDrop/DragDropSystem.cs b/Content.Client/DragDrop/DragDropSystem.cs
index f362f2a778..bf7f9b8911 100644
--- a/Content.Client/DragDrop/DragDropSystem.cs
+++ b/Content.Client/DragDrop/DragDropSystem.cs
@@ -204,7 +204,7 @@ namespace Content.Client.DragDrop
}
HighlightTargets();
- _outline.SetEnabled(false);
+ _outline.Enabled = false;
// drag initiated
return true;
@@ -257,7 +257,7 @@ namespace Content.Client.DragDrop
EntityManager.DeleteEntity(_dragShadow);
}
- _outline.SetEnabled(true);
+ _outline.Enabled = true;
_dragShadow = default;
_draggables.Clear();
_dragger = default;
@@ -353,7 +353,6 @@ namespace Content.Client.DragDrop
return false;
}
- // TODO make this just use TargetOutlineSystem
private void HighlightTargets()
{
if (_dragDropHelper.Dragged == default || Deleted(_dragDropHelper.Dragged) ||
@@ -371,7 +370,7 @@ namespace Content.Client.DragDrop
RemoveHighlights();
// find possible targets on screen even if not reachable
- // TODO: Duplicated in SpriteSystem and TargetOutlineSystem. Should probably be cached somewhere for a frame?
+ // TODO: Duplicated in SpriteSystem
var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition).Position;
var bounds = new Box2(mousePos - 1.5f, mousePos + 1.5f);
var pvsEntities = IoCManager.Resolve().GetEntitiesIntersecting(_eyeManager.CurrentMap, bounds, LookupFlags.Approximate | LookupFlags.IncludeAnchored);
diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index 9feac603db..941d319df0 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -101,9 +101,6 @@ namespace Content.Client.Entry
prototypes.RegisterIgnore("salvageMap");
prototypes.RegisterIgnore("gamePreset");
prototypes.RegisterIgnore("gameRule");
- prototypes.RegisterIgnore("worldSpell");
- prototypes.RegisterIgnore("entitySpell");
- prototypes.RegisterIgnore("instantSpell");
ClientContentIoC.Register();
@@ -192,6 +189,7 @@ namespace Content.Client.Entry
IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
+ IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index f1eddc53cd..8f10608e6f 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -16,7 +16,6 @@ namespace Content.Client.Entry
"Temperature",
"AtmosExposed",
"Explosive",
- "Vocal",
"OnUseTimerTrigger",
"WarpPoint",
"EmitSoundOnUse",
diff --git a/Content.Client/Hands/ShowHandItemOverlay.cs b/Content.Client/Hands/ShowHandItemOverlay.cs
index 01bae5802d..b6a6abcc07 100644
--- a/Content.Client/Hands/ShowHandItemOverlay.cs
+++ b/Content.Client/Hands/ShowHandItemOverlay.cs
@@ -5,6 +5,9 @@ using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Maths;
namespace Content.Client.Hands
{
@@ -13,15 +16,11 @@ namespace Content.Client.Hands
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IClyde _clyde = default!;
- [Dependency] private readonly IEntityManager _entMan = default!;
private readonly IRenderTexture _renderBackbuffer;
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
- public Texture? IconOverride;
- public EntityUid? EntityOverride;
-
public ShowHandItemOverlay()
{
IoCManager.InjectDependencies(this);
@@ -44,25 +43,15 @@ namespace Content.Client.Hands
protected override void Draw(in OverlayDrawArgs args)
{
- if (!_cfg.GetCVar(CCVars.HudHeldItemShow))
+ var sys = EntitySystem.Get();
+ var handEntity = sys.GetActiveHandEntity();
+
+ if (handEntity == null || !_cfg.GetCVar(CCVars.HudHeldItemShow) || !IoCManager.Resolve().HasComponent(handEntity))
return;
var screen = args.ScreenHandle;
- var offset = _cfg.GetCVar(CCVars.HudHeldItemOffset);
- var mousePos = _inputManager.MouseScreenPosition.Position;
-
- if (IconOverride != null)
- {
- screen.DrawTexture(IconOverride, mousePos - IconOverride.Size / 2 + offset, Color.White.WithAlpha(0.75f));
- return;
- }
-
- var handEntity = EntityOverride ?? EntitySystem.Get().GetActiveHandEntity();
-
- if (handEntity == null || !_entMan.HasComponent(handEntity))
- return;
-
var halfSize = _renderBackbuffer.Size / 2;
+
var uiScale = (args.ViewportControl as Control)?.UIScale ?? 1f;
screen.RenderInRenderTarget(_renderBackbuffer, () =>
@@ -70,7 +59,11 @@ namespace Content.Client.Hands
screen.DrawEntity(handEntity.Value, halfSize, new Vector2(1f, 1f) * uiScale, Direction.South);
}, Color.Transparent);
+ var offset = _cfg.GetCVar(CCVars.HudHeldItemOffset);
+
+ var mousePos = _inputManager.MouseScreenPosition.Position;
screen.DrawTexture(_renderBackbuffer.Texture, mousePos - halfSize + offset, Color.White.WithAlpha(0.75f));
+ // screen.DrawRect(UIBox2.FromDimensions((offset, offset) + mousePos, (32, 32)), Color.Red);
}
}
}
diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs
index 71751a6822..dbb02a7974 100644
--- a/Content.Client/IoC/ClientContentIoC.cs
+++ b/Content.Client/IoC/ClientContentIoC.cs
@@ -36,6 +36,7 @@ namespace Content.Client.IoC
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
+ IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
diff --git a/Content.Client/Outline/InteractionOutlineSystem.cs b/Content.Client/Outline/InteractionOutlineSystem.cs
index de5751ff62..a9847377b2 100644
--- a/Content.Client/Outline/InteractionOutlineSystem.cs
+++ b/Content.Client/Outline/InteractionOutlineSystem.cs
@@ -24,74 +24,14 @@ public sealed class InteractionOutlineSystem : EntitySystem
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
- ///
- /// Whether to currently draw the outline. The outline may be temporarily disabled by other systems
- ///
- private bool _enabled = true;
-
- ///
- /// Whether to draw the outline at all. Overrides .
- ///
- private bool _cvarEnabled = true;
+ public bool Enabled = true;
private EntityUid? _lastHoveredEntity;
- public override void Initialize()
- {
- base.Initialize();
-
- _configManager.OnValueChanged(CCVars.OutlineEnabled, SetCvarEnabled);
- }
-
- public override void Shutdown()
- {
- base.Shutdown();
-
- _configManager.UnsubValueChanged(CCVars.OutlineEnabled, SetCvarEnabled);
- }
-
- public void SetCvarEnabled(bool cvarEnabled)
- {
- _cvarEnabled = cvarEnabled;
-
- // clear last hover if required:
-
- if (_cvarEnabled)
- return;
-
- if (_lastHoveredEntity == null || Deleted(_lastHoveredEntity))
- return;
-
- if (TryComp(_lastHoveredEntity, out InteractionOutlineComponent? outline))
- outline.OnMouseLeave();
- }
-
- public void SetEnabled(bool enabled)
- {
- if (enabled == _enabled)
- return;
-
- _enabled = enabled;
-
- // clear last hover if required:
-
- if (enabled)
- return;
-
- if (_lastHoveredEntity == null || Deleted(_lastHoveredEntity))
- return;
-
- if (TryComp(_lastHoveredEntity, out InteractionOutlineComponent? outline))
- outline.OnMouseLeave();
- }
-
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
- if (!_enabled || !_cvarEnabled)
- return;
-
// If there is no local player, there is no session, and therefore nothing to do here.
var localPlayer = _playerManager.LocalPlayer;
if (localPlayer == null)
@@ -141,6 +81,16 @@ public sealed class InteractionOutlineSystem : EntitySystem
InteractionOutlineComponent? outline;
+ if (!Enabled || !_configManager.GetCVar(CCVars.OutlineEnabled))
+ {
+ if (entityToClick != null && TryComp(entityToClick, out outline))
+ {
+ outline.OnMouseLeave(); //Prevent outline remains from persisting post command.
+ }
+
+ return;
+ }
+
if (entityToClick == _lastHoveredEntity)
{
if (entityToClick != null && TryComp(entityToClick, out outline))
diff --git a/Content.Client/Outline/TargetOutlineSystem.cs b/Content.Client/Outline/TargetOutlineSystem.cs
deleted file mode 100644
index 98ef20c0de..0000000000
--- a/Content.Client/Outline/TargetOutlineSystem.cs
+++ /dev/null
@@ -1,179 +0,0 @@
-using Content.Shared.Interaction;
-using Content.Shared.Whitelist;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.Input;
-using Robust.Client.Player;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.Outline;
-
-///
-/// System used to indicate whether an entity is a valid target based on some criteria.
-///
-public sealed class TargetOutlineSystem : EntitySystem
-{
- [Dependency] private readonly IEyeManager _eyeManager = default!;
- [Dependency] private readonly IEntityLookup _lookup = default!;
- [Dependency] private readonly IInputManager _inputManager = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
-
- private bool _enabled = false;
-
- ///
- /// Whitelist that the target must satisfy.
- ///
- public EntityWhitelist? Whitelist = null;
-
- ///
- /// Predicate the target must satisfy.
- ///
- public Func? Predicate = null;
-
- ///
- /// Event to raise as targets to check whether they are valid.
- ///
- ///
- /// This event will be uncanceled and re-used.
- ///
- public CancellableEntityEventArgs? ValidationEvent = null;
-
- ///
- /// Minimum range for a target to be valid.
- ///
- ///
- /// If a target is further than this distance, they will still be highlighted in a different color.
- ///
- public float Range = -1;
-
- ///
- /// Whether to check if the player is unobstructed to the target;
- ///
- public bool CheckObstruction = true;
-
- ///
- /// The size of the box around the mouse to use when looking for valid targets.
- ///
- public float LookupSize = 2;
-
- private const string ShaderTargetValid = "SelectionOutlineInrange";
- private const string ShaderTargetInvalid = "SelectionOutline";
- private ShaderInstance? _shaderTargetValid;
- private ShaderInstance? _shaderTargetInvalid;
-
- private readonly HashSet _highlightedSprites = new();
-
- public override void Initialize()
- {
- base.Initialize();
-
- _shaderTargetValid = _prototypeManager.Index(ShaderTargetValid).Instance();
- _shaderTargetInvalid = _prototypeManager.Index(ShaderTargetInvalid).Instance();
- }
-
- public void Disable()
- {
- if (_enabled == false)
- return;
-
- _enabled = false;
- RemoveHighlights();
- }
-
- public void Enable(float range, bool checkObstructions, Func? predicate, EntityWhitelist? whitelist, CancellableEntityEventArgs? validationEvent)
- {
- Range = range;
- CheckObstruction = checkObstructions;
- Predicate = predicate;
- Whitelist = whitelist;
- ValidationEvent = validationEvent;
-
- _enabled = Predicate != null || Whitelist != null || ValidationEvent != null;
- }
-
- public override void Update(float frameTime)
- {
- base.FrameUpdate(frameTime);
-
- if (!_enabled)
- return;
-
- HighlightTargets();
- }
-
- private void HighlightTargets()
- {
- if (_playerManager.LocalPlayer?.ControlledEntity is not { Valid: true } player)
- return;
-
- // remove current highlights
- RemoveHighlights();
-
- // find possible targets on screen
- // TODO: Duplicated in SpriteSystem and DragDropSystem. Should probably be cached somewhere for a frame?
- var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition).Position;
- var bounds = new Box2(mousePos - LookupSize, mousePos + LookupSize);
- var pvsEntities = _lookup.GetEntitiesIntersecting(_eyeManager.CurrentMap, bounds, LookupFlags.Approximate | LookupFlags.IncludeAnchored);
-
- foreach (var entity in pvsEntities)
- {
- if (!TryComp(entity, out SpriteComponent? sprite) || !sprite.Visible)
- continue;
-
- // Check the predicate
- var valid = Predicate?.Invoke(entity) ?? true;
-
- // check the entity whitelist
- if (valid && Whitelist != null)
- valid = Whitelist.IsValid(entity);
-
- // and check the cancellable event
- if (valid && ValidationEvent != null)
- {
- ValidationEvent.Uncancel();
- RaiseLocalEvent(entity, (object) ValidationEvent, broadcast: false);
- valid = !ValidationEvent.Cancelled;
- }
-
- if (!valid)
- {
- // was this previously valid?
- if (_highlightedSprites.Remove(sprite))
- {
- sprite.PostShader = null;
- sprite.RenderOrder = 0;
- }
-
- continue;
- }
-
- // Range check
- if (CheckObstruction)
- valid = _interactionSystem.InRangeUnobstructed(player, entity, Range);
- else if (Range >= 0)
- {
- var origin = Transform(player).WorldPosition;
- var target = Transform(entity).WorldPosition;
- valid = (origin - target).LengthSquared <= Range;
- }
-
- // highlight depending on whether its in or out of range
- sprite.PostShader = valid ? _shaderTargetValid : _shaderTargetInvalid;
- sprite.RenderOrder = EntityManager.CurrentTick.Value;
- _highlightedSprites.Add(sprite);
- }
- }
-
- private void RemoveHighlights()
- {
- foreach (var sprite in _highlightedSprites)
- {
- sprite.PostShader = null;
- sprite.RenderOrder = 0;
- }
-
- _highlightedSprites.Clear();
- }
-}
diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs
new file mode 100644
index 0000000000..34801d65a9
--- /dev/null
+++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs
@@ -0,0 +1,389 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Content.Client.Actions;
+using Content.Client.Actions.UI;
+using Content.Client.Items.Components;
+using Content.Server.Actions;
+using Content.Server.Hands.Components;
+using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
+using Content.Shared.Actions.Prototypes;
+using Content.Shared.Cooldown;
+using Content.Shared.Item;
+using NUnit.Framework;
+using Robust.Client.UserInterface;
+using Robust.Server.Player;
+using Robust.Shared;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
+{
+ [TestFixture]
+ [TestOf(typeof(SharedActionsComponent))]
+ [TestOf(typeof(ClientActionsComponent))]
+ [TestOf(typeof(ServerActionsComponent))]
+ [TestOf(typeof(ItemActionsComponent))]
+ public sealed class ActionsComponentTests : ContentIntegrationTest
+ {
+ const string Prototypes = @"
+- type: entity
+ name: flashlight
+ parent: BaseItem
+ id: TestFlashlight
+ components:
+ - type: HandheldLight
+ - type: ItemActions
+ actions:
+ - actionType: ToggleLight
+ - type: PowerCellSlot
+ - type: Sprite
+ sprite: Objects/Tools/flashlight.rsi
+ layers:
+ - state: flashlight
+ - state: flashlight-overlay
+ shader: unshaded
+ visible: false
+ - type: Item
+ sprite: Objects/Tools/flashlight.rsi
+ HeldPrefix: off
+ - type: PointLight
+ enabled: false
+ radius: 3
+ - type: Appearance
+ visuals:
+ - type: FlashLightVisualizer
+";
+
+ [Test]
+ public async Task GrantsAndRevokesActionsTest()
+ {
+ var (client, server) = await StartConnectedServerClientPair();
+
+ await server.WaitIdleAsync();
+ await client.WaitIdleAsync();
+
+ var cEntities = client.ResolveDependency();
+
+ var sEntities = server.ResolveDependency();
+ var serverPlayerManager = server.ResolveDependency();
+ var innateActions = new List();
+
+ await server.WaitAssertion(() =>
+ {
+ var playerEnt = serverPlayerManager.Sessions.Single().AttachedEntity.GetValueOrDefault();
+ Assert.That(playerEnt, Is.Not.EqualTo(default));
+ var actionsComponent = sEntities.GetComponent(playerEnt);
+
+ // player should begin with their innate actions granted
+ innateActions.AddRange(actionsComponent.InnateActions);
+ foreach (var innateAction in actionsComponent.InnateActions)
+ {
+ Assert.That(actionsComponent.TryGetActionState(innateAction, out var innateState));
+ Assert.That(innateState.Enabled);
+ }
+ Assert.That(innateActions.Count, Is.GreaterThan(0));
+
+ actionsComponent.Grant(ActionType.DebugInstant);
+ Assert.That(actionsComponent.TryGetActionState(ActionType.HumanScream, out var state) && state.Enabled);
+ });
+
+ // check that client has the actions
+ await server.WaitRunTicks(5);
+ await client.WaitRunTicks(5);
+
+ var clientPlayerMgr = client.ResolveDependency();
+ var clientUIMgr = client.ResolveDependency();
+ var expectedOrder = new List();
+
+ await client.WaitAssertion(() =>
+ {
+ var local = clientPlayerMgr.LocalPlayer;
+ var controlled = local!.ControlledEntity;
+ var actionsComponent = cEntities.GetComponent(controlled!.Value);
+
+ // we should have our innate actions and debug1.
+ foreach (var innateAction in innateActions)
+ {
+ Assert.That(actionsComponent.TryGetActionState(innateAction, out var innateState));
+ Assert.That(innateState.Enabled);
+ }
+ Assert.That(actionsComponent.TryGetActionState(ActionType.DebugInstant, out var state) && state.Enabled);
+
+ // innate actions should've auto-populated into our slots (in non-deterministic order),
+ // but debug1 should be in the last slot
+ var actionsUI =
+ clientUIMgr.StateRoot.Children.FirstOrDefault(c => c is ActionsUI) as ActionsUI;
+ Assert.That(actionsUI, Is.Not.Null);
+
+ var expectedInnate = new HashSet(innateActions);
+ var expectEmpty = false;
+ expectedOrder.Clear();
+ foreach (var slot in actionsUI.Slots)
+ {
+ if (expectEmpty)
+ {
+ Assert.That(slot.HasAssignment, Is.False);
+ Assert.That(slot.Item, Is.EqualTo(default));
+ Assert.That(slot.Action, Is.Null);
+ Assert.That(slot.ActionEnabled, Is.False);
+ continue;
+ }
+ Assert.That(slot.HasAssignment);
+ // all the actions we gave so far are not tied to an item
+ Assert.That(slot.Item, Is.EqualTo(default));
+ Assert.That(slot.Action, Is.Not.Null);
+ Assert.That(slot.ActionEnabled);
+ var asAction = slot.Action as ActionPrototype;
+ Assert.That(asAction, Is.Not.Null);
+ expectedOrder.Add(asAction.ActionType);
+
+ if (expectedInnate.Count != 0)
+ {
+ Assert.That(expectedInnate.Remove(asAction.ActionType));
+ }
+ else
+ {
+ Assert.That(asAction.ActionType, Is.EqualTo(ActionType.DebugInstant));
+ Assert.That(slot.Cooldown, Is.Null);
+ expectEmpty = true;
+ }
+ }
+ });
+
+ // now revoke the action and check that the client sees it as revoked
+ await server.WaitAssertion(() =>
+ {
+ var playerEnt = serverPlayerManager.Sessions.Single().AttachedEntity.GetValueOrDefault();
+ Assert.That(playerEnt, Is.Not.EqualTo(default));
+ var actionsComponent = sEntities.GetComponent(playerEnt);
+ actionsComponent.Revoke(ActionType.DebugInstant);
+ });
+
+ await server.WaitRunTicks(5);
+ await client.WaitRunTicks(5);
+
+ await client.WaitAssertion(() =>
+ {
+ var local = clientPlayerMgr.LocalPlayer;
+ var controlled = local!.ControlledEntity;
+ var actionsComponent = cEntities.GetComponent(controlled!.Value);
+
+ // we should have our innate actions, but debug1 should be revoked
+ foreach (var innateAction in innateActions)
+ {
+ Assert.That(actionsComponent.TryGetActionState(innateAction, out var innateState));
+ Assert.That(innateState.Enabled);
+ }
+ Assert.That(actionsComponent.TryGetActionState(ActionType.DebugInstant, out _), Is.False);
+
+ // all actions should be in the same order as before, but the slot with DebugInstant should appear
+ // disabled.
+ var actionsUI =
+ clientUIMgr.StateRoot.Children.FirstOrDefault(c => c is ActionsUI) as ActionsUI;
+ Assert.That(actionsUI, Is.Not.Null);
+
+ var idx = 0;
+ foreach (var slot in actionsUI.Slots)
+ {
+ if (idx < expectedOrder.Count)
+ {
+ var expected = expectedOrder[idx++];
+ Assert.That(slot.HasAssignment);
+ // all the actions we gave so far are not tied to an item
+ Assert.That(slot.Item, Is.EqualTo(default));
+ Assert.That(slot.Action, Is.Not.Null);
+ var asAction = slot.Action as ActionPrototype;
+ Assert.That(asAction, Is.Not.Null);
+ Assert.That(expected, Is.EqualTo(asAction.ActionType));
+
+ if (asAction.ActionType == ActionType.DebugInstant)
+ {
+ Assert.That(slot.ActionEnabled, Is.False);
+ }
+ else
+ {
+ Assert.That(slot.ActionEnabled);
+ }
+ }
+ else
+ {
+ Assert.That(slot.HasAssignment, Is.False);
+ Assert.That(slot.Item, Is.EqualTo(default));
+ Assert.That(slot.Action, Is.Null);
+ Assert.That(slot.ActionEnabled, Is.False);
+ }
+ }
+ });
+ }
+
+ [Test]
+ public async Task GrantsAndRevokesItemActions()
+ {
+ var serverOptions = new ServerIntegrationOptions
+ {
+ ExtraPrototypes = Prototypes,
+ CVarOverrides =
+ {
+ {CVars.NetPVS.Name, "false"}
+ }
+ };
+ var clientOptions = new ClientIntegrationOptions { ExtraPrototypes = Prototypes };
+ var (client, server) = await StartConnectedServerClientPair(serverOptions: serverOptions, clientOptions: clientOptions);
+
+ await server.WaitIdleAsync();
+ await client.WaitIdleAsync();
+
+ var clientEntities = client.ResolveDependency();
+
+ var serverPlayerManager = server.ResolveDependency();
+ var serverEntManager = server.ResolveDependency();
+ var serverGameTiming = server.ResolveDependency();
+
+ var cooldown = Cooldowns.SecondsFromNow(30, serverGameTiming);
+
+ ServerActionsComponent serverActionsComponent = null;
+ ClientActionsComponent clientActionsComponent = null;
+ EntityUid serverPlayerEnt = default;
+ EntityUid serverFlashlight = default;
+
+ await server.WaitAssertion(() =>
+ {
+ serverPlayerEnt = serverPlayerManager.Sessions.Single().AttachedEntity.GetValueOrDefault();
+ Assert.That(serverPlayerEnt, Is.Not.EqualTo(default));
+ serverActionsComponent = serverEntManager.GetComponent(serverPlayerEnt);
+
+ // spawn and give them an item that has actions
+ serverFlashlight = serverEntManager.SpawnEntity("TestFlashlight",
+ new EntityCoordinates(serverPlayerEnt, (0, 0)));
+ Assert.That(serverEntManager.TryGetComponent(serverFlashlight, out var itemActions));
+ // we expect this only to have a toggle light action initially
+ var actionConfigs = itemActions.ActionConfigs.ToList();
+ Assert.That(actionConfigs.Count == 1);
+ Assert.That(actionConfigs[0].ActionType == ItemActionType.ToggleLight);
+ Assert.That(actionConfigs[0].Enabled);
+
+ // grant an extra item action, before pickup, initially disabled
+ itemActions.GrantOrUpdate(ItemActionType.DebugToggle, false);
+ serverEntManager.GetComponent(serverPlayerEnt).PutInHand(serverEntManager.GetComponent(serverFlashlight), false);
+ // grant an extra item action, after pickup, with a cooldown
+ itemActions.GrantOrUpdate(ItemActionType.DebugInstant, cooldown: cooldown);
+
+ Assert.That(serverActionsComponent.TryGetItemActionStates(serverFlashlight, out var state));
+ // they should have been granted all 3 actions
+ Assert.That(state.Count == 3);
+ Assert.That(state.TryGetValue(ItemActionType.ToggleLight, out var toggleLightState));
+ Assert.That(toggleLightState.Equals(new ActionState(true)));
+ Assert.That(state.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState));
+ Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown)));
+ Assert.That(state.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState));
+ Assert.That(debugToggleState.Equals(new ActionState(false)));
+ });
+
+ await server.WaitRunTicks(5);
+ await client.WaitRunTicks(5);
+
+ // check that client has the actions, and toggle the light on via the action slot it was auto-assigned to
+ var clientPlayerMgr = client.ResolveDependency();
+ var clientUIMgr = client.ResolveDependency();
+ EntityUid clientFlashlight = default;
+ await client.WaitAssertion(() =>
+ {
+ var local = clientPlayerMgr.LocalPlayer;
+ var controlled = local!.ControlledEntity;
+ clientActionsComponent = clientEntities.GetComponent(controlled!.Value);
+
+ var lightEntry = clientActionsComponent.ItemActionStates()
+ .Where(entry => entry.Value.ContainsKey(ItemActionType.ToggleLight))
+ .FirstOrNull();
+ clientFlashlight = lightEntry!.Value.Key;
+ Assert.That(lightEntry, Is.Not.Null);
+ Assert.That(lightEntry.Value.Value.TryGetValue(ItemActionType.ToggleLight, out var lightState));
+ Assert.That(lightState.Equals(new ActionState(true)));
+ Assert.That(lightEntry.Value.Value.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState));
+ Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown)));
+ Assert.That(lightEntry.Value.Value.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState));
+ Assert.That(debugToggleState.Equals(new ActionState(false)));
+
+ var actionsUI = clientUIMgr.StateRoot.Children.FirstOrDefault(c => c is ActionsUI) as ActionsUI;
+ Assert.That(actionsUI, Is.Not.Null);
+
+ var toggleLightSlot = actionsUI.Slots.FirstOrDefault(slot => slot.Action is ItemActionPrototype
+ {
+ ActionType: ItemActionType.ToggleLight
+ });
+ Assert.That(toggleLightSlot, Is.Not.Null);
+
+ clientActionsComponent.AttemptAction(toggleLightSlot);
+ });
+
+ await server.WaitRunTicks(5);
+ await client.WaitRunTicks(5);
+
+ // server should see the action toggled on
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(serverActionsComponent.ItemActionStates().TryGetValue(serverFlashlight, out var lightStates));
+ Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState));
+ Assert.That(lightState, Is.EqualTo(new ActionState(true, toggledOn: true)));
+ });
+
+ // client should see it toggled on.
+ await client.WaitAssertion(() =>
+ {
+ Assert.That(clientActionsComponent.ItemActionStates().TryGetValue(clientFlashlight, out var lightStates));
+ Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState));
+ Assert.That(lightState, Is.EqualTo(new ActionState(true, toggledOn: true)));
+ });
+
+ await server.WaitAssertion(() =>
+ {
+ // drop the item, and the item actions should go away
+ serverEntManager.GetComponent(serverPlayerEnt)
+ .TryDropEntity(serverFlashlight, serverEntManager.GetComponent(serverPlayerEnt).Coordinates, false);
+ Assert.That(serverActionsComponent.ItemActionStates().ContainsKey(serverFlashlight), Is.False);
+ });
+
+ await server.WaitRunTicks(5);
+ await client.WaitRunTicks(5);
+
+ // client should see they have no item actions for that item either.
+ await client.WaitAssertion(() =>
+ {
+ Assert.That(clientActionsComponent.ItemActionStates().ContainsKey(clientFlashlight), Is.False);
+ });
+
+ await server.WaitAssertion(() =>
+ {
+ // pick the item up again, the states should be back to what they were when dropped,
+ // as the states "stick" with the item
+ serverEntManager.GetComponent(serverPlayerEnt).PutInHand(serverEntManager.GetComponent(serverFlashlight), false);
+ Assert.That(serverActionsComponent.ItemActionStates().TryGetValue(serverFlashlight, out var lightStates));
+ Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState));
+ Assert.That(lightState.Equals(new ActionState(true, toggledOn: true)));
+ Assert.That(lightStates.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState));
+ Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown)));
+ Assert.That(lightStates.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState));
+ Assert.That(debugToggleState.Equals(new ActionState(false)));
+ });
+
+ await server.WaitRunTicks(5);
+ await client.WaitRunTicks(5);
+
+ // client should see the actions again, with their states back to what they were
+ await client.WaitAssertion(() =>
+ {
+ Assert.That(clientActionsComponent.ItemActionStates().TryGetValue(clientFlashlight, out var lightStates));
+ Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState));
+ Assert.That(lightState.Equals(new ActionState(true, toggledOn: true)));
+ Assert.That(lightStates.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState));
+ Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown)));
+ Assert.That(lightStates.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState));
+ Assert.That(debugToggleState.Equals(new ActionState(false)));
+ });
+ }
+ }
+}
diff --git a/Content.Server/Act/IDisarmedAct.cs b/Content.Server/Act/IDisarmedAct.cs
index c63a36eda3..9602cbabb2 100644
--- a/Content.Server/Act/IDisarmedAct.cs
+++ b/Content.Server/Act/IDisarmedAct.cs
@@ -1,6 +1,30 @@
+using System;
+using Robust.Shared.Analyzers;
+using Robust.Shared.GameObjects;
+
namespace Content.Server.Act
{
- public sealed class DisarmedEvent : HandledEntityEventArgs
+ ///
+ /// Implements behavior when an entity is disarmed.
+ ///
+ [RequiresExplicitImplementation, Obsolete("Use the directed event instead.")]
+ public interface IDisarmedAct
+ {
+ ///
+ /// Behavior when the entity is disarmed.
+ /// Return true to prevent the default disarm behavior,
+ /// or rest of IDisarmedAct behaviors that come after this one from happening.
+ ///
+ bool Disarmed(DisarmedActEvent @event);
+
+ ///
+ /// Priority for this disarm act.
+ /// Used to determine act execution order.
+ ///
+ int Priority => 0;
+ }
+
+ public sealed class DisarmedActEvent : HandledEntityEventArgs
{
///
/// The entity being disarmed.
diff --git a/Content.Server/Actions/Actions/CombatMode.cs b/Content.Server/Actions/Actions/CombatMode.cs
new file mode 100644
index 0000000000..9c628ad015
--- /dev/null
+++ b/Content.Server/Actions/Actions/CombatMode.cs
@@ -0,0 +1,29 @@
+using Content.Server.CombatMode;
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Popups;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Actions.Actions
+{
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class CombatMode : IToggleAction
+ {
+ public bool DoToggleAction(ToggleActionEventArgs args)
+ {
+ if (!IoCManager.Resolve().TryGetComponent(args.Performer, out CombatModeComponent? combatMode))
+ {
+ return false;
+ }
+
+ args.Performer.PopupMessage(Loc.GetString(args.ToggledOn ? "hud-combat-enabled" : "hud-combat-disabled"));
+ combatMode.IsInCombatMode = args.ToggledOn;
+
+ return true;
+ }
+ }
+}
diff --git a/Content.Server/Actions/Actions/DebugInstant.cs b/Content.Server/Actions/Actions/DebugInstant.cs
new file mode 100644
index 0000000000..f5ef59ceeb
--- /dev/null
+++ b/Content.Server/Actions/Actions/DebugInstant.cs
@@ -0,0 +1,35 @@
+using Content.Server.Popups;
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Actions.Behaviors.Item;
+using Content.Shared.Cooldown;
+using JetBrains.Annotations;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Actions.Actions
+{
+ ///
+ /// Just shows a popup message.asd
+ ///
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class DebugInstant : IInstantAction, IInstantItemAction
+ {
+ [DataField("message")] public string Message { get; [UsedImplicitly] private set; } = "Instant action used.";
+ [DataField("cooldown")] public float Cooldown { get; [UsedImplicitly] private set; }
+
+ public void DoInstantAction(InstantItemActionEventArgs args)
+ {
+ args.Performer.PopupMessageEveryone(Message);
+ if (Cooldown > 0)
+ {
+ args.ItemActions?.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(Cooldown));
+ }
+ }
+
+ public void DoInstantAction(InstantActionEventArgs args)
+ {
+ args.Performer.PopupMessageEveryone(Message);
+ args.PerformerActions?.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(Cooldown));
+ }
+ }
+}
diff --git a/Content.Server/Actions/Actions/DebugTargetEntity.cs b/Content.Server/Actions/Actions/DebugTargetEntity.cs
new file mode 100644
index 0000000000..6f9ca4beaa
--- /dev/null
+++ b/Content.Server/Actions/Actions/DebugTargetEntity.cs
@@ -0,0 +1,27 @@
+using Content.Server.Popups;
+using Content.Shared.Actions.Behaviors;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Actions.Actions
+{
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class DebugTargetEntity : ITargetEntityAction, ITargetEntityItemAction
+ {
+ public void DoTargetEntityAction(TargetEntityItemActionEventArgs args)
+ {
+ var entMan = IoCManager.Resolve();
+
+ args.Performer.PopupMessageEveryone(entMan.GetComponent(args.Item).EntityName + ": Clicked " +
+ entMan.GetComponent(args.Target).EntityName);
+ }
+
+ public void DoTargetEntityAction(TargetEntityActionEventArgs args)
+ {
+ args.Performer.PopupMessageEveryone("Clicked " + IoCManager.Resolve().GetComponent(args.Target).EntityName);
+ }
+ }
+}
diff --git a/Content.Server/Actions/Actions/DebugTargetPoint.cs b/Content.Server/Actions/Actions/DebugTargetPoint.cs
new file mode 100644
index 0000000000..f6013868d3
--- /dev/null
+++ b/Content.Server/Actions/Actions/DebugTargetPoint.cs
@@ -0,0 +1,26 @@
+using Content.Server.Popups;
+using Content.Shared.Actions.Behaviors;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Actions.Actions
+{
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class DebugTargetPoint : ITargetPointAction, ITargetPointItemAction
+ {
+ public void DoTargetPointAction(TargetPointItemActionEventArgs args)
+ {
+ args.Performer.PopupMessageEveryone(IoCManager.Resolve().GetComponent(args.Item).EntityName + ": Clicked local position " +
+ args.Target);
+ }
+
+ public void DoTargetPointAction(TargetPointActionEventArgs args)
+ {
+ args.Performer.PopupMessageEveryone("Clicked local position " +
+ args.Target);
+ }
+ }
+}
diff --git a/Content.Server/Actions/Actions/DebugToggle.cs b/Content.Server/Actions/Actions/DebugToggle.cs
new file mode 100644
index 0000000000..69d297079c
--- /dev/null
+++ b/Content.Server/Actions/Actions/DebugToggle.cs
@@ -0,0 +1,48 @@
+using Content.Server.Popups;
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Actions.Behaviors.Item;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Actions.Actions
+{
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class DebugToggle : IToggleAction, IToggleItemAction
+ {
+ [DataField("messageOn")] public string MessageOn { get; private set; } = "on!";
+ [DataField("messageOff")] public string MessageOff { get; private set; } = "off!";
+
+ public bool DoToggleAction(ToggleItemActionEventArgs args)
+ {
+ var entMan = IoCManager.Resolve();
+
+ if (args.ToggledOn)
+ {
+ args.Performer.PopupMessageEveryone(entMan.GetComponent(args.Item).EntityName + ": " + MessageOn);
+ }
+ else
+ {
+ args.Performer.PopupMessageEveryone(entMan.GetComponent(args.Item).EntityName + ": " +MessageOff);
+ }
+
+ return true;
+ }
+
+ public bool DoToggleAction(ToggleActionEventArgs args)
+ {
+ if (args.ToggledOn)
+ {
+ args.Performer.PopupMessageEveryone(MessageOn);
+ }
+ else
+ {
+ args.Performer.PopupMessageEveryone(MessageOff);
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/Content.Server/Actions/Actions/DisarmAction.cs b/Content.Server/Actions/Actions/DisarmAction.cs
new file mode 100644
index 0000000000..dd49625da2
--- /dev/null
+++ b/Content.Server/Actions/Actions/DisarmAction.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Linq;
+using Content.Server.Act;
+using Content.Server.Actions.Events;
+using Content.Server.Administration.Logs;
+using Content.Server.Interaction;
+using Content.Server.Popups;
+using Content.Server.Weapon.Melee;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Actions;
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Actions.Components;
+using Content.Shared.Audio;
+using Content.Shared.Cooldown;
+using Content.Shared.Database;
+using Content.Shared.Popups;
+using Content.Shared.Sound;
+using JetBrains.Annotations;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Actions.Actions
+{
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class DisarmAction : ITargetEntityAction
+ {
+ [DataField("failProb")] private float _failProb = 0.4f;
+ [DataField("pushProb")] private float _pushProb = 0.4f;
+ [DataField("cooldown")] private float _cooldown = 1.5f;
+
+ [ViewVariables]
+ [DataField("punchMissSound")]
+ private SoundSpecifier PunchMissSound { get; } = new SoundPathSpecifier("/Audio/Weapons/punchmiss.ogg");
+
+ [ViewVariables]
+ [DataField("disarmSuccessSound")]
+ private SoundSpecifier DisarmSuccessSound { get; } = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
+ public void DoTargetEntityAction(TargetEntityActionEventArgs args)
+ {
+ var entMan = IoCManager.Resolve();
+ var disarmedActs = entMan.GetComponents(args.Target).ToArray();
+ var attemptEvent = new DisarmAttemptEvent(args.Target, args.Performer);
+
+ entMan.EventBus.RaiseLocalEvent(args.Target, attemptEvent);
+
+ if (attemptEvent.Cancelled)
+ return;
+
+ var sys = EntitySystem.Get();
+
+ if (!sys.InRangeUnobstructed(args.Performer, args.Target)) return;
+
+ if (disarmedActs.Length == 0)
+ {
+ if (entMan.TryGetComponent(args.Performer, out ActorComponent? actor))
+ {
+ // Fall back to a normal interaction with the entity
+ var player = actor.PlayerSession;
+ var coordinates = entMan.GetComponent(args.Target).Coordinates;
+ var target = args.Target;
+ sys.HandleUseInteraction(player, coordinates, target);
+ return;
+ }
+
+ return;
+ }
+
+ if (!entMan.TryGetComponent(args.Performer, out var actions)) return;
+ if (args.Target == args.Performer || !EntitySystem.Get().CanAttack(args.Performer)) return;
+
+ var random = IoCManager.Resolve();
+ var system = EntitySystem.Get();
+
+ var diff = entMan.GetComponent(args.Target).MapPosition.Position - entMan.GetComponent(args.Performer).MapPosition.Position;
+ var angle = Angle.FromWorldVec(diff);
+
+ actions.Cooldown(ActionType.Disarm, Cooldowns.SecondsFromNow(_cooldown));
+
+ if (random.Prob(_failProb))
+ {
+ SoundSystem.Play(Filter.Pvs(args.Performer), PunchMissSound.GetSound(), args.Performer, AudioHelpers.WithVariation(0.025f));
+
+ args.Performer.PopupMessageOtherClients(Loc.GetString("disarm-action-popup-message-other-clients",
+ ("performerName", entMan.GetComponent(args.Performer).EntityName),
+ ("targetName", entMan.GetComponent(args.Target).EntityName)));
+ args.Performer.PopupMessageCursor(Loc.GetString("disarm-action-popup-message-cursor",
+ ("targetName", entMan.GetComponent(args.Target).EntityName)));
+ system.SendLunge(angle, args.Performer);
+ return;
+ }
+
+ system.SendAnimation("disarm", angle, args.Performer, args.Performer, new[] { args.Target });
+
+ var eventArgs = new DisarmedActEvent() { Target = args.Target, Source = args.Performer, PushProbability = _pushProb };
+
+ entMan.EventBus.RaiseLocalEvent(args.Target, eventArgs);
+
+ EntitySystem.Get().Add(LogType.DisarmedAction, LogImpact.Low, $"{entMan.ToPrettyString(args.Performer):user} used disarm on {entMan.ToPrettyString(args.Target):target}");
+
+ // Check if the event has been handled, and if so, do nothing else!
+ if (eventArgs.Handled)
+ return;
+
+ // Sort by priority.
+ Array.Sort(disarmedActs, (a, b) => a.Priority.CompareTo(b.Priority));
+
+ // TODO: Remove this shit.
+ foreach (var disarmedAct in disarmedActs)
+ {
+ if (disarmedAct.Disarmed(eventArgs))
+ return;
+ }
+
+ SoundSystem.Play(Filter.Pvs(args.Performer), DisarmSuccessSound.GetSound(), entMan.GetComponent(args.Performer).Coordinates, AudioHelpers.WithVariation(0.025f));
+ }
+ }
+}
diff --git a/Content.Server/Actions/Actions/GhostBoo.cs b/Content.Server/Actions/Actions/GhostBoo.cs
new file mode 100644
index 0000000000..190fc05be4
--- /dev/null
+++ b/Content.Server/Actions/Actions/GhostBoo.cs
@@ -0,0 +1,49 @@
+using System.Linq;
+using Content.Server.Ghost;
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Actions.Components;
+using Content.Shared.Cooldown;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Actions.Actions
+{
+ ///
+ /// Blink lights and scare livings
+ ///
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class GhostBoo : IInstantAction
+ {
+ [DataField("radius")] private float _radius = 3;
+ [DataField("cooldown")] private float _cooldown = 120;
+ [DataField("maxTargets")] private int _maxTargets = 3;
+
+ public void DoInstantAction(InstantActionEventArgs args)
+ {
+ var entMan = IoCManager.Resolve();
+
+ if (!entMan.TryGetComponent(args.Performer, out var actions)) return;
+
+ // find all IGhostBooAffected nearby and do boo on them
+ var ents = IoCManager.Resolve().GetEntitiesInRange(args.Performer, _radius);
+
+ var booCounter = 0;
+ foreach (var ent in ents)
+ {
+ var ghostBoo = new GhostBooEvent();
+ entMan.EventBus.RaiseLocalEvent(ent, ghostBoo);
+
+ if (ghostBoo.Handled)
+ booCounter++;
+
+ if (booCounter >= _maxTargets)
+ break;
+ }
+
+ actions.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(_cooldown));
+ }
+ }
+}
diff --git a/Content.Server/Actions/Actions/GuardianToggleAction.cs b/Content.Server/Actions/Actions/GuardianToggleAction.cs
new file mode 100644
index 0000000000..33b2b4b492
--- /dev/null
+++ b/Content.Server/Actions/Actions/GuardianToggleAction.cs
@@ -0,0 +1,38 @@
+using Content.Server.Guardian;
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Cooldown;
+using Content.Shared.Popups;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Actions.Actions
+{
+ ///
+ /// Manifests the guardian saved in the action, using the system
+ ///
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class ToggleGuardianAction : IInstantAction
+ {
+ [DataField("cooldown")] public float Cooldown { get; [UsedImplicitly] private set; }
+
+ public void DoInstantAction(InstantActionEventArgs args)
+ {
+ var entManager = IoCManager.Resolve();
+
+ if (entManager.TryGetComponent(args.Performer, out GuardianHostComponent? hostComponent) &&
+ hostComponent.HostedGuardian != null)
+ {
+ EntitySystem.Get().ToggleGuardian(hostComponent);
+ args.PerformerActions?.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(Cooldown));
+ }
+ else
+ {
+ args.Performer.PopupMessage(Loc.GetString("guardian-missing-invalid-action"));
+ }
+ }
+ }
+}
diff --git a/Content.Server/Actions/Actions/PAIMidi.cs b/Content.Server/Actions/Actions/PAIMidi.cs
new file mode 100644
index 0000000000..10e08b2731
--- /dev/null
+++ b/Content.Server/Actions/Actions/PAIMidi.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Linq;
+using Content.Server.Ghost;
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Actions.Components;
+using Content.Shared.Cooldown;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Server.GameObjects;
+using Content.Shared.Instruments;
+
+namespace Content.Server.Actions.Actions
+{
+ ///
+ /// Pull up MIDI instrument interface for PAIs to "play themselves"
+ ///
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class PAIMidi : IInstantAction
+ {
+
+ public void DoInstantAction(InstantActionEventArgs args)
+ {
+ var entMan = IoCManager.Resolve();
+
+ if (!entMan.TryGetComponent(args.Performer, out var serverUi)) return;
+ if (!entMan.TryGetComponent(args.Performer, out var actor)) return;
+ if (!serverUi.TryGetBoundUserInterface(InstrumentUiKey.Key,out var bui)) return;
+
+ bui.Toggle(actor.PlayerSession);
+
+ }
+ }
+}
diff --git a/Content.Server/Actions/Actions/ScreamAction.cs b/Content.Server/Actions/Actions/ScreamAction.cs
new file mode 100644
index 0000000000..b86842ad40
--- /dev/null
+++ b/Content.Server/Actions/Actions/ScreamAction.cs
@@ -0,0 +1,71 @@
+using Content.Shared.ActionBlocker;
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Actions.Components;
+using Content.Shared.Audio;
+using Content.Shared.CharacterAppearance;
+using Content.Shared.CharacterAppearance.Components;
+using Content.Shared.Cooldown;
+using Content.Shared.Sound;
+using JetBrains.Annotations;
+using Robust.Shared.Audio;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Serialization.Manager.Attributes;
+using System;
+using Robust.Shared.Serialization;
+
+namespace Content.Server.Actions.Actions
+{
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class ScreamAction : IInstantAction, ISerializationHooks
+ {
+ private const float Variation = 0.125f;
+ private const float Volume = 4f;
+
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ [DataField("male", required: true)] private SoundSpecifier _male = default!;
+ [DataField("female", required: true)] private SoundSpecifier _female = default!;
+ [DataField("wilhelm", required: true)] private SoundSpecifier _wilhelm = default!;
+
+ /// seconds
+ [DataField("cooldown")] private float _cooldown = 10;
+
+ void ISerializationHooks.AfterDeserialization()
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ public void DoInstantAction(InstantActionEventArgs args)
+ {
+ if (!EntitySystem.Get().CanSpeak(args.Performer)) return;
+ if (!_entMan.TryGetComponent(args.Performer, out var humanoid)) return;
+ if (!_entMan.TryGetComponent(args.Performer, out var actions)) return;
+
+ if (_random.Prob(.01f))
+ {
+ SoundSystem.Play(Filter.Pvs(args.Performer), _wilhelm.GetSound(), args.Performer, AudioParams.Default.WithVolume(Volume));
+ }
+ else
+ {
+ switch (humanoid.Sex)
+ {
+ case Sex.Male:
+ SoundSystem.Play(Filter.Pvs(args.Performer), _male.GetSound(), args.Performer, AudioHelpers.WithVariation(Variation).WithVolume(Volume));
+ break;
+ case Sex.Female:
+ SoundSystem.Play(Filter.Pvs(args.Performer), _female.GetSound(), args.Performer, AudioHelpers.WithVariation(Variation).WithVolume(Volume));
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ actions.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(_cooldown));
+ }
+ }
+}
diff --git a/Content.Server/Actions/ActionsSystem.cs b/Content.Server/Actions/ActionsSystem.cs
deleted file mode 100644
index 0b15b5770b..0000000000
--- a/Content.Server/Actions/ActionsSystem.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Content.Server.Chat.Managers;
-using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
-using JetBrains.Annotations;
-using Robust.Server.GameObjects;
-
-namespace Content.Server.Actions
-{
- [UsedImplicitly]
- public sealed class ActionsSystem : SharedActionsSystem
- {
- [Dependency] private readonly IChatManager _chatMan = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnPlayerAttached);
- }
-
- private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args)
- {
- // need to send state to new player.
- component.Dirty();
- }
-
- protected override bool PerformBasicActions(EntityUid user, ActionType action)
- {
- var result = base.PerformBasicActions(user, action);
-
- if (!string.IsNullOrWhiteSpace(action.Speech))
- {
- _chatMan.EntitySay(user, Loc.GetString(action.Speech));
- result = true;
- }
-
- return result;
- }
- }
-}
diff --git a/Content.Server/Actions/Commands/CooldownAction.cs b/Content.Server/Actions/Commands/CooldownAction.cs
new file mode 100644
index 0000000000..dfd9cb4f41
--- /dev/null
+++ b/Content.Server/Actions/Commands/CooldownAction.cs
@@ -0,0 +1,64 @@
+using System;
+using Content.Server.Administration;
+using Content.Server.Commands;
+using Content.Shared.Actions;
+using Content.Shared.Administration;
+using Robust.Server.Player;
+using Robust.Shared.Console;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Actions.Commands
+{
+ [AdminCommand(AdminFlags.Debug)]
+ public sealed class CooldownAction : IConsoleCommand
+ {
+ public string Command => "coolaction";
+ public string Description => "Sets a cooldown on an action for a player, defaulting to current player";
+ public string Help => "coolaction ";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var player = shell.Player as IPlayerSession;
+ if (player?.AttachedEntity is not {} attachedEntity) return;
+ if (args.Length > 2)
+ {
+ var target = args[2];
+ if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
+ }
+
+ if (!IoCManager.Resolve().TryGetComponent(attachedEntity, out ServerActionsComponent? actionsComponent))
+ {
+ shell.WriteError("user has no actions component");
+ return;
+ }
+
+ var actionTypeRaw = args[0];
+ if (!Enum.TryParse(actionTypeRaw, out var actionType))
+ {
+ shell.WriteLine("unrecognized ActionType enum value, please" +
+ " ensure you used correct casing: " + actionTypeRaw);
+ return;
+ }
+ var actionMgr = IoCManager.Resolve();
+
+ if (!actionMgr.TryGet(actionType, out var action))
+ {
+ shell.WriteLine("unrecognized actionType " + actionType);
+ return;
+ }
+
+ var cooldownStart = IoCManager.Resolve().CurTime;
+ if (!uint.TryParse(args[1], out var seconds))
+ {
+ shell.WriteLine("cannot parse seconds: " + args[1]);
+ return;
+ }
+
+ var cooldownEnd = cooldownStart.Add(TimeSpan.FromSeconds(seconds));
+
+ actionsComponent.Cooldown(action.ActionType, (cooldownStart, cooldownEnd));
+ }
+ }
+}
diff --git a/Content.Server/Actions/Commands/GrantAction.cs b/Content.Server/Actions/Commands/GrantAction.cs
new file mode 100644
index 0000000000..09d8fe4b7d
--- /dev/null
+++ b/Content.Server/Actions/Commands/GrantAction.cs
@@ -0,0 +1,53 @@
+using System;
+using Content.Server.Administration;
+using Content.Server.Commands;
+using Content.Shared.Actions;
+using Content.Shared.Administration;
+using Robust.Server.Player;
+using Robust.Shared.Console;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Server.Actions.Commands
+{
+ [AdminCommand(AdminFlags.Debug)]
+ public sealed class GrantAction : IConsoleCommand
+ {
+ public string Command => "grantaction";
+ public string Description => "Grants an action to a player, defaulting to current player";
+ public string Help => "grantaction ";
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var player = shell.Player as IPlayerSession;
+ if (player?.AttachedEntity == null) return;
+ var attachedEntity = player.AttachedEntity.Value;
+ if (args.Length > 1)
+ {
+ var target = args[1];
+ if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
+ }
+
+ if (attachedEntity == default) return;
+ if (!IoCManager.Resolve().TryGetComponent(attachedEntity, out ServerActionsComponent? actionsComponent))
+ {
+ shell.WriteLine("user has no actions component");
+ return;
+ }
+
+ var actionTypeRaw = args[0];
+ if (!Enum.TryParse(actionTypeRaw, out var actionType))
+ {
+ shell.WriteLine("unrecognized ActionType enum value, please" +
+ " ensure you used correct casing: " + actionTypeRaw);
+ return;
+ }
+ var actionMgr = IoCManager.Resolve();
+ if (!actionMgr.TryGet(actionType, out var action))
+ {
+ shell.WriteLine("unrecognized actionType " + actionType);
+ return;
+ }
+ actionsComponent.Grant(action.ActionType);
+ }
+ }
+}
diff --git a/Content.Server/Actions/Commands/RevokeAction.cs b/Content.Server/Actions/Commands/RevokeAction.cs
new file mode 100644
index 0000000000..39db78d88d
--- /dev/null
+++ b/Content.Server/Actions/Commands/RevokeAction.cs
@@ -0,0 +1,54 @@
+using System;
+using Content.Server.Administration;
+using Content.Server.Commands;
+using Content.Shared.Actions;
+using Content.Shared.Administration;
+using Robust.Server.Player;
+using Robust.Shared.Console;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Server.Actions.Commands
+{
+ [AdminCommand(AdminFlags.Debug)]
+ public sealed class RevokeAction : IConsoleCommand
+ {
+ public string Command => "revokeaction";
+ public string Description => "Revokes an action from a player, defaulting to current player";
+ public string Help => "revokeaction ";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var player = shell.Player as IPlayerSession;
+ if (player?.AttachedEntity == null) return;
+ var attachedEntity = player.AttachedEntity.Value;
+ if (args.Length > 1)
+ {
+ var target = args[1];
+ if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
+ }
+ if (attachedEntity == default) return;
+ if (!IoCManager.Resolve().TryGetComponent(attachedEntity, out ServerActionsComponent? actionsComponent))
+ {
+ shell.WriteLine("user has no actions component");
+ return;
+ }
+
+ var actionTypeRaw = args[0];
+ if (!Enum.TryParse(actionTypeRaw, out var actionType))
+ {
+ shell.WriteLine("unrecognized ActionType enum value, please" +
+ " ensure you used correct casing: " + actionTypeRaw);
+ return;
+ }
+ var actionMgr = IoCManager.Resolve();
+ if (!actionMgr.TryGet(actionType, out var action))
+ {
+ shell.WriteLine("unrecognized actionType " + actionType);
+ return;
+ }
+
+ actionsComponent.Revoke(action.ActionType);
+ }
+ }
+}
diff --git a/Content.Server/Actions/ServerActionsComponent.cs b/Content.Server/Actions/ServerActionsComponent.cs
new file mode 100644
index 0000000000..703e276174
--- /dev/null
+++ b/Content.Server/Actions/ServerActionsComponent.cs
@@ -0,0 +1,215 @@
+using System;
+using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
+using Content.Shared.Actions.Prototypes;
+using Content.Shared.Interaction;
+using Robust.Shared;
+using Robust.Shared.Configuration;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Network;
+using Robust.Shared.Players;
+
+namespace Content.Server.Actions
+{
+ [RegisterComponent]
+ [ComponentReference(typeof(SharedActionsComponent))]
+ public sealed class ServerActionsComponent : SharedActionsComponent
+ {
+ [Dependency] private readonly IConfigurationManager _configManager = default!;
+ [Dependency] private readonly IEntityManager _entities = default!;
+
+ private float MaxUpdateRange;
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+ _configManager.OnValueChanged(CVars.NetMaxUpdateRange, OnRangeChanged, true);
+ }
+
+ protected override void Shutdown()
+ {
+ base.Shutdown();
+ _configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, OnRangeChanged);
+ }
+
+ private void OnRangeChanged(float obj)
+ {
+ MaxUpdateRange = obj;
+ }
+
+ [Obsolete("Component Messages are deprecated, use Entity Events instead.")]
+ public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null)
+ {
+ base.HandleNetworkMessage(message, netChannel, session);
+
+ if (message is not BasePerformActionMessage performActionMessage) return;
+ if (session == null)
+ {
+ throw new ArgumentNullException(nameof(session));
+ }
+
+ if (session.AttachedEntity is not {Valid: true} player || player != Owner) return;
+ var attempt = ActionAttempt(performActionMessage, session);
+ if (attempt == null) return;
+
+ if (!attempt.TryGetActionState(this, out var actionState) || !actionState.Enabled)
+ {
+ Logger.DebugS("action", "user {0} attempted to use" +
+ " action {1} which is not granted to them", _entities.GetComponent(player).EntityName,
+ attempt);
+ return;
+ }
+
+ if (actionState.IsOnCooldown(GameTiming))
+ {
+ Logger.DebugS("action", "user {0} attempted to use" +
+ " action {1} which is on cooldown", _entities.GetComponent(player).EntityName,
+ attempt);
+ return;
+ }
+
+ switch (performActionMessage.BehaviorType)
+ {
+ case BehaviorType.Instant:
+ attempt.DoInstantAction(player);
+ break;
+ case BehaviorType.Toggle:
+ if (performActionMessage is not IToggleActionMessage toggleMsg) return;
+ if (toggleMsg.ToggleOn == actionState.ToggledOn)
+ {
+ Logger.DebugS("action", "user {0} attempted to" +
+ " toggle action {1} to {2}, but it is already toggled {2}", _entities.GetComponent(player).EntityName,
+ attempt.Action.Name, toggleMsg.ToggleOn);
+ return;
+ }
+
+ if (attempt.DoToggleAction(player, toggleMsg.ToggleOn))
+ {
+ attempt.ToggleAction(this, toggleMsg.ToggleOn);
+ }
+ else
+ {
+ // if client predicted the toggle will work, need to reset
+ // that prediction
+ Dirty();
+ }
+ break;
+ case BehaviorType.TargetPoint:
+ if (performActionMessage is not ITargetPointActionMessage targetPointMsg) return;
+ if (!CheckRangeAndSetFacing(targetPointMsg.Target, player)) return;
+ attempt.DoTargetPointAction(player, targetPointMsg.Target);
+ break;
+ case BehaviorType.TargetEntity:
+ if (performActionMessage is not ITargetEntityActionMessage targetEntityMsg) return;
+ if (!EntityManager.EntityExists(targetEntityMsg.Target))
+ {
+ Logger.DebugS("action", "user {0} attempted to" +
+ " perform target entity action {1} but could not find entity with " +
+ "provided uid {2}", _entities.GetComponent(player).EntityName, attempt.Action.Name,
+ targetEntityMsg.Target);
+ return;
+ }
+ if (!CheckRangeAndSetFacing(_entities.GetComponent(targetEntityMsg.Target).Coordinates, player)) return;
+
+ attempt.DoTargetEntityAction(player, targetEntityMsg.Target);
+ break;
+ case BehaviorType.None:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private IActionAttempt? ActionAttempt(BasePerformActionMessage message, ICommonSession session)
+ {
+ IActionAttempt? attempt;
+ var player = session.AttachedEntity;
+
+ switch (message)
+ {
+ case PerformActionMessage performActionMessage:
+ if (!ActionManager.TryGet(performActionMessage.ActionType, out var action))
+ {
+ Logger.DebugS("action", "user {0} attempted to perform" +
+ " unrecognized action {1}", player,
+ performActionMessage.ActionType);
+ return null;
+ }
+ attempt = new ActionAttempt(action);
+ break;
+ case PerformItemActionMessage performItemActionMessage:
+ var type = performItemActionMessage.ActionType;
+ if (!ActionManager.TryGet(type, out var itemAction))
+ {
+ Logger.DebugS("action", "user {0} attempted to perform" +
+ " unrecognized item action {1}",
+ player, type);
+ return null;
+ }
+
+ var item = performItemActionMessage.Item;
+ if (!EntityManager.EntityExists(item))
+ {
+ Logger.DebugS("action", "user {0} attempted to perform" +
+ " item action {1} for unknown item {2}",
+ player, type, item);
+ return null;
+ }
+
+ if (!_entities.TryGetComponent(item, out var actionsComponent))
+ {
+ Logger.DebugS("action", "user {0} attempted to perform" +
+ " item action {1} for item {2} which has no ItemActionsComponent",
+ player, type, item);
+ return null;
+ }
+
+ if (actionsComponent.Holder != player)
+ {
+ Logger.DebugS("action", "user {0} attempted to perform" +
+ " item action {1} for item {2} which they are not holding",
+ player, type, item);
+ return null;
+ }
+
+ attempt = new ItemActionAttempt(itemAction, item, actionsComponent);
+ break;
+ default:
+ return null;
+ }
+
+ if (message.BehaviorType != attempt.Action.BehaviorType)
+ {
+ Logger.DebugS("action", "user {0} attempted to" +
+ " perform action {1} as a {2} behavior, but this action is actually a" +
+ " {3} behavior", player, attempt, message.BehaviorType,
+ attempt.Action.BehaviorType);
+ return null;
+ }
+
+ return attempt;
+ }
+
+ private bool CheckRangeAndSetFacing(EntityCoordinates target, EntityUid player)
+ {
+ // ensure it's within their clickable range
+ var targetWorldPos = target.ToMapPos(EntityManager);
+ var rangeBox = new Box2(_entities.GetComponent(player).WorldPosition, _entities.GetComponent(player).WorldPosition)
+ .Enlarged(MaxUpdateRange);
+ if (!rangeBox.Contains(targetWorldPos))
+ {
+ Logger.DebugS("action", "user {0} attempted to" +
+ " perform target action further than allowed range",
+ _entities.GetComponent(player).EntityName);
+ return false;
+ }
+
+ EntitySystem.Get().TryFaceCoordinates(player, targetWorldPos);
+ return true;
+ }
+ }
+}
diff --git a/Content.Server/Actions/Spells/GiveItemSpell.cs b/Content.Server/Actions/Spells/GiveItemSpell.cs
new file mode 100644
index 0000000000..a57f5b5783
--- /dev/null
+++ b/Content.Server/Actions/Spells/GiveItemSpell.cs
@@ -0,0 +1,77 @@
+using Content.Server.Hands.Components;
+using Content.Server.Popups;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Cooldown;
+using Content.Shared.Item;
+using Content.Shared.Popups;
+using Content.Shared.Sound;
+using JetBrains.Annotations;
+using Robust.Shared.Audio;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Log;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Actions.Spells
+{
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class GiveItemSpell : IInstantAction
+ { //TODO: Needs to be an EntityPrototype for proper validation
+ [ViewVariables] [DataField("castMessage")] public string? CastMessage { get; set; } = default!;
+ [ViewVariables] [DataField("cooldown")] public float CoolDown { get; set; } = 1f;
+ [ViewVariables] [DataField("spellItem", customTypeSerializer:typeof(PrototypeIdSerializer))] public string ItemProto { get; set; } = default!;
+
+ [ViewVariables] [DataField("castSound", required: true)] public SoundSpecifier CastSound { get; set; } = default!;
+
+ //Rubber-band snapping items into player's hands, originally was a workaround, later found it works quite well with stuns
+ //Not sure if needs fixing
+
+ public void DoInstantAction(InstantActionEventArgs args)
+ {
+ var entMan = IoCManager.Resolve();
+
+ var caster = args.Performer;
+
+ if (!entMan.TryGetComponent(caster, out HandsComponent? handsComponent))
+ {
+ caster.PopupMessage(Loc.GetString("spell-fail-no-hands"));
+ return;
+ }
+
+ if (!EntitySystem.Get().CanInteract(caster, null)) return;
+
+ // TODO: Nix when we get EntityPrototype serializers
+ if (!IoCManager.Resolve().HasIndex(ItemProto))
+ {
+ Logger.Error($"Invalid prototype {ItemProto} supplied for {nameof(GiveItemSpell)}");
+ return;
+ }
+
+ // TODO: Look this is shitty and ideally a test would do it
+ var spawnedProto = entMan.SpawnEntity(ItemProto, entMan.GetComponent(caster).MapPosition);
+
+ if (!entMan.TryGetComponent(spawnedProto, out SharedItemComponent? itemComponent))
+ {
+ Logger.Error($"Tried to use {nameof(GiveItemSpell)} but prototype has no {nameof(SharedItemComponent)}?");
+ entMan.DeleteEntity(spawnedProto);
+ return;
+ }
+
+ args.PerformerActions?.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(CoolDown));
+
+ if (CastMessage != null)
+ caster.PopupMessageEveryone(CastMessage);
+
+ handsComponent.PutInHandOrDrop(itemComponent);
+
+ SoundSystem.Play(Filter.Pvs(caster), CastSound.GetSound(), caster);
+ }
+ }
+}
diff --git a/Content.Server/Atmos/Components/GasTankComponent.cs b/Content.Server/Atmos/Components/GasTankComponent.cs
index f2f11aeacb..54f725a7fd 100644
--- a/Content.Server/Atmos/Components/GasTankComponent.cs
+++ b/Content.Server/Atmos/Components/GasTankComponent.cs
@@ -1,21 +1,30 @@
+using System;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Explosion.EntitySystems;
using Content.Server.UserInterface;
+using Content.Shared.ActionBlocker;
using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
+using Content.Shared.Actions.Behaviors.Item;
+using Content.Shared.Actions.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Audio;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Sound;
+using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
using Robust.Shared.Player;
+using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
+using Robust.Shared.ViewVariables;
namespace Content.Server.Atmos.Components
{
@@ -81,9 +90,6 @@ namespace Content.Server.Atmos.Components
[DataField("tankFragmentScale")]
public float TankFragmentScale { get; set; } = 10 * Atmospherics.OneAtmosphere;
- [DataField("toggleAction", required: true)]
- public InstantAction ToggleAction = default!;
-
protected override void Initialize()
{
base.Initialize();
@@ -159,7 +165,6 @@ namespace Content.Server.Atmos.Components
var internals = GetInternalsComponent();
if (internals == null) return;
IsConnected = internals.TryConnectTank(Owner);
- EntitySystem.Get().SetToggled(ToggleAction, IsConnected);
UpdateUserInterface();
}
@@ -167,7 +172,6 @@ namespace Content.Server.Atmos.Components
{
if (!IsConnected) return;
IsConnected = false;
- EntitySystem.Get().SetToggled(ToggleAction, false);
GetInternalsComponent(owner)?.DisconnectTank();
UpdateUserInterface();
}
@@ -183,6 +187,9 @@ namespace Content.Server.Atmos.Components
InternalsConnected = IsConnected,
CanConnectInternals = IsFunctional && internals != null
});
+
+ if (internals == null || !_entityManager.TryGetComponent(Owner, out var itemActions)) return;
+ itemActions.GrantOrUpdate(ItemActionType.ToggleInternals, IsFunctional, IsConnected);
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
@@ -302,4 +309,22 @@ namespace Content.Server.Atmos.Components
DisconnectFromInternals(eventArgs.User);
}
}
+
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class ToggleInternalsAction : IToggleItemAction
+ {
+ public bool DoToggleAction(ToggleItemActionEventArgs args)
+ {
+ if (!EntitySystem.Get().CanInteract(args.Performer, args.Item))
+ return false;
+
+ if (!IoCManager.Resolve().TryGetComponent(args.Item, out var gasTankComponent)) return false;
+ // no change
+ if (gasTankComponent.IsConnected == args.ToggledOn) return false;
+ gasTankComponent.ToggleInternals();
+ // did we successfully toggle to the desired status?
+ return gasTankComponent.IsConnected == args.ToggledOn;
+ }
+ }
}
diff --git a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
index af969e75b3..bc8d7af6ca 100644
--- a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
@@ -1,9 +1,10 @@
using Content.Server.Atmos.Components;
-using Content.Shared.Actions;
-using Content.Shared.Toggleable;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
namespace Content.Server.Atmos.EntitySystems
{
@@ -19,23 +20,6 @@ namespace Content.Server.Atmos.EntitySystems
{
base.Initialize();
SubscribeLocalEvent>(AddOpenUIVerb);
- SubscribeLocalEvent(OnGetActions);
- SubscribeLocalEvent(OnActionToggle);
- }
-
- private void OnGetActions(EntityUid uid, GasTankComponent component, GetActionsEvent args)
- {
- args.Actions.Add(component.ToggleAction);
- }
-
- private void OnActionToggle(EntityUid uid, GasTankComponent component, ToggleActionEvent args)
- {
- if (args.Handled)
- return;
-
- component.ToggleInternals();
-
- args.Handled = true;
}
private void AddOpenUIVerb(EntityUid uid, GasTankComponent component, GetVerbsEvent args)
diff --git a/Content.Server/Chemistry/ReagentEffects/DoAction.cs b/Content.Server/Chemistry/ReagentEffects/DoAction.cs
new file mode 100644
index 0000000000..12f8b3c509
--- /dev/null
+++ b/Content.Server/Chemistry/ReagentEffects/DoAction.cs
@@ -0,0 +1,34 @@
+using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
+using Content.Shared.Actions.Prototypes;
+using Content.Shared.Chemistry.Reagent;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Chemistry.ReagentEffects;
+
+///
+/// Forces someone to do a certain action, if they have it.
+///
+public sealed class DoAction : ReagentEffect
+{
+ [DataField("action", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))]
+ public string Action = default!;
+
+ public override void Effect(ReagentEffectArgs args)
+ {
+ if (args.EntityManager.TryGetComponent(args.SolutionEntity, out SharedActionsComponent? actions))
+ {
+ if (!IoCManager.Resolve().TryIndex(Action, out var proto))
+ return;
+
+ if (actions.IsGranted(proto.ActionType))
+ {
+ var attempt = new ActionAttempt(proto);
+ attempt.DoInstantAction(args.SolutionEntity);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Chemistry/ReagentEffects/Scream.cs b/Content.Server/Chemistry/ReagentEffects/Scream.cs
deleted file mode 100644
index 27cb88851a..0000000000
--- a/Content.Server/Chemistry/ReagentEffects/Scream.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using Content.Server.Speech;
-using Content.Shared.Chemistry.Reagent;
-
-namespace Content.Server.Chemistry.ReagentEffects;
-
-///
-/// Forces someone to scream their lungs out.
-///
-public sealed class Scream : ReagentEffect
-{
- public override void Effect(ReagentEffectArgs args)
- {
- EntitySystem.Get().TryScream(args.SolutionEntity);
- }
-}
diff --git a/Content.Server/Clothing/Components/MagbootsComponent.cs b/Content.Server/Clothing/Components/MagbootsComponent.cs
index 65d075a678..418f071019 100644
--- a/Content.Server/Clothing/Components/MagbootsComponent.cs
+++ b/Content.Server/Clothing/Components/MagbootsComponent.cs
@@ -1,4 +1,6 @@
using Content.Shared.Actions;
+using Content.Shared.Actions.Behaviors.Item;
+using Content.Shared.Actions.Components;
using Content.Shared.Clothing;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
@@ -36,6 +38,8 @@ namespace Content.Server.Clothing.Components
EntitySystem.Get().UpdateMagbootEffects(container.Owner, Owner, true, this);
}
+ if(_entMan.TryGetComponent(Owner, out var itemActions))
+ itemActions.Toggle(ItemActionType.ToggleMagboots, On);
if (_entMan.TryGetComponent(Owner, out var item))
item.EquippedPrefix = On ? "on" : null;
if(_entMan.TryGetComponent(Owner, out var sprite))
@@ -45,14 +49,33 @@ namespace Content.Server.Clothing.Components
}
}
- void IActivate.Activate(ActivateEventArgs eventArgs)
+ public void Toggle(EntityUid user)
{
On = !On;
}
+ void IActivate.Activate(ActivateEventArgs eventArgs)
+ {
+ Toggle(eventArgs.User);
+ }
+
public override ComponentState GetComponentState()
{
return new MagbootsComponentState(On);
}
}
+
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class ToggleMagbootsAction : IToggleItemAction
+ {
+ public bool DoToggleAction(ToggleItemActionEventArgs args)
+ {
+ if (!IoCManager.Resolve().TryGetComponent(args.Item, out var magboots))
+ return false;
+
+ magboots.Toggle(args.Performer);
+ return true;
+ }
+ }
}
diff --git a/Content.Server/Clothing/MagbootsSystem.cs b/Content.Server/Clothing/MagbootsSystem.cs
index 8e14b61da3..c5252ebec0 100644
--- a/Content.Server/Clothing/MagbootsSystem.cs
+++ b/Content.Server/Clothing/MagbootsSystem.cs
@@ -1,17 +1,18 @@
using Content.Server.Alert;
using Content.Server.Atmos.Components;
using Content.Server.Clothing.Components;
-using Content.Shared.Actions;
using Content.Shared.Alert;
-using Content.Shared.Clothing;
using Content.Shared.Inventory.Events;
using Content.Shared.Movement.EntitySystems;
using Content.Shared.Slippery;
using Content.Shared.Verbs;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
namespace Content.Server.Clothing
{
- public sealed class MagbootsSystem : SharedMagbootsSystem
+ public sealed class MagbootsSystem : EntitySystem
{
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
diff --git a/Content.Server/CombatMode/CombatModeSystem.cs b/Content.Server/CombatMode/CombatModeSystem.cs
index 47a57dc9fb..cbb595c84f 100644
--- a/Content.Server/CombatMode/CombatModeSystem.cs
+++ b/Content.Server/CombatMode/CombatModeSystem.cs
@@ -1,82 +1,10 @@
-using Content.Server.Act;
-using Content.Server.Actions.Events;
-using Content.Server.Administration.Logs;
-using Content.Server.Popups;
-using Content.Server.Weapon.Melee;
-using Content.Shared.ActionBlocker;
-using Content.Shared.Audio;
-using Content.Shared.CombatMode;
-using Content.Shared.Database;
+using Content.Shared.CombatMode;
using JetBrains.Annotations;
-using Robust.Shared.Audio;
-using Robust.Shared.Player;
-using Robust.Shared.Random;
namespace Content.Server.CombatMode
{
[UsedImplicitly]
public sealed class CombatModeSystem : SharedCombatModeSystem
{
- [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
- [Dependency] private readonly MeleeWeaponSystem _meleeWeaponSystem = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
- [Dependency] private readonly AdminLogSystem _logSystem = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnEntityActionPerform);
- }
-
- private void OnEntityActionPerform(EntityUid uid, SharedCombatModeComponent component, DisarmActionEvent args)
- {
- if (args.Handled)
- return;
-
- if (!_actionBlockerSystem.CanAttack(args.Performer))
- return;
-
- var attemptEvent = new DisarmAttemptEvent(args.Target, args.Performer);
- RaiseLocalEvent(args.Target, attemptEvent);
- if (attemptEvent.Cancelled)
- return;
-
- var diff = Transform(args.Target).MapPosition.Position - Transform(args.Performer).MapPosition.Position;
- var angle = Angle.FromWorldVec(diff);
-
- var filterAll = Filter.Pvs(args.Performer);
- var filterOther = filterAll.RemoveWhereAttachedEntity(e => e == args.Performer);
-
- args.Handled = true;
-
- if (_random.Prob(component.DisarmFailChance))
- {
- SoundSystem.Play(Filter.Pvs(args.Performer), component.DisarmFailSound.GetSound(), args.Performer, AudioHelpers.WithVariation(0.025f));
-
- var targetName = Name(args.Target);
-
- var msgOther = Loc.GetString(
- "disarm-action-popup-message-other-clients",
- ("performerName", Name(args.Performer)),
- ("targetName", targetName));
-
- var msgUser = Loc.GetString("disarm-action-popup-message-cursor", ("targetName", targetName ));
-
- _popupSystem.PopupEntity(msgOther, args.Performer, filterOther);
- _popupSystem.PopupEntity(msgUser, args.Performer, Filter.Entities(args.Performer));
-
- _meleeWeaponSystem.SendLunge(angle, args.Performer);
- return;
- }
-
- _meleeWeaponSystem.SendAnimation("disarm", angle, args.Performer, args.Performer, new[] { args.Target });
- SoundSystem.Play(filterAll, component.DisarmSuccessSound.GetSound(), args.Performer, AudioHelpers.WithVariation(0.025f));
- _logSystem.Add(LogType.DisarmedAction, $"{ToPrettyString(args.Performer):user} used disarm on {ToPrettyString(args.Target):target}");
-
- var eventArgs = new DisarmedEvent() { Target = args.Target, Source = args.Performer, PushProbability = component.DisarmPushChance };
- RaiseLocalEvent(args.Target, eventArgs);
- }
}
}
diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs
index 0d4b9125dd..13d9e6d616 100644
--- a/Content.Server/Entry/EntryPoint.cs
+++ b/Content.Server/Entry/EntryPoint.cs
@@ -113,6 +113,7 @@ namespace Content.Server.Entry
else
{
IoCManager.Resolve().Initialize();
+ IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
diff --git a/Content.Server/Ghost/Components/GhostComponent.cs b/Content.Server/Ghost/Components/GhostComponent.cs
index 6142b9e82b..5752451106 100644
--- a/Content.Server/Ghost/Components/GhostComponent.cs
+++ b/Content.Server/Ghost/Components/GhostComponent.cs
@@ -1,7 +1,6 @@
-using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
+using System;
using Content.Shared.Ghost;
-using Robust.Shared.Utility;
+using Robust.Shared.GameObjects;
namespace Content.Server.Ghost.Components
{
@@ -10,23 +9,5 @@ namespace Content.Server.Ghost.Components
public sealed class GhostComponent : SharedGhostComponent
{
public TimeSpan TimeOfDeath { get; set; } = TimeSpan.Zero;
-
- [DataField("booRadius")]
- public float BooRadius = 3;
-
- [DataField("booMaxTargets")]
- public int BooMaxTargets = 3;
-
- [DataField("action")]
- public InstantAction Action = new()
- {
- UseDelay = TimeSpan.FromSeconds(120),
- Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/scream.png")),
- Name = "action-name-boo",
- Description = "action-description-boo",
- Event = new BooActionEvent(),
- };
}
-
- public sealed class BooActionEvent : PerformActionEvent { }
}
diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs
index 893ff986a0..a31854723e 100644
--- a/Content.Server/Ghost/GhostSystem.cs
+++ b/Content.Server/Ghost/GhostSystem.cs
@@ -7,7 +7,6 @@ using Content.Server.Mind.Components;
using Content.Server.Players;
using Content.Server.Visible;
using Content.Server.Warps;
-using Content.Shared.Actions;
using Content.Shared.Examine;
using Content.Shared.Follower;
using Content.Shared.Ghost;
@@ -16,6 +15,10 @@ using Content.Shared.Movement.EntitySystems;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Content.Server.Ghost
@@ -27,9 +30,7 @@ namespace Content.Server.Ghost
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
- [Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
- [Dependency] private readonly IEntityLookup _lookup = default!;
[Dependency] private readonly FollowerSystem _followerSystem = default!;
public override void Initialize()
@@ -50,30 +51,6 @@ namespace Content.Server.Ghost
SubscribeNetworkEvent(OnGhostReturnToBodyRequest);
SubscribeNetworkEvent(OnGhostWarpToLocationRequest);
SubscribeNetworkEvent(OnGhostWarpToTargetRequest);
-
- SubscribeLocalEvent(OnActionPerform);
- }
- private void OnActionPerform(EntityUid uid, GhostComponent component, BooActionEvent args)
- {
- if (args.Handled)
- return;
-
- var ents = _lookup.GetEntitiesInRange(args.Performer, component.BooRadius);
-
- var booCounter = 0;
- foreach (var ent in ents)
- {
- var ghostBoo = new GhostBooEvent();
- RaiseLocalEvent(ent, ghostBoo);
-
- if (ghostBoo.Handled)
- booCounter++;
-
- if (booCounter >= component.BooMaxTargets)
- break;
- }
-
- args.Handled = true;
}
private void OnRelayMoveInput(EntityUid uid, GhostOnMoveComponent component, RelayMoveInputEvent args)
@@ -101,8 +78,6 @@ namespace Content.Server.Ghost
}
component.TimeOfDeath = _gameTiming.RealTime;
-
- _actions.AddAction(uid, component.Action, null);
}
private void OnGhostShutdown(EntityUid uid, GhostComponent component, ComponentShutdown args)
@@ -123,8 +98,6 @@ namespace Content.Server.Ghost
{
eye.VisibilityMask &= ~(uint) VisibilityFlags.Ghost;
}
-
- _actions.RemoveAction(uid, component.Action);
}
}
diff --git a/Content.Server/Guardian/GuardianHostComponent.cs b/Content.Server/Guardian/GuardianHostComponent.cs
index dfd108d523..9771087e21 100644
--- a/Content.Server/Guardian/GuardianHostComponent.cs
+++ b/Content.Server/Guardian/GuardianHostComponent.cs
@@ -1,7 +1,6 @@
-using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
using Robust.Shared.Containers;
-using Robust.Shared.Utility;
+using Robust.Shared.GameObjects;
+using Robust.Shared.ViewVariables;
namespace Content.Server.Guardian
{
@@ -23,17 +22,5 @@ namespace Content.Server.Guardian
/// Container which holds the guardian
///
[ViewVariables] public ContainerSlot GuardianContainer = default!;
-
- [DataField("action")]
- public InstantAction Action = new()
- {
- Name = "action-name-guardian",
- Description = "action-description-guardian",
- Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/manifest.png")),
- UseDelay = TimeSpan.FromSeconds(2),
- Event = new GuardianToggleActionEvent(),
- };
}
-
- public sealed class GuardianToggleActionEvent : PerformActionEvent { };
}
diff --git a/Content.Server/Guardian/GuardianSystem.cs b/Content.Server/Guardian/GuardianSystem.cs
index c766e3c967..0a01eedf48 100644
--- a/Content.Server/Guardian/GuardianSystem.cs
+++ b/Content.Server/Guardian/GuardianSystem.cs
@@ -1,7 +1,9 @@
+using Content.Server.Actions;
using Content.Server.DoAfter;
using Content.Server.Hands.Components;
using Content.Server.Popups;
using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
using Content.Shared.Audio;
using Content.Shared.Damage;
using Content.Shared.Examine;
@@ -11,6 +13,10 @@ using Content.Shared.MobState;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Log;
using Robust.Shared.Player;
using Robust.Shared.Utility;
@@ -24,7 +30,6 @@ namespace Content.Server.Guardian
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly DamageableSystem _damageSystem = default!;
- [Dependency] private readonly SharedActionsSystem _actionSystem = default!;
public override void Initialize()
{
@@ -45,22 +50,9 @@ namespace Content.Server.Guardian
SubscribeLocalEvent(OnHostStateChange);
SubscribeLocalEvent(OnHostShutdown);
- SubscribeLocalEvent(OnPerformAction);
-
SubscribeLocalEvent(OnGuardianAttackAttempt);
}
- private void OnPerformAction(EntityUid uid, GuardianHostComponent component, GuardianToggleActionEvent args)
- {
- if (args.Handled)
- return;
-
- if (component.HostedGuardian != null)
- ToggleGuardian(component);
-
- args.Handled = true;
- }
-
private void OnGuardianUnplayer(EntityUid uid, GuardianComponent component, PlayerDetachedEvent args)
{
var host = component.Host;
@@ -82,14 +74,12 @@ namespace Content.Server.Guardian
private void OnHostInit(EntityUid uid, GuardianHostComponent component, ComponentInit args)
{
component.GuardianContainer = uid.EnsureContainer("GuardianContainer");
- _actionSystem.AddAction(uid, component.Action, null);
}
private void OnHostShutdown(EntityUid uid, GuardianHostComponent component, ComponentShutdown args)
{
if (component.HostedGuardian == null) return;
EntityManager.QueueDeleteEntity(component.HostedGuardian.Value);
- _actionSystem.RemoveAction(uid, component.Action);
}
private void OnGuardianAttackAttempt(EntityUid uid, GuardianComponent component, AttackAttemptEvent args)
@@ -161,6 +151,9 @@ namespace Content.Server.Guardian
return;
}
+ // Can't work without actions
+ EntityManager.EnsureComponent(target);
+
if (component.Injecting) return;
component.Injecting = true;
@@ -182,7 +175,8 @@ namespace Content.Server.Guardian
comp.Used ||
!TryComp(ev.User, out var hands) ||
!hands.IsHolding(comp.Owner) ||
- HasComp(ev.Target))
+ HasComp(ev.Target) ||
+ !TryComp(ev.Target, out var actions))
{
comp.Injecting = false;
return;
@@ -200,6 +194,8 @@ namespace Content.Server.Guardian
{
guardianComponent.Host = ev.Target;
+ // Grant the user the recall action and notify them
+ actions.Grant(ActionType.ManifestGuardian);
SoundSystem.Play(Filter.Entities(ev.Target), "/Audio/Effects/guardian_inject.ogg", ev.Target);
_popupSystem.PopupEntity(Loc.GetString("guardian-created"), ev.Target, Filter.Entities(ev.Target));
diff --git a/Content.Server/Hands/Components/HandsComponent.cs b/Content.Server/Hands/Components/HandsComponent.cs
index d89ef55e46..99c1f2ccb9 100644
--- a/Content.Server/Hands/Components/HandsComponent.cs
+++ b/Content.Server/Hands/Components/HandsComponent.cs
@@ -13,19 +13,27 @@ using Content.Shared.Popups;
using Content.Shared.Pulling.Components;
using Content.Shared.Sound;
using Robust.Shared.Audio;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
using Robust.Shared.Player;
+using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Hands.Components
{
[RegisterComponent]
[ComponentReference(typeof(SharedHandsComponent))]
#pragma warning disable 618
- public sealed class HandsComponent : SharedHandsComponent, IBodyPartAdded, IBodyPartRemoved
+ public sealed class HandsComponent : SharedHandsComponent, IBodyPartAdded, IBodyPartRemoved, IDisarmedAct
#pragma warning restore 618
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IEntityManager _entities = default!;
+ [DataField("disarmedSound")] SoundSpecifier _disarmedSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
+
+ int IDisarmedAct.Priority => int.MaxValue; // We want this to be the last disarm act to run.
+
#region Pull/Disarm
void IBodyPartAdded.BodyPartAdded(BodyPartAddedEventArgs args)
@@ -54,10 +62,33 @@ namespace Content.Server.Hands.Components
RemoveHand(args.Slot);
}
- public bool BreakPulls()
+ bool IDisarmedAct.Disarmed(DisarmedActEvent @event)
+ {
+ if (BreakPulls())
+ return false;
+
+ var source = @event.Source;
+ var target = @event.Target;
+
+ SoundSystem.Play(Filter.Pvs(source), _disarmedSound.GetSound(), source, AudioHelpers.WithVariation(0.025f));
+
+ if (ActiveHand != null && Drop(ActiveHand, false))
+ {
+ source.PopupMessageOtherClients(Loc.GetString("hands-component-disarm-success-others-message", ("disarmer", _entities.GetComponent(source).EntityName), ("disarmed", _entities.GetComponent(target).EntityName)));
+ source.PopupMessageCursor(Loc.GetString("hands-component-disarm-success-message", ("disarmed", _entities.GetComponent(target).EntityName)));
+ }
+ else
+ {
+ source.PopupMessageOtherClients(Loc.GetString("hands-component-shove-success-others-message", ("shover", _entities.GetComponent(source).EntityName), ("shoved", _entities.GetComponent(target).EntityName)));
+ source.PopupMessageCursor(Loc.GetString("hands-component-shove-success-message", ("shoved", _entities.GetComponent(target).EntityName)));
+ }
+
+ return true;
+ }
+
+ private bool BreakPulls()
{
// What is this API??
- // I just wanted to do actions not deal with this shit...
if (!_entities.TryGetComponent(Owner, out SharedPullerComponent? puller)
|| puller.Pulling is not {Valid: true} pulling || !_entities.TryGetComponent(puller.Pulling.Value, out SharedPullableComponent? pullable))
return false;
diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs
index cc892205d9..b20f039ca4 100644
--- a/Content.Server/Hands/Systems/HandsSystem.cs
+++ b/Content.Server/Hands/Systems/HandsSystem.cs
@@ -1,12 +1,9 @@
using System.Linq;
-using Content.Server.Act;
using Content.Server.Administration.Logs;
using Content.Server.Hands.Components;
-using Content.Server.Popups;
using Content.Server.Stack;
using Content.Server.Storage.Components;
using Content.Server.Strip;
-using Content.Server.Stunnable;
using Content.Server.Throwing;
using Content.Shared.ActionBlocker;
using Content.Shared.Database;
@@ -21,7 +18,6 @@ using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
-using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Input.Binding;
@@ -42,7 +38,6 @@ namespace Content.Server.Hands.Systems
[Dependency] private readonly AdminLogSystem _logSystem = default!;
[Dependency] private readonly StrippableSystem _strippableSystem = default!;
[Dependency] private readonly SharedHandVirtualItemSystem _virtualSystem = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
public override void Initialize()
{
@@ -53,7 +48,6 @@ namespace Content.Server.Hands.Systems
SubscribeNetworkEvent(HandleInteractUsingInHand);
SubscribeNetworkEvent(HandleUseInHand);
SubscribeNetworkEvent(HandleMoveItemFromHand);
- SubscribeLocalEvent(OnDisarmed, before: new[] { typeof(StunSystem) });
SubscribeLocalEvent(HandlePullAttempt);
SubscribeLocalEvent(HandlePullStarted);
@@ -82,26 +76,6 @@ namespace Content.Server.Hands.Systems
args.State = new HandsComponentState(hands.Hands, hands.ActiveHand);
}
- private void OnDisarmed(EntityUid uid, HandsComponent component, DisarmedEvent args)
- {
- if (args.Handled || component.BreakPulls())
- return;
-
- if (component.ActiveHand == null || !component.Drop(component.ActiveHand, false))
- return;
-
- var targetName = Name(args.Target);
-
- var msgOther = Loc.GetString("hands-component-disarm-success-others-message", ("disarmer", Name(args.Source)), ("disarmed", targetName));
- var msgUser = Loc.GetString("hands-component-disarm-success-message", ("disarmed", targetName));
-
- var filter = Filter.Pvs(args.Source).RemoveWhereAttachedEntity(e => e == args.Source);
- _popupSystem.PopupEntity(msgOther, args.Source, filter);
- _popupSystem.PopupEntity(msgUser, args.Source, Filter.Entities(args.Source));
-
- args.Handled = true; // no shove/stun.
- }
-
#region EntityInsertRemove
public override void RemoveHeldEntityFromHand(EntityUid uid, Hand hand, SharedHandsComponent? hands = null)
{
diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs
index 6a7640880e..e9d57e70e6 100644
--- a/Content.Server/IoC/ServerContentIoC.cs
+++ b/Content.Server/IoC/ServerContentIoC.cs
@@ -35,6 +35,7 @@ namespace Content.Server.IoC
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
+ IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
diff --git a/Content.Server/Light/Components/HandheldLightComponent.cs b/Content.Server/Light/Components/HandheldLightComponent.cs
index 5e7477f854..a24dba0fd2 100644
--- a/Content.Server/Light/Components/HandheldLightComponent.cs
+++ b/Content.Server/Light/Components/HandheldLightComponent.cs
@@ -2,6 +2,7 @@ using System.Threading.Tasks;
using Content.Server.Clothing.Components;
using Content.Server.Light.EntitySystems;
using Content.Shared.ActionBlocker;
+using Content.Shared.Actions.Behaviors.Item;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Light.Component;
@@ -47,4 +48,17 @@ namespace Content.Server.Light.Components
///
public byte? LastLevel;
}
+
+ [UsedImplicitly]
+ [DataDefinition]
+ public sealed class ToggleLightAction : IToggleItemAction
+ {
+ public bool DoToggleAction(ToggleItemActionEventArgs args)
+ {
+ if (!EntitySystem.Get().CanInteract(args.Performer, args.Item)) return false;
+ if (!IoCManager.Resolve().TryGetComponent(args.Item, out var lightComponent)) return false;
+ if (lightComponent.Activated == args.ToggledOn) return false;
+ return EntitySystem.Get().ToggleStatus(args.Performer, lightComponent);
+ }
+ }
}
diff --git a/Content.Server/Light/Components/UnpoweredFlashlightComponent.cs b/Content.Server/Light/Components/UnpoweredFlashlightComponent.cs
index 1dc8fbe162..dcfe866498 100644
--- a/Content.Server/Light/Components/UnpoweredFlashlightComponent.cs
+++ b/Content.Server/Light/Components/UnpoweredFlashlightComponent.cs
@@ -1,5 +1,7 @@
-using Content.Shared.Actions.ActionTypes;
using Content.Shared.Sound;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
namespace Content.Server.Light.Components
{
@@ -14,8 +16,5 @@ namespace Content.Server.Light.Components
public SoundSpecifier ToggleSound = new SoundPathSpecifier("/Audio/Items/flashlight_pda.ogg");
[ViewVariables] public bool LightOn = false;
-
- [DataField("toggleAction", required: true)]
- public InstantAction ToggleAction = default!;
}
}
diff --git a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs
index 2bbcf5770c..2b462bb696 100644
--- a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs
+++ b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs
@@ -1,19 +1,25 @@
-using Content.Server.Actions;
+using System.Collections.Generic;
+using Content.Server.Clothing.Components;
using Content.Server.Light.Components;
using Content.Server.Popups;
using Content.Server.PowerCell;
+using Content.Shared.ActionBlocker;
using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.Light.Component;
using Content.Shared.Rounding;
-using Content.Shared.Toggleable;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
+using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Utility;
@@ -24,7 +30,6 @@ namespace Content.Server.Light.EntitySystems
{
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
- [Dependency] private readonly ActionsSystem _actionSystem = default!;
// TODO: Ideally you'd be able to subscribe to power stuff to get events at certain percentages.. or something?
// But for now this will be better anyway.
@@ -42,27 +47,6 @@ namespace Content.Server.Light.EntitySystems
SubscribeLocalEvent>(AddToggleLightVerb);
SubscribeLocalEvent(OnActivate);
-
- SubscribeLocalEvent(OnGetActions);
- SubscribeLocalEvent(OnToggleAction);
- }
-
- private void OnGetActions(EntityUid uid, HandheldLightComponent component, GetActionsEvent args)
- {
- args.Actions.Add(component.ToggleAction);
- }
-
- private void OnToggleAction(EntityUid uid, HandheldLightComponent component, ToggleActionEvent args)
- {
- if (args.Handled)
- return;
-
- if (component.Activated)
- TurnOff(component);
- else
- TurnOn(args.Performer, component);
-
- args.Handled = true;
}
private void OnGetState(EntityUid uid, HandheldLightComponent component, ref ComponentGetState args)
@@ -171,6 +155,7 @@ namespace Content.Server.Light.EntitySystems
SetState(component, false);
component.Activated = false;
+ UpdateLightAction(component);
_activeLights.Remove(component);
component.LastLevel = null;
component.Dirty(EntityManager);
@@ -189,6 +174,7 @@ namespace Content.Server.Light.EntitySystems
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOnFailSound.GetSound(), component.Owner);
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), component.Owner, Filter.Entities(user));
+ UpdateLightAction(component);
return false;
}
@@ -199,10 +185,12 @@ namespace Content.Server.Light.EntitySystems
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOnFailSound.GetSound(), component.Owner);
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-dead-message"), component.Owner, Filter.Entities(user));
+ UpdateLightAction(component);
return false;
}
component.Activated = true;
+ UpdateLightAction(component);
SetState(component, true);
_activeLights.Add(component);
component.LastLevel = GetLevel(component);
@@ -229,8 +217,13 @@ namespace Content.Server.Light.EntitySystems
{
item.EquippedPrefix = on ? "on" : "off";
}
+ }
- _actionSystem.SetToggled(component.ToggleAction, on);
+ private void UpdateLightAction(HandheldLightComponent component)
+ {
+ if (!EntityManager.TryGetComponent(component.Owner, out ItemActionsComponent? actions)) return;
+
+ actions.Toggle(ItemActionType.ToggleLight, component.Activated);
}
public void TryUpdate(HandheldLightComponent component, float frameTime)
diff --git a/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs b/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs
index 682e387da8..a8aabcb31a 100644
--- a/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs
+++ b/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs
@@ -1,41 +1,24 @@
using Content.Server.Light.Components;
using Content.Server.Light.Events;
-using Content.Shared.Actions;
using Content.Shared.Light;
-using Content.Shared.Toggleable;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Localization;
using Robust.Shared.Player;
+using System;
+using Robust.Shared.IoC;
namespace Content.Server.Light.EntitySystems
{
public sealed class UnpoweredFlashlightSystem : EntitySystem
{
- [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
-
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent>(AddToggleLightVerbs);
- SubscribeLocalEvent(OnGetActions);
- SubscribeLocalEvent(OnToggleAction);
- }
-
- private void OnToggleAction(EntityUid uid, UnpoweredFlashlightComponent component, ToggleActionEvent args)
- {
- if (args.Handled)
- return;
-
- ToggleLight(component);
-
- args.Handled = true;
- }
-
- private void OnGetActions(EntityUid uid, UnpoweredFlashlightComponent component, GetActionsEvent args)
- {
- args.Actions.Add(component.ToggleAction);
}
private void AddToggleLightVerbs(EntityUid uid, UnpoweredFlashlightComponent component, GetVerbsEvent args)
@@ -66,7 +49,7 @@ namespace Content.Server.Light.EntitySystems
SoundSystem.Play(Filter.Pvs(light.Owner), flashlight.ToggleSound.GetSound(), flashlight.Owner);
RaiseLocalEvent(flashlight.Owner, new LightToggleEvent(flashlight.LightOn));
- _actionsSystem.SetToggled(flashlight.ToggleAction, flashlight.LightOn);
}
+
}
}
diff --git a/Content.Server/PAI/PAISystem.cs b/Content.Server/PAI/PAISystem.cs
index 24462c89c0..eccdd40908 100644
--- a/Content.Server/PAI/PAISystem.cs
+++ b/Content.Server/PAI/PAISystem.cs
@@ -13,7 +13,6 @@ using Robust.Shared.Log;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Player;
-using Content.Shared.Actions;
namespace Content.Server.PAI
{
@@ -21,7 +20,6 @@ namespace Content.Server.PAI
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly InstrumentSystem _instrumentSystem = default!;
- [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
public override void Initialize()
{
@@ -32,21 +30,6 @@ namespace Content.Server.PAI
SubscribeLocalEvent(OnMindAdded);
SubscribeLocalEvent(OnMindRemoved);
SubscribeLocalEvent>(AddWipeVerb);
-
- SubscribeLocalEvent(OnStartup);
- SubscribeLocalEvent(OnShutdown);
- }
-
- private void OnStartup(EntityUid uid, PAIComponent component, ComponentStartup args)
- {
- if (component.MidiAction != null)
- _actionsSystem.AddAction(uid, component.MidiAction, null);
- }
-
- private void OnShutdown(EntityUid uid, PAIComponent component, ComponentShutdown args)
- {
- if (component.MidiAction != null)
- _actionsSystem.RemoveAction(uid, component.MidiAction);
}
private void OnExamined(EntityUid uid, PAIComponent component, ExaminedEvent args)
diff --git a/Content.Server/Speech/Components/VocalComponent.cs b/Content.Server/Speech/Components/VocalComponent.cs
deleted file mode 100644
index 6f84290d33..0000000000
--- a/Content.Server/Speech/Components/VocalComponent.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
-using Content.Shared.Sound;
-using Robust.Shared.Audio;
-using Robust.Shared.Utility;
-
-
-namespace Content.Server.Speech.Components;
-
-///
-/// Component required for entities to be able to scream.
-///
-[RegisterComponent]
-public sealed class VocalComponent : Component
-{
- [DataField("maleScream")]
- public SoundSpecifier MaleScream = new SoundCollectionSpecifier("MaleScreams");
-
- [DataField("femaleScream")]
- public SoundSpecifier FemaleScream = new SoundCollectionSpecifier("FemaleScreams");
-
- [DataField("wilhelm")]
- public SoundSpecifier Wilhelm = new SoundPathSpecifier("/Audio/Voice/Human/wilhelm_scream.ogg");
-
- [DataField("audioParams")]
- public AudioParams AudioParams = AudioParams.Default.WithVolume(4f);
-
- public const float Variation = 0.125f;
-
- // Not using the in-build sound support for actions, given that the sound is modified non-prototype specific factors like gender.
- [DataField("action", required: true)]
- public InstantAction Action = new()
- {
- UseDelay = TimeSpan.FromSeconds(10),
- Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/scream.png")),
- Name = "action-name-scream",
- Description = "AAAAAAAAAAAAAAAAAAAAAAAAA",
- Event = new ScreamActionEvent(),
- };
-}
-
-public sealed class ScreamActionEvent : PerformActionEvent { };
diff --git a/Content.Server/Speech/VocalSystem.cs b/Content.Server/Speech/VocalSystem.cs
deleted file mode 100644
index 39ae115f4f..0000000000
--- a/Content.Server/Speech/VocalSystem.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using Content.Server.Speech.Components;
-using Content.Shared.ActionBlocker;
-using Content.Shared.Actions;
-using Content.Shared.CharacterAppearance;
-using Content.Shared.CharacterAppearance.Components;
-using Robust.Shared.Audio;
-using Robust.Shared.Player;
-using Robust.Shared.Random;
-
-namespace Content.Server.Speech;
-
-///
-/// Fer Screamin
-///
-///
-/// Or I guess other vocalizations, like laughing. If fun is ever legalized on the station.
-///
-public sealed class VocalSystem : EntitySystem
-{
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly SharedActionsSystem _actions = default!;
- [Dependency] private readonly ActionBlockerSystem _blocker = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnActionPerform);
- SubscribeLocalEvent(OnStartup);
- SubscribeLocalEvent(OnShutdown);
- }
-
- private void OnStartup(EntityUid uid, VocalComponent component, ComponentStartup args)
- {
- _actions.AddAction(uid, component.Action, null);
- }
-
- private void OnShutdown(EntityUid uid, VocalComponent component, ComponentShutdown args)
- {
- _actions.RemoveAction(uid, component.Action);
- }
-
- private void OnActionPerform(EntityUid uid, VocalComponent component, ScreamActionEvent args)
- {
- if (args.Handled)
- return;
-
- args.Handled = TryScream(uid, component);
- }
-
- public bool TryScream(EntityUid uid, VocalComponent? component = null)
- {
- if (!Resolve(uid, ref component, false))
- return false;
-
- if (!_blocker.CanSpeak(uid))
- return false;
-
- // Currently this requires humanoid appearance & doesn't have any sort of fall-back or gender-neutral scream.
- if (!TryComp(uid, out HumanoidAppearanceComponent? humanoid))
- return false;
-
- if (_random.Prob(.01f))
- {
- SoundSystem.Play(Filter.Pvs(uid), component.Wilhelm.GetSound(), uid, component.AudioParams);
- return true;
- }
-
- var scale = (float) _random.NextGaussian(1, VocalComponent.Variation);
- var pitchedParams = component.AudioParams.WithPitchScale(scale);
-
- switch (humanoid.Sex)
- {
- case Sex.Male:
- SoundSystem.Play(Filter.Pvs(uid), component.MaleScream.GetSound(), uid, pitchedParams);
- break;
- case Sex.Female:
- SoundSystem.Play(Filter.Pvs(uid), component.FemaleScream.GetSound(), uid, pitchedParams);
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
-
- return true;
- }
-}
diff --git a/Content.Server/Stunnable/StunSystem.cs b/Content.Server/Stunnable/StunSystem.cs
index 8a5e22bcc2..a9cc74eea2 100644
--- a/Content.Server/Stunnable/StunSystem.cs
+++ b/Content.Server/Stunnable/StunSystem.cs
@@ -26,10 +26,10 @@ namespace Content.Server.Stunnable
{
base.Initialize();
- SubscribeLocalEvent(OnDisarmed);
+ SubscribeLocalEvent(OnDisarmed);
}
- private void OnDisarmed(EntityUid uid, StatusEffectsComponent status, DisarmedEvent args)
+ private void OnDisarmed(EntityUid uid, StatusEffectsComponent status, DisarmedActEvent args)
{
if (args.Handled || !_random.Prob(args.PushProbability))
return;
diff --git a/Content.Server/UserInterface/ActivatableUISystem.cs b/Content.Server/UserInterface/ActivatableUISystem.cs
index f88cb94bd9..eb9959aeff 100644
--- a/Content.Server/UserInterface/ActivatableUISystem.cs
+++ b/Content.Server/UserInterface/ActivatableUISystem.cs
@@ -1,7 +1,5 @@
using Content.Server.Administration.Managers;
using Content.Server.Ghost.Components;
-using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
using Content.Shared.Hands;
using Content.Shared.Interaction;
using Content.Shared.Verbs;
@@ -23,29 +21,12 @@ namespace Content.Server.UserInterface
SubscribeLocalEvent(OnActivate);
SubscribeLocalEvent(OnUseInHand);
SubscribeLocalEvent((uid, aui, _) => CloseAll(uid, aui));
- SubscribeLocalEvent((uid, aui, _) => CloseAll(uid, aui));
+ SubscribeLocalEvent((uid, aui, _) => CloseAll(uid, aui));
// *THIS IS A BLATANT WORKAROUND!* RATIONALE: Microwaves need it
SubscribeLocalEvent(OnParentChanged);
SubscribeLocalEvent(OnUIClose);
SubscribeLocalEvent>(AddOpenUiVerb);
-
- SubscribeLocalEvent(OnActionPerform);
- }
-
- private void OnActionPerform(EntityUid uid, ServerUserInterfaceComponent component, OpenUiActionEvent args)
- {
- if (args.Handled || args.Key == null)
- return;
-
- if (!TryComp(args.Performer, out ActorComponent? actor))
- return;
-
- if (!component.TryGetBoundUserInterface(args.Key, out var bui))
- return;
-
- bui.Toggle(actor.PlayerSession);
- args.Handled = true;
}
private void AddOpenUiVerb(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent args)
diff --git a/Content.Server/UserInterface/OpenUiActionEvent.cs b/Content.Server/UserInterface/OpenUiActionEvent.cs
deleted file mode 100644
index e7ea6174b7..0000000000
--- a/Content.Server/UserInterface/OpenUiActionEvent.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Content.Shared.Actions;
-using Robust.Shared.Reflection;
-using Robust.Shared.Serialization;
-
-namespace Content.Server.UserInterface;
-
-public sealed class OpenUiActionEvent : PerformActionEvent, ISerializationHooks
-{
- [ViewVariables]
- public Enum? Key { get; set; }
-
- [DataField("key", readOnly: true, required: true)]
- private string _keyRaw = default!;
-
- void ISerializationHooks.AfterDeserialization()
- {
- var reflectionManager = IoCManager.Resolve();
- if (reflectionManager.TryParseEnumReference(_keyRaw, out var key))
- Key = key;
- else
- Logger.Error($"Invalid UI key ({_keyRaw}) in open-UI action");
- }
-}
diff --git a/Content.Server/Wieldable/WieldableSystem.cs b/Content.Server/Wieldable/WieldableSystem.cs
index 31ee3065dc..d993bc7eba 100644
--- a/Content.Server/Wieldable/WieldableSystem.cs
+++ b/Content.Server/Wieldable/WieldableSystem.cs
@@ -28,7 +28,7 @@ namespace Content.Server.Wieldable
SubscribeLocalEvent(OnUseInHand);
SubscribeLocalEvent(OnItemWielded);
SubscribeLocalEvent(OnItemUnwielded);
- SubscribeLocalEvent(OnItemLeaveHand);
+ SubscribeLocalEvent(OnItemLeaveHand);
SubscribeLocalEvent(OnVirtualItemDeleted);
SubscribeLocalEvent>(AddToggleWieldVerb);
@@ -208,7 +208,7 @@ namespace Content.Server.Wieldable
_virtualItemSystem.DeleteInHandsMatching(args.User.Value, uid);
}
- private void OnItemLeaveHand(EntityUid uid, WieldableComponent component, GotUnequippedHandEvent args)
+ private void OnItemLeaveHand(EntityUid uid, WieldableComponent component, UnequippedHandEvent args)
{
if (!component.Wielded || component.Owner != args.Unequipped)
return;
diff --git a/Content.Shared.Database/LogType.cs b/Content.Shared.Database/LogType.cs
index 24990cbea8..e98818d52a 100644
--- a/Content.Shared.Database/LogType.cs
+++ b/Content.Shared.Database/LogType.cs
@@ -63,6 +63,5 @@ public enum LogType
Emitter = 59,
GhostRoleTaken = 60,
Chat = 61,
- Action = 62,
Emag = 69,
}
diff --git a/Content.Shared/Actions/ActionEvents.cs b/Content.Shared/Actions/ActionEvents.cs
deleted file mode 100644
index 0a28d470b1..0000000000
--- a/Content.Shared/Actions/ActionEvents.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Content.Shared.Actions.ActionTypes;
-using Robust.Shared.Map;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Actions;
-
-public sealed class GetActionsEvent : EntityEventArgs
-{
- public SortedSet Actions = new();
-}
-
-///
-/// Event used to communicate with the client that the user wishes to perform some action.
-///
-///
-/// Basically a wrapper for that the action system will validate before performing
-/// (check cooldown, target, enabling-entity)
-///
-[Serializable, NetSerializable]
-public sealed class RequestPerformActionEvent : EntityEventArgs
-{
- public readonly ActionType Action;
- public readonly EntityUid? EntityTarget;
- public readonly MapCoordinates? MapTarget;
-
- public RequestPerformActionEvent(InstantAction action)
- {
- Action = action;
- }
-
- public RequestPerformActionEvent(EntityTargetAction action, EntityUid entityTarget)
- {
- Action = action;
- EntityTarget = entityTarget;
- }
-
- public RequestPerformActionEvent(WorldTargetAction action, MapCoordinates mapTarget)
- {
- Action = action;
- MapTarget = mapTarget;
- }
-}
-
-[ImplicitDataDefinitionForInheritors]
-public abstract class PerformActionEvent : HandledEntityEventArgs
-{
- ///
- /// The user performing the action
- ///
- public EntityUid Performer;
-}
-
-public abstract class PerformEntityTargetActionEvent : PerformActionEvent
-{
- public EntityUid Target;
-}
-
-public abstract class PerformWorldTargetActionEvent : PerformActionEvent
-{
- public MapCoordinates Target;
-}
diff --git a/Content.Shared/Actions/ActionManager.cs b/Content.Shared/Actions/ActionManager.cs
new file mode 100644
index 0000000000..8166f79dc2
--- /dev/null
+++ b/Content.Shared/Actions/ActionManager.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Shared.Actions.Prototypes;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Actions
+{
+ ///
+ /// Provides access to all configured actions by action type.
+ ///
+ public sealed class ActionManager
+ {
+ [Dependency]
+ private readonly IPrototypeManager _prototypeManager = default!;
+
+ private readonly Dictionary _typeToAction = new();
+ private readonly Dictionary _typeToItemAction = new();
+
+ public void Initialize()
+ {
+ foreach (var action in _prototypeManager.EnumeratePrototypes())
+ {
+ if (!_typeToAction.TryAdd(action.ActionType, action))
+ {
+ Logger.ErrorS("action",
+ "Found action with duplicate actionType {0} - all actions must have" +
+ " a unique actionType, this one will be skipped", action.ActionType);
+ }
+ }
+
+ foreach (var action in _prototypeManager.EnumeratePrototypes())
+ {
+ if (!_typeToItemAction.TryAdd(action.ActionType, action))
+ {
+ Logger.ErrorS("action",
+ "Found itemAction with duplicate actionType {0} - all actions must have" +
+ " a unique actionType, this one will be skipped", action.ActionType);
+ }
+ }
+ }
+
+ /// all action prototypes of all types
+ public IEnumerable EnumerateActions()
+ {
+ return _typeToAction.Values.Concat(_typeToItemAction.Values);
+ }
+
+
+ ///
+ /// Tries to get the action of the indicated type
+ ///
+ /// true if found
+ public bool TryGet(ActionType actionType, [NotNullWhen(true)] out ActionPrototype? action)
+ {
+ return _typeToAction.TryGetValue(actionType, out action);
+ }
+
+ ///
+ /// Tries to get the item action of the indicated type
+ ///
+ /// true if found
+ public bool TryGet(ItemActionType actionType, [NotNullWhen(true)] out ItemActionPrototype? action)
+ {
+ return _typeToItemAction.TryGetValue(actionType, out action);
+ }
+ }
+}
diff --git a/Content.Shared/Actions/ActionType.cs b/Content.Shared/Actions/ActionType.cs
new file mode 100644
index 0000000000..4eb19eaf34
--- /dev/null
+++ b/Content.Shared/Actions/ActionType.cs
@@ -0,0 +1,41 @@
+namespace Content.Shared.Actions
+{
+ ///
+ /// Every possible action. Corresponds to actionType in action prototypes.
+ ///
+ public enum ActionType : byte
+ {
+ Error,
+ HumanScream,
+ VoxScream,
+ CombatMode,
+ Disarm,
+ GhostBoo,
+ DebugInstant,
+ DebugToggle,
+ DebugTargetPoint,
+ DebugTargetPointRepeat,
+ DebugTargetEntity,
+ DebugTargetEntityRepeat,
+ SpellPie,
+ ManifestGuardian,
+ PAIMidi
+ }
+
+ ///
+ /// Every possible item action. Corresponds to actionType in itemAction prototypes.
+ ///
+ public enum ItemActionType : byte
+ {
+ Error,
+ ToggleInternals,
+ ToggleLight,
+ ToggleMagboots,
+ DebugInstant,
+ DebugToggle,
+ DebugTargetPoint,
+ DebugTargetPointRepeat,
+ DebugTargetEntity,
+ DebugTargetEntityRepeat
+ }
+}
diff --git a/Content.Shared/Actions/ActionTypes/ActionType.cs b/Content.Shared/Actions/ActionTypes/ActionType.cs
deleted file mode 100644
index 9166dd1d32..0000000000
--- a/Content.Shared/Actions/ActionTypes/ActionType.cs
+++ /dev/null
@@ -1,268 +0,0 @@
-using Content.Shared.Sound;
-using Robust.Shared.Audio;
-using Robust.Shared.Serialization;
-using Robust.Shared.Utility;
-
-namespace Content.Shared.Actions.ActionTypes;
-
-[DataDefinition]
-[ImplicitDataDefinitionForInheritors]
-[Serializable, NetSerializable]
-public abstract class ActionType : IEquatable, IComparable, ICloneable
-{
- ///
- /// Icon representing this action in the UI.
- ///
- [DataField("icon")]
- public SpriteSpecifier? Icon;
-
- ///
- /// For toggle actions only, icon to show when toggled on. If omitted, the action will simply be highlighted
- /// when turned on.
- ///
- [DataField("iconOn")]
- public SpriteSpecifier? IconOn;
-
- ///
- /// If not null, this color will modulate the action icon color.
- ///
- ///
- /// This currently only exists for decal-placement actions, so that the action icons correspond to the color of
- /// the decal. But this is probably useful for other actions, including maybe changing color on toggle.
- ///
- [DataField("iconColor")]
- public Color IconColor = Color.White;
-
- ///
- /// Name to show in UI.
- ///
- [DataField("name", required: true)]
- public string Name = string.Empty;
-
- ///
- /// Description to show in UI. Accepts formatting.
- ///
- [DataField("description")]
- public string Description = string.Empty;
-
- ///
- /// Keywords that can be used to search for this action in the action menu.
- ///
- [DataField("keywords")]
- public HashSet Keywords = new();
-
- ///
- /// Whether this action is currently enabled. If not enabled, this action cannot be performed.
- ///
- [DataField("enabled")]
- public bool Enabled = true;
-
- ///
- /// The toggle state of this action. Toggling switches the currently displayed icon, see and .
- ///
- ///
- /// The toggle can set directly via , but it will also be
- /// automatically toggled for targeted-actions while selecting a target.
- ///
- public bool Toggled;
-
- ///
- /// The current cooldown on the action.
- ///
- public (TimeSpan Start, TimeSpan End)? Cooldown;
-
- ///
- /// Time interval between action uses.
- ///
- [DataField("useDelay")]
- public TimeSpan? UseDelay;
-
- ///
- /// Convenience tool for actions with limited number of charges. Automatically decremented on use, and the
- /// action is disabled when it reaches zero. Does NOT automatically remove the action from the action bar.
- ///
- [DataField("charges")]
- public int? Charges;
-
- ///
- /// The entity that enables / provides this action. If the action is innate, this may be the user themselves. If
- /// this action has no provider (e.g., mapping tools), the this will result in broadcast events.
- ///
- public EntityUid? Provider;
-
- ///
- /// Whether the action system should block this action if the user cannot currently interact. Some spells or
- /// abilities may want to disable this and implement their own checks.
- ///
- [DataField("checkCanInteract")]
- public bool CheckCanInteract = true;
-
- ///
- /// If true, will simply execute the action locally without sending to the server.
- ///
- [DataField("clientExclusive")]
- public bool ClientExclusive = false;
-
- ///
- /// Determines the order in which actions are automatically added the action bar.
- ///
- [DataField("priority")]
- public int Priority = 0;
-
- ///
- /// What entity, if any, currently has this action in the actions component?
- ///
- [ViewVariables]
- public EntityUid? AttachedEntity;
-
- ///
- /// Whether or not to automatically add this action to the action bar when it becomes available.
- ///
- [DataField("autoPopulate")]
- public bool AutoPopulate = true;
-
- ///
- /// Temporary actions are removed from the action component when removed from the action-bar/GUI. Currently,
- /// should only be used for client-exclusive actions (server is not notified).
- ///
- ///
- /// Currently there is no way for a player to just voluntarily remove actions. They can hide them from the
- /// toolbar, but not actually remove them. This is undesirable for things like dynamically added mapping
- /// entity-selection actions, as the # of actions would just keep increasing.
- ///
- [DataField("temporary")]
- public bool Temporary;
-
- ///
- /// Determines the appearance of the entity-icon for actions that are enabled via some entity.
- ///
- [DataField("itemIconStyle")]
- public ItemActionIconStyle ItemIconStyle;
-
- ///
- /// If not null, the user will speak these words when performing the action. Convenient feature to have for some
- /// actions. Gets passed through localization.
- ///
- [DataField("speech")]
- public string? Speech;
-
- ///
- /// If not null, this sound will be played when performing this action.
- ///
- [DataField("sound")]
- public SoundSpecifier? Sound;
-
- [DataField("audioParams")]
- public AudioParams? AudioParams;
-
- ///
- /// A pop-up to show the user when performing this action. Gets passed through localization.
- ///
- [DataField("userPopup")]
- public string? UserPopup;
-
- ///
- /// A pop-up to show to all players when performing this action. Gets passed through localization.
- ///
- [DataField("popup")]
- public string? Popup;
-
- ///
- /// If not null, this string will be appended to the pop-up localization strings when the action was toggled on
- /// after execution. Exists to make it easy to have a different pop-up for turning the action on or off (e.g.,
- /// combat mode toggle).
- ///
- [DataField("popupToggleSuffix")]
- public string? PopupToggleSuffix = null;
-
- ///
- /// Compares two actions based on their properties. This is used to determine equality when the client requests the
- /// server to perform some action. Also determines the order in which actions are automatically added to the action bar.
- ///
- ///
- /// Basically: if an action has the same priority, name, and is enabled by the same entity, then the actions are considered equal.
- /// The entity-check is required to avoid toggling all flashlights simultaneously whenever a flashlight-hoarder uses an action.
- ///
- public virtual int CompareTo(object? obj)
- {
- if (obj is not ActionType otherAction)
- return -1;
-
- if (Priority != otherAction.Priority)
- return otherAction.Priority - Priority;
-
- var name = Loc.GetString(Name);
- var otherName = Loc.GetString(otherAction.Name);
- if (name != otherName)
- return string.Compare(name, otherName, StringComparison.CurrentCulture);
-
- if (Provider != otherAction.Provider)
- {
- if (Provider == null)
- return -1;
-
- if (otherAction.Provider == null)
- return 1;
-
- // uid to int casting... it says "Do NOT use this in content". You can't tell me what to do.
- return (int) Provider - (int) otherAction.Provider;
- }
-
- return 0;
- }
-
- ///
- /// Proper client-side state handling requires the ability to clone an action from the component state.
- /// Otherwise modifying the action can lead to modifying the stored server state.
- ///
- public abstract object Clone();
-
- public virtual void CopyFrom(object objectToClone)
- {
- if (objectToClone is not ActionType toClone)
- return;
-
- // This is pretty Ugly to look at. But actions are sent to the client in a component state, so they have to be
- // cloneable. Would be easy if this were a struct of only value-types, but I don't want to restrict actions like
- // that.
- Priority = toClone.Priority;
- Icon = toClone.Icon;
- IconOn = toClone.IconOn;
- Name = toClone.Name;
- Description = toClone.Description;
- Provider = toClone.Provider;
- AttachedEntity = toClone.AttachedEntity;
- Enabled = toClone.Enabled;
- Toggled = toClone.Toggled;
- Cooldown = toClone.Cooldown;
- Charges = toClone.Charges;
- Keywords = new(toClone.Keywords);
- AutoPopulate = toClone.AutoPopulate;
- ItemIconStyle = toClone.ItemIconStyle;
- CheckCanInteract = toClone.CheckCanInteract;
- Speech = toClone.Speech;
- UseDelay = toClone.UseDelay;
- Sound = toClone.Sound;
- AudioParams = toClone.AudioParams;
- UserPopup = toClone.UserPopup;
- Popup = toClone.Popup;
- PopupToggleSuffix = toClone.PopupToggleSuffix;
- ItemIconStyle = toClone.ItemIconStyle;
- }
-
- public bool Equals(ActionType? other)
- {
- return CompareTo(other) == 0;
- }
-
- public override int GetHashCode()
- {
- unchecked
- {
- var hashCode = Priority.GetHashCode();
- hashCode = (hashCode * 397) ^ Name.GetHashCode();
- hashCode = (hashCode * 397) ^ Provider.GetHashCode();
- return hashCode;
- }
- }
-}
diff --git a/Content.Shared/Actions/ActionTypes/InstantAction.cs b/Content.Shared/Actions/ActionTypes/InstantAction.cs
deleted file mode 100644
index 28a8b607f4..0000000000
--- a/Content.Shared/Actions/ActionTypes/InstantAction.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Actions.ActionTypes;
-
-///
-/// Instantaneous action with no extra targeting information. Will result in being raised.
-///
-[Serializable, NetSerializable]
-[Friend(typeof(SharedActionsSystem))]
-[Virtual]
-public class InstantAction : ActionType
-{
- ///
- /// The local-event to raise when this action is performed.
- ///
- [DataField("event")]
- [NonSerialized]
- public PerformActionEvent? Event;
-
- public InstantAction() { }
- public InstantAction(InstantAction toClone)
- {
- CopyFrom(toClone);
- }
-
- public override void CopyFrom(object objectToClone)
- {
- base.CopyFrom(objectToClone);
-
- if (objectToClone is not InstantAction toClone)
- return;
-
- // Events should be re-usable, and shouldn't be modified during prediction.
- Event = toClone.Event;
- }
-
- public override object Clone()
- {
- return new InstantAction(this);
- }
-}
diff --git a/Content.Shared/Actions/ActionTypes/TargetedAction.cs b/Content.Shared/Actions/ActionTypes/TargetedAction.cs
deleted file mode 100644
index a5ecd581d6..0000000000
--- a/Content.Shared/Actions/ActionTypes/TargetedAction.cs
+++ /dev/null
@@ -1,153 +0,0 @@
-using Content.Shared.Interaction;
-using Content.Shared.Whitelist;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Actions.ActionTypes;
-
-[Serializable, NetSerializable]
-public abstract class TargetedAction : ActionType
-{
- ///
- /// For entity- or map-targeting actions, if this is true the action will remain selected after it is used, so
- /// it can be continuously re-used. If this is false, the action will be deselected after one use.
- ///
- [DataField("repeat")]
- public bool Repeat;
-
- ///
- /// For entity- or map-targeting action, determines whether the action is deselected if the user doesn't click a valid target.
- ///
- [DataField("deselectOnMiss")]
- public bool DeselectOnMiss;
-
- ///
- /// Whether the action system should block this action if the user cannot actually access the target
- /// (unobstructed, in inventory, in backpack, etc). Some spells or abilities may want to disable this and
- /// implement their own checks.
- ///
- ///
- /// Even if this is false, the will still be checked.
- ///
- [DataField("checkCanAccess")]
- public bool CheckCanAccess = true;
-
- [DataField("range")]
- public float Range = SharedInteractionSystem.InteractionRange;
-
- ///
- /// If the target is invalid, this bool determines whether the left-click will default to performing a standard-interaction
- ///
- ///
- /// Interactions will still be blocked if the target-validation generates a pop-up
- ///
- [DataField("interactOnMiss")]
- public bool InteractOnMiss = false;
-
- ///
- /// If true, and if is enabled, then this action's icon will be drawn by that
- /// over lay in place of the currently held item "held item".
- ///
- [DataField("targetingIndicator")]
- public bool TargetingIndicator = true;
-
- public override void CopyFrom(object objectToClone)
- {
- base.CopyFrom(objectToClone);
-
- if (objectToClone is not TargetedAction toClone)
- return;
-
- Range = toClone.Range;
- CheckCanAccess = toClone.CheckCanAccess;
- DeselectOnMiss = toClone.DeselectOnMiss;
- Repeat = toClone.Repeat;
- InteractOnMiss = toClone.InteractOnMiss;
- TargetingIndicator = toClone.TargetingIndicator;
- }
-}
-
-///
-/// Action that targets some entity. Will result in being raised.
-///
-[Serializable, NetSerializable]
-[Friend(typeof(SharedActionsSystem))]
-[Virtual]
-public class EntityTargetAction : TargetedAction
-{
- ///
- /// The local-event to raise when this action is performed.
- ///
- [NonSerialized]
- [DataField("event")]
- public PerformEntityTargetActionEvent? Event;
-
- [DataField("whitelist")]
- public EntityWhitelist? Whitelist;
-
- [DataField("canTargetSelf")]
- public bool CanTargetSelf = true;
-
- public EntityTargetAction() { }
- public EntityTargetAction(EntityTargetAction toClone)
- {
- CopyFrom(toClone);
- }
- public override void CopyFrom(object objectToClone)
- {
- base.CopyFrom(objectToClone);
-
- if (objectToClone is not EntityTargetAction toClone)
- return;
-
- CanTargetSelf = toClone.CanTargetSelf;
-
- // This isn't a deep copy, but I don't expect white-lists to ever be edited during prediction. So good enough?
- Whitelist = toClone.Whitelist;
-
- // Events should be re-usable, and shouldn't be modified during prediction.
- Event = toClone.Event;
- }
-
- public override object Clone()
- {
- return new EntityTargetAction(this);
- }
-}
-
-///
-/// Action that targets some map coordinates. Will result in being raised.
-///
-[Serializable, NetSerializable]
-[Friend(typeof(SharedActionsSystem))]
-[Virtual]
-public class WorldTargetAction : TargetedAction
-{
- ///
- /// The local-event to raise when this action is performed.
- ///
- [DataField("event")]
- [NonSerialized]
- public PerformWorldTargetActionEvent? Event;
-
- public WorldTargetAction() { }
- public WorldTargetAction(WorldTargetAction toClone)
- {
- CopyFrom(toClone);
- }
-
- public override void CopyFrom(object objectToClone)
- {
- base.CopyFrom(objectToClone);
-
- if (objectToClone is not WorldTargetAction toClone)
- return;
-
- // Events should be re-usable, and shouldn't be modified during prediction.
- Event = toClone.Event;
- }
-
- public override object Clone()
- {
- return new WorldTargetAction(this);
- }
-}
diff --git a/Content.Shared/Actions/ActionsComponent.cs b/Content.Shared/Actions/ActionsComponent.cs
deleted file mode 100644
index 8c969228b2..0000000000
--- a/Content.Shared/Actions/ActionsComponent.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using Content.Shared.Actions.ActionTypes;
-using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Actions;
-
-[NetworkedComponent]
-[RegisterComponent]
-[Friend(typeof(SharedActionsSystem))]
-public sealed class ActionsComponent : Component
-{
- [ViewVariables]
- public SortedSet Actions = new();
-}
-
-[Serializable, NetSerializable]
-public sealed class ActionsComponentState : ComponentState
-{
- public readonly List Actions;
-
- public ActionsComponentState(List actions)
- {
- Actions = actions;
- }
-}
-
-///
-/// Determines how the action icon appears in the hotbar for item actions.
-///
-public enum ItemActionIconStyle : byte
-{
- ///
- /// The default - The item icon will be big with a small action icon in the corner
- ///
- BigItem,
-
- ///
- /// The action icon will be big with a small item icon in the corner
- ///
- BigAction,
-
- ///
- /// BigAction but no item icon will be shown in the corner.
- ///
- NoItem
-}
diff --git a/Content.Shared/Actions/Behaviors/IActionBehavior.cs b/Content.Shared/Actions/Behaviors/IActionBehavior.cs
new file mode 100644
index 0000000000..304db1620e
--- /dev/null
+++ b/Content.Shared/Actions/Behaviors/IActionBehavior.cs
@@ -0,0 +1,46 @@
+using System;
+using Content.Shared.Actions.Components;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Shared.Actions.Behaviors
+{
+ ///
+ /// Currently just a marker interface delineating the different possible
+ /// types of action behaviors.
+ ///
+ public interface IActionBehavior
+ {
+ }
+
+ ///
+ /// Base class for all action event args
+ ///
+ public abstract class ActionEventArgs : EventArgs
+ {
+ ///
+ /// Entity performing the action.
+ ///
+ public readonly EntityUid Performer;
+ ///
+ /// Action being performed
+ ///
+ public readonly ActionType ActionType;
+ ///
+ /// Actions component of the performer.
+ ///
+ public readonly SharedActionsComponent? PerformerActions;
+
+ public ActionEventArgs(EntityUid performer, ActionType actionType)
+ {
+ Performer = performer;
+ ActionType = actionType;
+ if (!IoCManager.Resolve().TryGetComponent(Performer, out PerformerActions))
+ {
+ throw new InvalidOperationException($"performer {IoCManager.Resolve().GetComponent(performer).EntityName} tried to perform action {actionType} " +
+ $" but the performer had no actions component," +
+ " which should never occur");
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Behaviors/IInstantAction.cs b/Content.Shared/Actions/Behaviors/IInstantAction.cs
new file mode 100644
index 0000000000..7d4f88d005
--- /dev/null
+++ b/Content.Shared/Actions/Behaviors/IInstantAction.cs
@@ -0,0 +1,25 @@
+using Robust.Shared.GameObjects;
+
+namespace Content.Shared.Actions.Behaviors
+{
+ ///
+ /// Action which does something immediately when used and has
+ /// no target.
+ ///
+ public interface IInstantAction : IActionBehavior
+ {
+
+ ///
+ /// Invoked when the instant action should be performed.
+ /// Implementation should perform the server side logic of the action.
+ ///
+ void DoInstantAction(InstantActionEventArgs args);
+ }
+
+ public sealed class InstantActionEventArgs : ActionEventArgs
+ {
+ public InstantActionEventArgs(EntityUid performer, ActionType actionType) : base(performer, actionType)
+ {
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Behaviors/ITargetEntityAction.cs b/Content.Shared/Actions/Behaviors/ITargetEntityAction.cs
new file mode 100644
index 0000000000..629acc0713
--- /dev/null
+++ b/Content.Shared/Actions/Behaviors/ITargetEntityAction.cs
@@ -0,0 +1,30 @@
+using Robust.Shared.GameObjects;
+
+namespace Content.Shared.Actions.Behaviors
+{
+ ///
+ /// Action which is used on a targeted entity.
+ ///
+ public interface ITargetEntityAction : IActionBehavior
+ {
+ ///
+ /// Invoked when the target entity action should be performed.
+ /// Implementation should perform the server side logic of the action.
+ ///
+ void DoTargetEntityAction(TargetEntityActionEventArgs args);
+ }
+
+ public sealed class TargetEntityActionEventArgs : ActionEventArgs
+ {
+ ///
+ /// Entity being targeted
+ ///
+ public readonly EntityUid Target;
+
+ public TargetEntityActionEventArgs(EntityUid performer, ActionType actionType, EntityUid target) :
+ base(performer, actionType)
+ {
+ Target = target;
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Behaviors/ITargetEntityItemAction.cs b/Content.Shared/Actions/Behaviors/ITargetEntityItemAction.cs
new file mode 100644
index 0000000000..9bf07df15b
--- /dev/null
+++ b/Content.Shared/Actions/Behaviors/ITargetEntityItemAction.cs
@@ -0,0 +1,32 @@
+using Content.Shared.Actions.Behaviors.Item;
+using Robust.Shared.GameObjects;
+
+namespace Content.Shared.Actions.Behaviors
+{
+ ///
+ /// Item action which is used on a targeted entity.
+ ///
+ public interface ITargetEntityItemAction : IItemActionBehavior
+ {
+ ///
+ /// Invoked when the target entity action should be performed.
+ /// Implementation should perform the server side logic of the action.
+ ///
+ void DoTargetEntityAction(TargetEntityItemActionEventArgs args);
+ }
+
+ public sealed class TargetEntityItemActionEventArgs : ItemActionEventArgs
+ {
+ ///
+ /// Entity being targeted
+ ///
+ public readonly EntityUid Target;
+
+ public TargetEntityItemActionEventArgs(EntityUid performer, EntityUid target, EntityUid item,
+ ItemActionType actionType) : base(performer, item, actionType)
+ {
+ Target = target;
+
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Behaviors/ITargetPointAction.cs b/Content.Shared/Actions/Behaviors/ITargetPointAction.cs
new file mode 100644
index 0000000000..2b329df310
--- /dev/null
+++ b/Content.Shared/Actions/Behaviors/ITargetPointAction.cs
@@ -0,0 +1,32 @@
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+
+namespace Content.Shared.Actions.Behaviors
+{
+ ///
+ /// Action which requires the user to select a target point, which
+ /// does not necessarily have an entity on it.
+ ///
+ public interface ITargetPointAction : IActionBehavior
+ {
+ ///
+ /// Invoked when the target point action should be performed.
+ /// Implementation should perform the server side logic of the action.
+ ///
+ void DoTargetPointAction(TargetPointActionEventArgs args);
+ }
+
+ public sealed class TargetPointActionEventArgs : ActionEventArgs
+ {
+ ///
+ /// Local coordinates of the targeted position.
+ ///
+ public readonly EntityCoordinates Target;
+
+ public TargetPointActionEventArgs(EntityUid performer, EntityCoordinates target, ActionType actionType)
+ : base(performer, actionType)
+ {
+ Target = target;
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Behaviors/ITargetPointItemAction.cs b/Content.Shared/Actions/Behaviors/ITargetPointItemAction.cs
new file mode 100644
index 0000000000..050b055f83
--- /dev/null
+++ b/Content.Shared/Actions/Behaviors/ITargetPointItemAction.cs
@@ -0,0 +1,33 @@
+using Content.Shared.Actions.Behaviors.Item;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+
+namespace Content.Shared.Actions.Behaviors
+{
+ ///
+ /// Item action which requires the user to select a target point, which
+ /// does not necessarily have an entity on it.
+ ///
+ public interface ITargetPointItemAction : IItemActionBehavior
+ {
+ ///
+ /// Invoked when the target point action should be performed.
+ /// Implementation should perform the server side logic of the action.
+ ///
+ void DoTargetPointAction(TargetPointItemActionEventArgs args);
+ }
+
+ public sealed class TargetPointItemActionEventArgs : ItemActionEventArgs
+ {
+ ///
+ /// Local coordinates of the targeted position.
+ ///
+ public readonly EntityCoordinates Target;
+
+ public TargetPointItemActionEventArgs(EntityUid performer, EntityCoordinates target, EntityUid item,
+ ItemActionType actionType) : base(performer, item, actionType)
+ {
+ Target = target;
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Behaviors/IToggleAction.cs b/Content.Shared/Actions/Behaviors/IToggleAction.cs
new file mode 100644
index 0000000000..c1263c5fd4
--- /dev/null
+++ b/Content.Shared/Actions/Behaviors/IToggleAction.cs
@@ -0,0 +1,41 @@
+using Robust.Shared.GameObjects;
+
+namespace Content.Shared.Actions.Behaviors
+{
+ ///
+ /// Action which can be toggled on and off
+ ///
+ public interface IToggleAction : IActionBehavior
+ {
+ ///
+ /// Invoked when the action will be toggled on/off.
+ /// Implementation should perform the server side logic of whatever
+ /// happens when it is toggled on / off.
+ ///
+ /// true if the attempt to toggle was successful, meaning the state should be toggled to the desired value.
+ /// False to leave toggle status unchanged. This is NOT returning the new toggle status, it is only returning
+ /// whether the attempt to toggle to the indicated status was successful.
+ ///
+ /// Note that it's still okay if the implementation directly modifies toggle status via SharedActionsComponent,
+ /// this is just an additional level of safety to ensure implementations will always
+ /// explicitly indicate if the toggle status should be changed.
+ bool DoToggleAction(ToggleActionEventArgs args);
+ }
+
+ public sealed class ToggleActionEventArgs : ActionEventArgs
+ {
+ ///
+ /// True if the toggle is attempting to be toggled on, false if attempting to toggle off
+ ///
+ public readonly bool ToggledOn;
+ ///
+ /// Opposite of ToggledOn
+ ///
+ public bool ToggledOff => !ToggledOn;
+
+ public ToggleActionEventArgs(EntityUid performer, ActionType actionType, bool toggledOn) : base(performer, actionType)
+ {
+ ToggledOn = toggledOn;
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Behaviors/Item/IInstantItemAction.cs b/Content.Shared/Actions/Behaviors/Item/IInstantItemAction.cs
new file mode 100644
index 0000000000..8adce4ce9a
--- /dev/null
+++ b/Content.Shared/Actions/Behaviors/Item/IInstantItemAction.cs
@@ -0,0 +1,26 @@
+using Robust.Shared.GameObjects;
+
+namespace Content.Shared.Actions.Behaviors.Item
+{
+ ///
+ /// Item action which does something immediately when used and has
+ /// no target.
+ ///
+ public interface IInstantItemAction : IItemActionBehavior
+ {
+
+ ///
+ /// Invoked when the instant action should be performed.
+ /// Implementation should perform the server side logic of the action.
+ ///
+ void DoInstantAction(InstantItemActionEventArgs args);
+ }
+
+ public sealed class InstantItemActionEventArgs : ItemActionEventArgs
+ {
+ public InstantItemActionEventArgs(EntityUid performer, EntityUid item, ItemActionType actionType) :
+ base(performer, item, actionType)
+ {
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Behaviors/Item/IItemActionBehavior.cs b/Content.Shared/Actions/Behaviors/Item/IItemActionBehavior.cs
new file mode 100644
index 0000000000..d185eb6281
--- /dev/null
+++ b/Content.Shared/Actions/Behaviors/Item/IItemActionBehavior.cs
@@ -0,0 +1,53 @@
+using System;
+using Content.Shared.Actions.Components;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Shared.Actions.Behaviors.Item
+{
+ ///
+ /// Currently just a marker interface delineating the different possible
+ /// types of item action behaviors.
+ ///
+ public interface IItemActionBehavior
+ {
+
+ }
+
+ ///
+ /// Base class for all item action event args
+ ///
+ public abstract class ItemActionEventArgs : EventArgs
+ {
+ ///
+ /// Entity performing the action.
+ ///
+ public readonly EntityUid Performer;
+ ///
+ /// Item being used to perform the action
+ ///
+ public readonly EntityUid Item;
+ ///
+ /// Action being performed
+ ///
+ public readonly ItemActionType ActionType;
+ ///
+ /// Item actions component of the item.
+ ///
+ public readonly ItemActionsComponent? ItemActions;
+
+ public ItemActionEventArgs(EntityUid performer, EntityUid item, ItemActionType actionType)
+ {
+ Performer = performer;
+ ActionType = actionType;
+ Item = item;
+ var entMan = IoCManager.Resolve();
+ if (!entMan.TryGetComponent(Item, out ItemActions))
+ {
+ throw new InvalidOperationException($"performer {entMan.GetComponent(performer).EntityName} tried to perform item action {actionType} " +
+ $" for item {entMan.GetComponent(Item).EntityName} but the item had no ItemActionsComponent," +
+ " which should never occur");
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Behaviors/Item/IToggleItemAction.cs b/Content.Shared/Actions/Behaviors/Item/IToggleItemAction.cs
new file mode 100644
index 0000000000..953de58a47
--- /dev/null
+++ b/Content.Shared/Actions/Behaviors/Item/IToggleItemAction.cs
@@ -0,0 +1,42 @@
+using Robust.Shared.GameObjects;
+
+namespace Content.Shared.Actions.Behaviors.Item
+{
+ ///
+ /// Item action which can be toggled on and off
+ ///
+ public interface IToggleItemAction : IItemActionBehavior
+ {
+ ///
+ /// Invoked when the action will be toggled on/off.
+ /// Implementation should perform the server side logic of whatever
+ /// happens when it is toggled on / off.
+ ///
+ /// true if the attempt to toggle was successful, meaning the state should be toggled to the desired value.
+ /// False to leave toggle status unchanged. This is NOT returning the new toggle status, it is only returning
+ /// whether the attempt to toggle to the indicated status was successful.
+ ///
+ /// Note that it's still okay if the implementation directly modifies toggle status via ItemActionsComponent,
+ /// this is just an additional level of safety to ensure implementations will always
+ /// explicitly indicate if the toggle status should be changed.
+ bool DoToggleAction(ToggleItemActionEventArgs args);
+ }
+
+ public sealed class ToggleItemActionEventArgs : ItemActionEventArgs
+ {
+ ///
+ /// True if the toggle was toggled on, false if it was toggled off
+ ///
+ public readonly bool ToggledOn;
+ ///
+ /// Opposite of ToggledOn
+ ///
+ public bool ToggledOff => !ToggledOn;
+
+ public ToggleItemActionEventArgs(EntityUid performer, bool toggledOn, EntityUid item,
+ ItemActionType actionType) : base(performer, item, actionType)
+ {
+ ToggledOn = toggledOn;
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Components/ItemActionsComponent.cs b/Content.Shared/Actions/Components/ItemActionsComponent.cs
new file mode 100644
index 0000000000..b80b87bbb2
--- /dev/null
+++ b/Content.Shared/Actions/Components/ItemActionsComponent.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Collections.Generic;
+using Content.Shared.Hands;
+using Content.Shared.Hands.Components;
+using Content.Shared.Inventory;
+using Robust.Shared.Analyzers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Shared.Actions.Components
+{
+ ///
+ /// This should be used on items which provide actions. Defines which actions the item provides
+ /// and allows modifying the states of those actions. Item components should use this rather than
+ /// SharedActionsComponent on the player to handle granting / revoking / modifying the states of the
+ /// actions provided by this item.
+ ///
+ /// When a player equips this item, all the actions defined in this component will be granted to the
+ /// player in their current states. This means the states will persist between players.
+ ///
+ /// Currently only maintained server side and not synced to client, as are all the equip/unequip events.
+ ///
+ [RegisterComponent]
+ public sealed class ItemActionsComponent : Component
+ {
+ ///
+ /// Configuration for the item actions initially provided by this item. Actions defined here
+ /// will be automatically granted unless their state is modified using the methods
+ /// on this component. Additional actions can be granted by this item via GrantOrUpdate
+ ///
+ public IEnumerable ActionConfigs => _actionConfigs;
+
+ public bool IsEquipped;
+
+ ///
+ /// hand it's currently in, null if not in a hand.
+ ///
+ public Hand? InHand;
+
+ ///
+ /// Entity currently holding this in hand or equip slot. Null if not held.
+ ///
+ public EntityUid? Holder;
+ // cached actions component of the holder, since we'll need to access it frequently
+ public SharedActionsComponent? HolderActionsComponent;
+
+ [DataField("actions")]
+ private List _actionConfigs
+ {
+ get => internalActionConfigs;
+ set
+ {
+ internalActionConfigs = value;
+ foreach (var actionConfig in value)
+ {
+ GrantOrUpdate(actionConfig.ActionType, actionConfig.Enabled, false, null);
+ }
+ }
+ }
+
+ // State of all actions provided by this item.
+ private readonly Dictionary _actions = new();
+ private List internalActionConfigs = new ();
+
+ protected override void Startup()
+ {
+ base.Startup();
+ GrantOrUpdateAllToHolder();
+ }
+
+ protected override void Shutdown()
+ {
+ base.Shutdown();
+ RevokeAllFromHolder();
+ }
+
+ public void GrantOrUpdateAllToHolder()
+ {
+ if (HolderActionsComponent == null) return;
+ foreach (var (actionType, state) in _actions)
+ {
+ HolderActionsComponent.GrantOrUpdateItemAction(actionType, Owner, state);
+ }
+ }
+
+ public void RevokeAllFromHolder()
+ {
+ if (HolderActionsComponent == null) return;
+ foreach (var (actionType, state) in _actions)
+ {
+ HolderActionsComponent.RevokeItemAction(actionType, Owner);
+ }
+ }
+
+ ///
+ /// Update the state of the action, granting it if it isn't already granted.
+ /// If the action had any existing state, those specific fields will be overwritten by any
+ /// corresponding non-null arguments.
+ ///
+ /// action being granted / updated
+ /// When null, preserves the current enable status of the action, defaulting
+ /// to true if action has no current state.
+ /// When non-null, indicates whether the entity is able to perform the action (if disabled,
+ /// the player will see they have the action but it will appear greyed out)
+ /// When null, preserves the current toggle status of the action, defaulting
+ /// to false if action has no current state.
+ /// When non-null, action will be shown toggled to this value
+ /// When null (unless clearCooldown is true), preserves the current cooldown status of the action, defaulting
+ /// to no cooldown if action has no current state.
+ /// When non-null or clearCooldown is true, action cooldown will be set to this value. Note that this cooldown
+ /// is tied to this item.
+ /// If true, setting cooldown to null will clear the current cooldown
+ /// of this action rather than preserving it.
+ public void GrantOrUpdate(ItemActionType actionType, bool? enabled = null,
+ bool? toggleOn = null,
+ (TimeSpan start, TimeSpan end)? cooldown = null, bool clearCooldown = false)
+ {
+ var dirty = false;
+ // this will be overwritten if we find the value in our dict, otherwise
+ // we will use this as our new action state.
+ if (!_actions.TryGetValue(actionType, out var actionState))
+ {
+ dirty = true;
+ actionState = new ActionState(enabled ?? true, toggleOn ?? false);
+ }
+
+ if (enabled.HasValue && enabled != actionState.Enabled)
+ {
+ dirty = true;
+ actionState.Enabled = true;
+ }
+
+ if ((cooldown.HasValue || clearCooldown) && actionState.Cooldown != cooldown)
+ {
+ dirty = true;
+ actionState.Cooldown = cooldown;
+ }
+
+ if (toggleOn.HasValue && actionState.ToggledOn != toggleOn.Value)
+ {
+ dirty = true;
+ actionState.ToggledOn = toggleOn.Value;
+ }
+
+ if (!dirty) return;
+
+ _actions[actionType] = actionState;
+ HolderActionsComponent?.GrantOrUpdateItemAction(actionType, Owner, actionState);
+ }
+
+ ///
+ /// Update the cooldown of a particular action. Actions on cooldown cannot be used.
+ /// Setting the cooldown to null clears it.
+ ///
+ public void Cooldown(ItemActionType actionType, (TimeSpan start, TimeSpan end)? cooldown = null)
+ {
+ GrantOrUpdate(actionType, cooldown: cooldown, clearCooldown: true);
+ }
+
+ ///
+ /// Enable / disable this action. Disabled actions are still shown to the player, but
+ /// shown as not usable.
+ ///
+ public void SetEnabled(ItemActionType actionType, bool enabled)
+ {
+ GrantOrUpdate(actionType, enabled);
+ }
+
+ ///
+ /// Toggle the action on / off
+ ///
+ public void Toggle(ItemActionType actionType, bool toggleOn)
+ {
+ GrantOrUpdate(actionType, toggleOn: toggleOn);
+ }
+ }
+
+ ///
+ /// Configuration for an item action provided by an item.
+ ///
+ [DataDefinition]
+ public sealed class ItemActionConfig : ISerializationHooks
+ {
+ [DataField("actionType", required: true)]
+ public ItemActionType ActionType { get; private set; } = ItemActionType.Error;
+
+ ///
+ /// Whether action is initially enabled on this item. Defaults to true.
+ ///
+ public bool Enabled { get; private set; } = true;
+
+ void ISerializationHooks.AfterDeserialization()
+ {
+ if (ActionType == ItemActionType.Error)
+ {
+ Logger.ErrorS("action", "invalid or missing actionType");
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Components/SharedActionsComponent.cs b/Content.Shared/Actions/Components/SharedActionsComponent.cs
new file mode 100644
index 0000000000..5fb0ecf68d
--- /dev/null
+++ b/Content.Shared/Actions/Components/SharedActionsComponent.cs
@@ -0,0 +1,641 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Shared.Actions.Prototypes;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Robust.Shared.Players;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Timing;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Shared.Actions.Components
+{
+ ///
+ /// Manages the actions available to an entity.
+ /// Should only be used for player-controlled entities.
+ ///
+ /// Actions are granted directly to the owner entity. Item actions are granted via a particular item which
+ /// must be in the owner's inventory (the action is revoked when item leaves the owner's inventory). This
+ /// should almost always be done via ItemActionsComponent on the item entity (which also tracks the
+ /// cooldowns associated with the actions on that item).
+ ///
+ /// Actions can still have an associated state even when revoked. For example, a flashlight toggle action
+ /// may be unusable while the player is stunned, but this component will still have an entry for the action
+ /// so the user can see whether it's currently toggled on or off.
+ ///
+ [NetworkedComponent()]
+ public abstract class SharedActionsComponent : Component
+ {
+ private static readonly TimeSpan CooldownExpiryThreshold = TimeSpan.FromSeconds(10);
+
+ [Dependency]
+ protected readonly ActionManager ActionManager = default!;
+ [Dependency]
+ protected readonly IGameTiming GameTiming = default!;
+ [Dependency]
+ protected readonly IEntityManager EntityManager = default!;
+
+ ///
+ /// Actions granted to this entity as soon as they spawn, regardless
+ /// of the status of the entity.
+ ///
+ public IEnumerable InnateActions => _innateActions ?? Enumerable.Empty();
+ [DataField("innateActions")]
+ private List? _innateActions = null;
+
+
+ // entries are removed from this if they are at the initial state (not enabled, no cooldown, toggled off).
+ // a system runs which periodically removes cooldowns from entries when they are revoked and their
+ // cooldowns have expired for a long enough time, also removing the entry if it is then at initial state.
+ // This helps to keep our component state smaller.
+ [ViewVariables]
+ private Dictionary _actions = new();
+
+ // Holds item action states. Item actions are only added to this when granted, and are removed
+ // when revoked or when they leave inventory. This is almost entirely handled by ItemActionsComponent on
+ // item entities.
+ [ViewVariables]
+ private Dictionary> _itemActions =
+ new();
+
+ protected override void Startup()
+ {
+ base.Startup();
+ foreach (var actionType in InnateActions)
+ {
+ Grant(actionType);
+ }
+ }
+
+
+ public override ComponentState GetComponentState()
+ {
+ return new ActionComponentState(_actions, _itemActions);
+ }
+
+ public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
+ {
+ base.HandleComponentState(curState, nextState);
+
+ if (curState is not ActionComponentState state)
+ {
+ return;
+ }
+ _actions = state.Actions;
+ _itemActions = state.ItemActions;
+ }
+
+ ///
+ /// Gets the action state associated with the specified action type, if it has been
+ /// granted, has a cooldown, or has been toggled on
+ ///
+ /// false if not found for this action type
+ public bool TryGetActionState(ActionType actionType, out ActionState actionState)
+ {
+ return _actions.TryGetValue(actionType, out actionState);
+ }
+
+ ///
+ /// Gets the item action states associated with the specified item if any have been granted
+ /// and not yet revoked.
+ ///
+ /// false if no states found for this item action type.
+ public bool TryGetItemActionStates(EntityUid item, [NotNullWhen((true))] out IReadOnlyDictionary? itemActionStates)
+ {
+ if (_itemActions.TryGetValue(item, out var actualItemActionStates))
+ {
+ itemActionStates = actualItemActionStates;
+ return true;
+ }
+
+ itemActionStates = null;
+ return false;
+ }
+
+ ///
+ /// Gets the item action state associated with the specified item action type for the specified item, if it has any.
+ ///
+ /// false if no state found for this item action type for this item
+ public bool TryGetItemActionState(ItemActionType actionType, EntityUid item, out ActionState actionState)
+ {
+ if (_itemActions.TryGetValue(item, out var actualItemActionStates))
+ {
+ return actualItemActionStates.TryGetValue(actionType, out actionState);
+ }
+
+ actionState = default;
+ return false;
+ }
+
+ /// true if the action is granted and enabled (if item action, if granted and enabled for any item)
+ public bool IsGranted(BaseActionPrototype actionType)
+ {
+ return actionType switch
+ {
+ ActionPrototype actionPrototype => IsGranted(actionPrototype.ActionType),
+ ItemActionPrototype itemActionPrototype => IsGranted(itemActionPrototype.ActionType),
+ _ => false
+ };
+ }
+
+ public bool IsGranted(ActionType actionType)
+ {
+ if (TryGetActionState(actionType, out var actionState))
+ {
+ return actionState.Enabled;
+ }
+
+ return false;
+ }
+
+ /// true if the action is granted and enabled for any item. This
+ /// has to traverse the entire item state dictionary so please avoid frequent calls.
+ public bool IsGranted(ItemActionType actionType)
+ {
+ return _itemActions.Values.SelectMany(vals => vals)
+ .Any(state => state.Key == actionType && state.Value.Enabled);
+ }
+
+ ///
+ /// Gets all action types that have non-initial state (granted, have a cooldown, or toggled on).
+ ///
+ public IReadOnlyDictionary ActionStates()
+ {
+ return _actions;
+ }
+
+ ///
+ /// Gets all items that have actions currently granted (that are not revoked
+ /// and still in inventory).
+ /// Map from item uid -> (action type -> associated action state)
+ /// PLEASE DO NOT MODIFY THE INNER DICTIONARY! I CANNOT CAST IT TO IReadOnlyDictionary!
+ ///
+ public IReadOnlyDictionary> ItemActionStates()
+ {
+ return _itemActions;
+ }
+
+ ///
+ /// Creates or updates the action state with the supplied non-null values
+ ///
+ private void GrantOrUpdate(ActionType actionType, bool? enabled = null, bool? toggleOn = null,
+ (TimeSpan start, TimeSpan end)? cooldown = null, bool clearCooldown = false)
+ {
+ var dirty = false;
+ if (!_actions.TryGetValue(actionType, out var actionState))
+ {
+ // no state at all for this action, create it anew
+ dirty = true;
+ actionState = new ActionState(enabled ?? false, toggleOn ?? false);
+ }
+
+ if (enabled.HasValue && actionState.Enabled != enabled.Value)
+ {
+ dirty = true;
+ actionState.Enabled = enabled.Value;
+ }
+
+ if ((cooldown.HasValue || clearCooldown) && actionState.Cooldown != cooldown)
+ {
+ dirty = true;
+ actionState.Cooldown = cooldown;
+ }
+
+ if (toggleOn.HasValue && actionState.ToggledOn != toggleOn.Value)
+ {
+ dirty = true;
+ actionState.ToggledOn = toggleOn.Value;
+ }
+
+ if (!dirty) return;
+
+ _actions[actionType] = actionState;
+ AfterActionChanged();
+ Dirty();
+ }
+
+ ///
+ /// Intended to only be used by ItemActionsComponent.
+ /// Updates the state of the item action provided by the item, granting the action
+ /// if it is not yet granted to the player. Should be called whenever the
+ /// status changes. The existing state will be completely overwritten by the new state.
+ ///
+ public void GrantOrUpdateItemAction(ItemActionType actionType, EntityUid item, ActionState state)
+ {
+ if (!_itemActions.TryGetValue(item, out var itemStates))
+ {
+ itemStates = new Dictionary();
+ _itemActions[item] = itemStates;
+ }
+
+ itemStates[actionType] = state;
+ AfterActionChanged();
+ Dirty();
+ }
+
+ ///
+ /// Intended to only be used by ItemActionsComponent. Revokes the item action so the player no longer
+ /// sees it and can no longer use it.
+ ///
+ public void RevokeItemAction(ItemActionType actionType, EntityUid item)
+ {
+ if (!_itemActions.TryGetValue(item, out var itemStates))
+ return;
+
+ itemStates.Remove(actionType);
+ if (itemStates.Count == 0)
+ {
+ _itemActions.Remove(item);
+ }
+ AfterActionChanged();
+ Dirty();
+ }
+
+ ///
+ /// Grants the entity the ability to perform the action, optionally overriding its
+ /// current state with specified values.
+ ///
+ /// Even if the action was already granted, if the action had any state (cooldown, toggle) prior to this method
+ /// being called, it will be preserved, with specific fields optionally overridden by any of the provided
+ /// non-null arguments.
+ ///
+ /// When null, preserves the current toggle status of the action, defaulting
+ /// to false if action has no current state.
+ /// When non-null, action will be shown toggled to this value
+ /// When null, preserves the current cooldown status of the action, defaulting
+ /// to no cooldown if action has no current state.
+ /// When non-null, action cooldown will be set to this value.
+ public void Grant(ActionType actionType, bool? toggleOn = null,
+ (TimeSpan start, TimeSpan end)? cooldown = null)
+ {
+ GrantOrUpdate(actionType, true, toggleOn, cooldown);
+ }
+
+ ///
+ /// Grants the entity the ability to perform the action, resetting its state
+ /// to its initial state and settings its state based on supplied parameters.
+ ///
+ /// Even if the action was already granted, if the action had any state (cooldown, toggle) prior to this method
+ /// being called, it will be reset to initial (no cooldown, toggled off).
+ ///
+ /// action will be shown toggled to this value
+ /// action cooldown will be set to this value (by default the cooldown is cleared).
+ public void GrantFromInitialState(ActionType actionType, bool toggleOn = false,
+ (TimeSpan start, TimeSpan end)? cooldown = null)
+ {
+ _actions.Remove(actionType);
+ Grant(actionType, toggleOn, cooldown);
+ }
+
+ ///
+ /// Sets the cooldown for the action. Actions on cooldown cannot be used.
+ ///
+ /// This will work even if the action is revoked -
+ /// for example if there's an ability with a cooldown which is temporarily unusable due
+ /// to the player being stunned, the cooldown will still tick down even while the player
+ /// is stunned.
+ ///
+ /// Setting cooldown to null clears it.
+ ///
+ public void Cooldown(ActionType actionType, (TimeSpan start, TimeSpan end)? cooldown)
+ {
+ GrantOrUpdate(actionType, cooldown: cooldown, clearCooldown: true);
+ }
+
+ ///
+ /// Revokes the ability to perform the action for this entity. Current state
+ /// of the action (toggle / cooldown) is preserved.
+ ///
+ public void Revoke(ActionType actionType)
+ {
+ if (!_actions.TryGetValue(actionType, out var actionState)) return;
+
+ if (!actionState.Enabled) return;
+
+ actionState.Enabled = false;
+
+ // don't store it anymore if its at its initial state.
+ if (actionState.IsAtInitialState)
+ {
+ _actions.Remove(actionType);
+ }
+ else
+ {
+ _actions[actionType] = actionState;
+ }
+
+ AfterActionChanged();
+ Dirty();
+ }
+
+ ///
+ /// Toggles the action to the specified value. Works even if the action is on cooldown
+ /// or revoked.
+ ///
+ public void ToggleAction(ActionType actionType, bool toggleOn)
+ {
+ Grant(actionType, toggleOn);
+ }
+
+ ///
+ /// Clears any cooldowns which have expired beyond the predefined threshold.
+ /// this should be run periodically to ensure we don't have unbounded growth of
+ /// our saved action data, and keep our component state sent to the client as minimal as possible.
+ ///
+ public void ExpireCooldowns()
+ {
+
+ // actions - only clear cooldowns and remove associated action state
+ // if the action is at initial state
+ var actionTypesToRemove = new List();
+ foreach (var (actionType, actionState) in _actions)
+ {
+ // ignore it unless we may be able to delete it due to
+ // clearing the cooldown
+ if (!actionState.IsAtInitialStateExceptCooldown) continue;
+ if (!actionState.Cooldown.HasValue)
+ {
+ actionTypesToRemove.Add(actionType);
+ continue;
+ }
+ var expiryTime = GameTiming.CurTime - actionState.Cooldown.Value.Item2;
+ if (expiryTime > CooldownExpiryThreshold)
+ {
+ actionTypesToRemove.Add(actionType);
+ }
+ }
+
+ foreach (var remove in actionTypesToRemove)
+ {
+ _actions.Remove(remove);
+ }
+ }
+
+ ///
+ /// Invoked after a change has been made to an action state in this component.
+ ///
+ protected virtual void AfterActionChanged() { }
+ }
+
+ [Serializable, NetSerializable]
+ public sealed class ActionComponentState : ComponentState
+ {
+ public Dictionary Actions;
+ public Dictionary> ItemActions;
+
+ public ActionComponentState(Dictionary actions,
+ Dictionary> itemActions)
+ {
+ Actions = actions;
+ ItemActions = itemActions;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public struct ActionState
+ {
+ ///
+ /// False if this action is not currently allowed to be performed.
+ ///
+ public bool Enabled;
+ ///
+ /// Only used for toggle actions, indicates whether it's currently toggled on or off
+ /// TODO: Eventually this should probably be a byte so we it can toggle through multiple states.
+ ///
+ public bool ToggledOn;
+ public (TimeSpan start, TimeSpan end)? Cooldown;
+ public bool IsAtInitialState => IsAtInitialStateExceptCooldown && !Cooldown.HasValue;
+ public bool IsAtInitialStateExceptCooldown => !Enabled && !ToggledOn;
+
+ ///
+ /// Creates an action state for the indicated type, defaulting to the
+ /// initial state.
+ ///
+ public ActionState(bool enabled = false, bool toggledOn = false, (TimeSpan start, TimeSpan end)? cooldown = null)
+ {
+ Enabled = enabled;
+ ToggledOn = toggledOn;
+ Cooldown = cooldown;
+ }
+
+ public bool IsOnCooldown(TimeSpan curTime)
+ {
+ if (Cooldown == null) return false;
+ return curTime < Cooldown.Value.Item2;
+ }
+ public bool IsOnCooldown(IGameTiming gameTiming)
+ {
+ return IsOnCooldown(gameTiming.CurTime);
+ }
+
+ public bool Equals(ActionState other)
+ {
+ return Enabled == other.Enabled && ToggledOn == other.ToggledOn && Nullable.Equals(Cooldown, other.Cooldown);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is ActionState other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Enabled, ToggledOn, Cooldown);
+ }
+ }
+
+ [Serializable, NetSerializable]
+#pragma warning disable 618
+ public abstract class BasePerformActionMessage : ComponentMessage
+#pragma warning restore 618
+ {
+ public abstract BehaviorType BehaviorType { get; }
+ }
+
+ [Serializable, NetSerializable]
+ public abstract class PerformActionMessage : BasePerformActionMessage
+ {
+ public readonly ActionType ActionType;
+
+ protected PerformActionMessage(ActionType actionType)
+ {
+ Directed = true;
+ ActionType = actionType;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public abstract class PerformItemActionMessage : BasePerformActionMessage
+ {
+ public readonly ItemActionType ActionType;
+ public readonly EntityUid Item;
+
+ protected PerformItemActionMessage(ItemActionType actionType, EntityUid item)
+ {
+ Directed = true;
+ ActionType = actionType;
+ Item = item;
+ }
+ }
+
+
+ ///
+ /// A message that tells server we want to run the instant action logic.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class PerformInstantActionMessage : PerformActionMessage
+ {
+ public override BehaviorType BehaviorType => BehaviorType.Instant;
+
+ public PerformInstantActionMessage(ActionType actionType) : base(actionType)
+ {
+ }
+ }
+
+ ///
+ /// A message that tells server we want to run the instant action logic.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class PerformInstantItemActionMessage : PerformItemActionMessage
+ {
+ public override BehaviorType BehaviorType => BehaviorType.Instant;
+
+ public PerformInstantItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item)
+ {
+ }
+ }
+
+ public interface IToggleActionMessage
+ {
+ bool ToggleOn { get; }
+ }
+
+ public interface ITargetPointActionMessage
+ {
+ ///
+ /// Targeted local coordinates
+ ///
+ EntityCoordinates Target { get; }
+ }
+
+ public interface ITargetEntityActionMessage
+ {
+ ///
+ /// Targeted entity
+ ///
+ EntityUid Target { get; }
+ }
+
+ ///
+ /// A message that tells server we want to toggle on the indicated action.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class PerformToggleOnActionMessage : PerformActionMessage, IToggleActionMessage
+ {
+ public override BehaviorType BehaviorType => BehaviorType.Toggle;
+ public bool ToggleOn => true;
+ public PerformToggleOnActionMessage(ActionType actionType) : base(actionType) { }
+ }
+
+ ///
+ /// A message that tells server we want to toggle off the indicated action.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class PerformToggleOffActionMessage : PerformActionMessage, IToggleActionMessage
+ {
+ public override BehaviorType BehaviorType => BehaviorType.Toggle;
+ public bool ToggleOn => false;
+ public PerformToggleOffActionMessage(ActionType actionType) : base(actionType) { }
+ }
+
+ ///
+ /// A message that tells server we want to toggle on the indicated action.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class PerformToggleOnItemActionMessage : PerformItemActionMessage, IToggleActionMessage
+ {
+ public override BehaviorType BehaviorType => BehaviorType.Toggle;
+ public bool ToggleOn => true;
+ public PerformToggleOnItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item) { }
+ }
+
+ ///
+ /// A message that tells server we want to toggle off the indicated action.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class PerformToggleOffItemActionMessage : PerformItemActionMessage, IToggleActionMessage
+ {
+ public override BehaviorType BehaviorType => BehaviorType.Toggle;
+ public bool ToggleOn => false;
+ public PerformToggleOffItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item) { }
+ }
+
+ ///
+ /// A message that tells server we want to target the provided point with a particular action.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class PerformTargetPointActionMessage : PerformActionMessage, ITargetPointActionMessage
+ {
+ public override BehaviorType BehaviorType => BehaviorType.TargetPoint;
+ private readonly EntityCoordinates _target;
+ public EntityCoordinates Target => _target;
+
+ public PerformTargetPointActionMessage(ActionType actionType, EntityCoordinates target) : base(actionType)
+ {
+ _target = target;
+ }
+ }
+
+ ///
+ /// A message that tells server we want to target the provided point with a particular action.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class PerformTargetPointItemActionMessage : PerformItemActionMessage, ITargetPointActionMessage
+ {
+ private readonly EntityCoordinates _target;
+ public EntityCoordinates Target => _target;
+ public override BehaviorType BehaviorType => BehaviorType.TargetPoint;
+
+ public PerformTargetPointItemActionMessage(ItemActionType actionType, EntityUid item, EntityCoordinates target) : base(actionType, item)
+ {
+ _target = target;
+ }
+ }
+
+ ///
+ /// A message that tells server we want to target the provided entity with a particular action.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class PerformTargetEntityActionMessage : PerformActionMessage, ITargetEntityActionMessage
+ {
+ public override BehaviorType BehaviorType => BehaviorType.TargetEntity;
+ private readonly EntityUid _target;
+ public EntityUid Target => _target;
+
+ public PerformTargetEntityActionMessage(ActionType actionType, EntityUid target) : base(actionType)
+ {
+ _target = target;
+ }
+ }
+
+ ///
+ /// A message that tells server we want to target the provided entity with a particular action.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class PerformTargetEntityItemActionMessage : PerformItemActionMessage, ITargetEntityActionMessage
+ {
+ public override BehaviorType BehaviorType => BehaviorType.TargetEntity;
+ private readonly EntityUid _target;
+ public EntityUid Target => _target;
+
+ public PerformTargetEntityItemActionMessage(ItemActionType actionType, EntityUid item, EntityUid target) : base(actionType, item)
+ {
+ _target = target;
+ }
+ }
+}
diff --git a/Content.Shared/Actions/IActionAttempt.cs b/Content.Shared/Actions/IActionAttempt.cs
new file mode 100644
index 0000000000..ab3726057f
--- /dev/null
+++ b/Content.Shared/Actions/IActionAttempt.cs
@@ -0,0 +1,226 @@
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Actions.Behaviors.Item;
+using Content.Shared.Actions.Components;
+using Content.Shared.Actions.Prototypes;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Map;
+
+namespace Content.Shared.Actions
+{
+ ///
+ /// An attempt to perform a specific action. Main purpose of this interface is to
+ /// reduce code duplication related to handling attempts to perform non-item vs item actions by
+ /// providing a single interface for various functionality that needs to be performed on both.
+ ///
+ public interface IActionAttempt
+ {
+ ///
+ /// Action Prototype attempting to be performed
+ ///
+ BaseActionPrototype Action { get; }
+#pragma warning disable 618
+ ComponentMessage PerformInstantActionMessage();
+ ComponentMessage PerformToggleActionMessage(bool on);
+ ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args);
+ ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args);
+#pragma warning restore 618
+ ///
+ /// Tries to get the action state for this action from the actionsComponent, returning
+ /// true if found.
+ ///
+ bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState);
+
+ ///
+ /// Toggles the action within the provided action component
+ ///
+ void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn);
+
+ ///
+ /// Perform the server-side logic of the action
+ ///
+ void DoInstantAction(EntityUid player);
+
+ ///
+ /// Perform the server-side logic of the toggle action
+ ///
+ /// true if the attempt to toggle was successful, meaning the state should be toggled to the
+ /// indicated value
+ bool DoToggleAction(EntityUid player, bool on);
+
+ ///
+ /// Perform the server-side logic of the target point action
+ ///
+ void DoTargetPointAction(EntityUid player, EntityCoordinates target);
+
+ ///
+ /// Perform the server-side logic of the target entity action
+ ///
+ void DoTargetEntityAction(EntityUid player, EntityUid target);
+ }
+
+ public sealed class ActionAttempt : IActionAttempt
+ {
+ private readonly ActionPrototype _action;
+
+ public BaseActionPrototype Action => _action;
+
+ public ActionAttempt(ActionPrototype action)
+ {
+ _action = action;
+ }
+
+ public bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState)
+ {
+ return actionsComponent.TryGetActionState(_action.ActionType, out actionState);
+ }
+
+ public void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn)
+ {
+ actionsComponent.ToggleAction(_action.ActionType, toggleOn);
+ }
+
+ public void DoInstantAction(EntityUid player)
+ {
+ _action.InstantAction.DoInstantAction(new InstantActionEventArgs(player, _action.ActionType));
+ }
+
+ public bool DoToggleAction(EntityUid player, bool on)
+ {
+ return _action.ToggleAction.DoToggleAction(new ToggleActionEventArgs(player, _action.ActionType, on));
+ }
+
+ public void DoTargetPointAction(EntityUid player, EntityCoordinates target)
+ {
+ _action.TargetPointAction.DoTargetPointAction(new TargetPointActionEventArgs(player, target, _action.ActionType));
+ }
+
+ public void DoTargetEntityAction(EntityUid player, EntityUid target)
+ {
+ _action.TargetEntityAction.DoTargetEntityAction(new TargetEntityActionEventArgs(player, _action.ActionType,
+ target));
+ }
+
+#pragma warning disable 618
+ public ComponentMessage PerformInstantActionMessage()
+#pragma warning restore 618
+ {
+ return new PerformInstantActionMessage(_action.ActionType);
+ }
+
+#pragma warning disable 618
+ public ComponentMessage PerformToggleActionMessage(bool toggleOn)
+#pragma warning restore 618
+ {
+ if (toggleOn)
+ {
+ return new PerformToggleOnActionMessage(_action.ActionType);
+ }
+ return new PerformToggleOffActionMessage(_action.ActionType);
+ }
+
+#pragma warning disable 618
+ public ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
+#pragma warning restore 618
+ {
+ return new PerformTargetPointActionMessage(_action.ActionType, args.Coordinates);
+ }
+
+#pragma warning disable 618
+ public ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
+#pragma warning restore 618
+ {
+ return new PerformTargetEntityActionMessage(_action.ActionType, args.EntityUid);
+ }
+
+ public override string ToString()
+ {
+ return $"{nameof(_action)}: {_action.ActionType}";
+ }
+ }
+
+ public sealed class ItemActionAttempt : IActionAttempt
+ {
+ private readonly ItemActionPrototype _action;
+ private readonly EntityUid _item;
+ private readonly ItemActionsComponent _itemActions;
+
+ public BaseActionPrototype Action => _action;
+
+ public ItemActionAttempt(ItemActionPrototype action, EntityUid item, ItemActionsComponent itemActions)
+ {
+ _action = action;
+ _item = item;
+ _itemActions = itemActions;
+ }
+
+ public void DoInstantAction(EntityUid player)
+ {
+ _action.InstantAction.DoInstantAction(new InstantItemActionEventArgs(player, _item, _action.ActionType));
+ }
+
+ public bool DoToggleAction(EntityUid player, bool on)
+ {
+ return _action.ToggleAction.DoToggleAction(new ToggleItemActionEventArgs(player, on, _item, _action.ActionType));
+ }
+
+ public void DoTargetPointAction(EntityUid player, EntityCoordinates target)
+ {
+ _action.TargetPointAction.DoTargetPointAction(new TargetPointItemActionEventArgs(player, target, _item,
+ _action.ActionType));
+ }
+
+ public void DoTargetEntityAction(EntityUid player, EntityUid target)
+ {
+ _action.TargetEntityAction.DoTargetEntityAction(new TargetEntityItemActionEventArgs(player, target,
+ _item, _action.ActionType));
+ }
+
+ public bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState)
+ {
+ return actionsComponent.TryGetItemActionState(_action.ActionType, _item, out actionState);
+ }
+
+ public void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn)
+ {
+ _itemActions.Toggle(_action.ActionType, toggleOn);
+ }
+
+#pragma warning disable 618
+ public ComponentMessage PerformInstantActionMessage()
+#pragma warning restore 618
+ {
+ return new PerformInstantItemActionMessage(_action.ActionType, _item);
+ }
+
+#pragma warning disable 618
+ public ComponentMessage PerformToggleActionMessage(bool toggleOn)
+#pragma warning restore 618
+ {
+ if (toggleOn)
+ {
+ return new PerformToggleOnItemActionMessage(_action.ActionType, _item);
+ }
+ return new PerformToggleOffItemActionMessage(_action.ActionType, _item);
+ }
+
+#pragma warning disable 618
+ public ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
+#pragma warning restore 618
+ {
+ return new PerformTargetPointItemActionMessage(_action.ActionType, _item, args.Coordinates);
+ }
+
+#pragma warning disable 618
+ public ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
+#pragma warning restore 618
+ {
+ return new PerformTargetEntityItemActionMessage(_action.ActionType, _item, args.EntityUid);
+ }
+
+ public override string ToString()
+ {
+ return $"{nameof(_action)}: {_action.ActionType}, {nameof(_item)}: {_item}";
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Prototypes/ActionPrototype.cs b/Content.Shared/Actions/Prototypes/ActionPrototype.cs
new file mode 100644
index 0000000000..6288e0f35e
--- /dev/null
+++ b/Content.Shared/Actions/Prototypes/ActionPrototype.cs
@@ -0,0 +1,102 @@
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Module;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Shared.Actions.Prototypes
+{
+ ///
+ /// An action which is granted directly to an entity (such as an innate ability
+ /// or skill).
+ ///
+ [Prototype("action")]
+ [DataDefinition]
+ public sealed class ActionPrototype : BaseActionPrototype, ISerializationHooks
+ {
+ ///
+ /// Type of action, no 2 action prototypes should have the same one.
+ ///
+ [DataField("actionType", required: true)]
+ public ActionType ActionType { get; set; }
+
+ [DataField("behavior", serverOnly: true)]
+ private IActionBehavior? Behavior { get; set; }
+
+ ///
+ /// The IInstantAction that should be invoked when performing this
+ /// action. Null if this is not an Instant ActionBehaviorType.
+ /// Will be null on client side if the behavior is not in Content.Client.
+ ///
+ public IInstantAction InstantAction { get; private set; } = default!;
+
+ ///
+ /// The IToggleAction that should be invoked when performing this
+ /// action. Null if this is not a Toggle ActionBehaviorType.
+ /// Will be null on client side if the behavior is not in Content.Client.
+ ///
+ public IToggleAction ToggleAction { get; private set; } = default!;
+
+ ///
+ /// The ITargetEntityAction that should be invoked when performing this
+ /// action. Null if this is not a TargetEntity ActionBehaviorType.
+ /// Will be null on client side if the behavior is not in Content.Client.
+ ///
+ public ITargetEntityAction TargetEntityAction { get; private set; } = default!;
+
+ ///
+ /// The ITargetPointAction that should be invoked when performing this
+ /// action. Null if this is not a TargetPoint ActionBehaviorType.
+ /// Will be null on client side if the behavior is not in Content.Client.
+ ///
+ public ITargetPointAction TargetPointAction { get; private set; } = default!;
+
+ public override string ID => ActionType.ToString();
+
+ void ISerializationHooks.AfterDeserialization()
+ {
+ base.AfterDeserialization();
+
+ if (ActionType == ActionType.Error)
+ {
+ Logger.ErrorS("action", "missing or invalid actionType for action with name {0}", Name);
+ }
+
+ if (IoCManager.Resolve().IsClientModule) return;
+
+ switch (Behavior)
+ {
+ case null:
+ BehaviorType = BehaviorType.None;
+ Logger.ErrorS("action", "missing or invalid behavior for action with name {0}", Name);
+ break;
+ case IInstantAction instantAction:
+ ValidateBehaviorType(BehaviorType.Instant, typeof(IInstantAction));
+ BehaviorType = BehaviorType.Instant;
+ InstantAction = instantAction;
+ break;
+ case IToggleAction toggleAction:
+ ValidateBehaviorType(BehaviorType.Toggle, typeof(IToggleAction));
+ BehaviorType = BehaviorType.Toggle;
+ ToggleAction = toggleAction;
+ break;
+ case ITargetEntityAction targetEntity:
+ ValidateBehaviorType(BehaviorType.TargetEntity, typeof(ITargetEntityAction));
+ BehaviorType = BehaviorType.TargetEntity;
+ TargetEntityAction = targetEntity;
+ break;
+ case ITargetPointAction targetPointAction:
+ ValidateBehaviorType(BehaviorType.TargetPoint, typeof(ITargetPointAction));
+ BehaviorType = BehaviorType.TargetPoint;
+ TargetPointAction = targetPointAction;
+ break;
+ default:
+ BehaviorType = BehaviorType.None;
+ Logger.ErrorS("action", "unrecognized behavior type for action with name {0}", Name);
+ break;
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Actions/Prototypes/BaseActionPrototype.cs b/Content.Shared/Actions/Prototypes/BaseActionPrototype.cs
new file mode 100644
index 0000000000..671e40c3ac
--- /dev/null
+++ b/Content.Shared/Actions/Prototypes/BaseActionPrototype.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using Robust.Shared.Log;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Utility;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Shared.Actions.Prototypes
+{
+ ///
+ /// Base class for action prototypes.
+ ///
+ [ImplicitDataDefinitionForInheritors]
+ public abstract class BaseActionPrototype : IPrototype, ISerializationHooks
+ {
+ public abstract string ID { get; }
+
+ ///
+ /// Icon representing this action in the UI.
+ ///
+ [ViewVariables]
+ [DataField("icon")]
+ public SpriteSpecifier Icon { get; } = SpriteSpecifier.Invalid;
+
+ ///
+ /// For toggle actions only, icon to show when toggled on. If omitted,
+ /// the action will simply be highlighted when turned on.
+ ///
+ [ViewVariables]
+ [DataField("iconOn")]
+ public SpriteSpecifier IconOn { get; } = SpriteSpecifier.Invalid;
+
+ ///
+ /// Name to show in UI. Accepts formatting.
+ ///
+ [DataField("name")]
+ public FormattedMessage Name { get; private set; } = new();
+
+ ///
+ /// Description to show in UI. Accepts formatting.
+ ///
+ [DataField("description")]
+ public FormattedMessage Description { get; } = new();
+
+ ///
+ /// Requirements message to show in UI. Accepts formatting, but generally should be avoided
+ /// so the requirements message isn't too prominent in the tooltip.
+ ///
+ [DataField("requires")]
+ public string Requires { get; } = string.Empty;
+
+ ///
+ /// The type of behavior this action has. This is valid clientside and serverside.
+ ///
+ [DataField("behaviorType")]
+ public BehaviorType BehaviorType { get; protected set; } = BehaviorType.None;
+
+ ///
+ /// For targetpoint or targetentity actions, if this is true the action will remain
+ /// selected after it is used, so it can be continuously re-used. If this is false,
+ /// the action will be deselected after one use.
+ ///
+ [DataField("repeat")]
+ public bool Repeat { get; }
+
+ ///
+ /// For TargetEntity/TargetPoint actions, should the action be de-selected if currently selected (choosing a target)
+ /// when it goes on cooldown. Defaults to false.
+ ///
+ [DataField("deselectOnCooldown")]
+ public bool DeselectOnCooldown { get; }
+
+ ///
+ /// For TargetEntity actions, should the action be de-selected if the user doesn't click an entity when
+ /// selecting a target. Defaults to false.
+ ///
+ [DataField("deselectWhenEntityNotClicked")]
+ public bool DeselectWhenEntityNotClicked { get; }
+
+ [DataField("filters")] private List _filters = new();
+
+ ///
+ /// Filters that can be used to filter this item in action menu.
+ ///
+ public IEnumerable Filters => _filters;
+
+ [DataField("keywords")] private List _keywords = new();
+
+ ///
+ /// Keywords that can be used to search this item in action menu.
+ ///
+ public IEnumerable Keywords => _keywords;
+
+ ///
+ /// True if this is an action that requires selecting a target
+ ///
+ public bool IsTargetAction =>
+ BehaviorType is BehaviorType.TargetEntity or BehaviorType.TargetPoint;
+
+ public virtual void AfterDeserialization()
+ {
+ Name = new FormattedMessage();
+ Name.AddText(ID);
+
+ if (BehaviorType == BehaviorType.None)
+ {
+ Logger.ErrorS("action", $"Missing behaviorType for action with name {Name}");
+ }
+
+ if (BehaviorType is not (BehaviorType.Toggle or BehaviorType.TargetEntity or BehaviorType.TargetPoint) && IconOn != SpriteSpecifier.Invalid)
+ {
+ Logger.ErrorS("action", $"for action {Name}, iconOn was specified but behavior" +
+ " $type was {BehaviorType}. iconOn is only supported for Toggle behavior type.");
+ }
+
+ if (Repeat && BehaviorType != BehaviorType.TargetEntity && BehaviorType != BehaviorType.TargetPoint)
+ {
+ Logger.ErrorS("action", $" action named {Name} used repeat: true, but this is only supported for" +
+ " $TargetEntity and TargetPoint behaviorType and its behaviorType is {BehaviorType}");
+ }
+ }
+
+ protected void ValidateBehaviorType(BehaviorType expected, Type actualInterface)
+ {
+ if (BehaviorType != expected)
+ {
+ Logger.ErrorS("action", $"for action named {Name}, behavior implements " +
+ $"{actualInterface.Name}, so behaviorType should be {expected} but was {BehaviorType}");
+ }
+ }
+ }
+
+ ///
+ /// The behavior / logic of the action. Each of these corresponds to a particular IActionBehavior
+ /// (for actions) or IItemActionBehavior (for item actions)
+ /// interface. Corresponds to action.behaviorType in YAML
+ ///
+ public enum BehaviorType
+ {
+ ///
+ /// Action doesn't do anything.
+ ///
+ None,
+
+ ///
+ /// IInstantAction/IInstantItemAction. Action which does something immediately when used and has
+ /// no target.
+ ///
+ Instant,
+
+ ///
+ /// IToggleAction/IToggleItemAction Action which can be toggled on and off
+ ///
+ Toggle,
+
+ ///
+ /// ITargetEntityAction/ITargetEntityItemAction. Action which is used on a targeted entity.
+ ///
+ TargetEntity,
+
+ ///
+ /// ITargetPointAction/ITargetPointItemAction. Action which requires the user to select a target point, which
+ /// does not necessarily have an entity on it.
+ ///
+ TargetPoint
+ }
+}
diff --git a/Content.Shared/Actions/Prototypes/ItemActionPrototype.cs b/Content.Shared/Actions/Prototypes/ItemActionPrototype.cs
new file mode 100644
index 0000000000..6a3d40358e
--- /dev/null
+++ b/Content.Shared/Actions/Prototypes/ItemActionPrototype.cs
@@ -0,0 +1,125 @@
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Actions.Behaviors.Item;
+using Content.Shared.Module;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Shared.Actions.Prototypes
+{
+ ///
+ /// An action which is granted to an entity via an item (such as toggling a flashlight).
+ ///
+ [Prototype("itemAction")]
+ [DataDefinition]
+ public sealed class ItemActionPrototype : BaseActionPrototype, ISerializationHooks
+ {
+ ///
+ /// Type of item action, no 2 itemAction prototypes should have the same one.
+ ///
+ [DataField("actionType")]
+ public ItemActionType ActionType { get; private set; } = ItemActionType.Error;
+
+ ///
+ [DataField("iconStyle")]
+ public ItemActionIconStyle IconStyle { get; private set; } = ItemActionIconStyle.BigItem;
+
+ ///
+ /// The IInstantItemAction that should be invoked when performing this
+ /// action. Null if this is not an Instant ActionBehaviorType.
+ /// Will be null on client side if the behavior is not in Content.Client.
+ ///
+ public IInstantItemAction InstantAction { get; private set; } = default!;
+
+ ///
+ /// The IToggleItemAction that should be invoked when performing this
+ /// action. Null if this is not a Toggle ActionBehaviorType.
+ /// Will be null on client side if the behavior is not in Content.Client.
+ ///
+ public IToggleItemAction ToggleAction { get; private set; } = default!;
+
+ ///
+ /// The ITargetEntityItemAction that should be invoked when performing this
+ /// action. Null if this is not a TargetEntity ActionBehaviorType.
+ /// Will be null on client side if the behavior is not in Content.Client.
+ ///
+ public ITargetEntityItemAction TargetEntityAction { get; private set; } = default!;
+
+ ///
+ /// The ITargetPointItemAction that should be invoked when performing this
+ /// action. Null if this is not a TargetPoint ActionBehaviorType.
+ /// Will be null on client side if the behavior is not in Content.Client.
+ ///
+ public ITargetPointItemAction TargetPointAction { get; private set; } = default!;
+
+ [DataField("behavior", readOnly: true, serverOnly: true)]
+ public IItemActionBehavior? ItemActionBehavior { get; private set; }
+
+ public override string ID => ActionType.ToString();
+
+ public override void AfterDeserialization()
+ {
+ base.AfterDeserialization();
+ if (ActionType == ItemActionType.Error)
+ {
+ Logger.ErrorS("action", "missing or invalid actionType for action with name {0}", Name);
+ }
+
+ // TODO: Split this class into server/client after RobustToolbox#1405
+ if (IoCManager.Resolve().IsClientModule) return;
+
+ switch (ItemActionBehavior)
+ {
+ case null:
+ BehaviorType = BehaviorType.None;
+ Logger.ErrorS("action", "missing or invalid behavior for action with name {0}", Name);
+ break;
+ case IInstantItemAction instantAction:
+ ValidateBehaviorType(BehaviorType.Instant, typeof(IInstantItemAction));
+ BehaviorType = BehaviorType.Instant;
+ InstantAction = instantAction;
+ break;
+ case IToggleItemAction toggleAction:
+ ValidateBehaviorType(BehaviorType.Toggle, typeof(IToggleItemAction));
+ BehaviorType = BehaviorType.Toggle;
+ ToggleAction = toggleAction;
+ break;
+ case ITargetEntityItemAction targetEntity:
+ ValidateBehaviorType(BehaviorType.TargetEntity, typeof(ITargetEntityItemAction));
+ BehaviorType = BehaviorType.TargetEntity;
+ TargetEntityAction = targetEntity;
+ break;
+ case ITargetPointItemAction targetPointAction:
+ ValidateBehaviorType(BehaviorType.TargetPoint, typeof(ITargetPointItemAction));
+ BehaviorType = BehaviorType.TargetPoint;
+ TargetPointAction = targetPointAction;
+ break;
+ default:
+ BehaviorType = BehaviorType.None;
+ Logger.ErrorS("action", "unrecognized behavior type for action with name {0}", Name);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Determines how the action icon appears in the hotbar for item actions.
+ ///
+ public enum ItemActionIconStyle : byte
+ {
+ ///
+ /// The default - the item icon will be big with a small action icon in the corner
+ ///
+ BigItem,
+ ///
+ /// The action icon will be big with a small item icon in the corner
+ ///
+ BigAction,
+ ///
+ /// BigAction but no item icon will be shown in the corner.
+ ///
+ NoItem
+ }
+}
diff --git a/Content.Shared/Actions/SharedActionSystem.cs b/Content.Shared/Actions/SharedActionSystem.cs
new file mode 100644
index 0000000000..df54099f83
--- /dev/null
+++ b/Content.Shared/Actions/SharedActionSystem.cs
@@ -0,0 +1,74 @@
+using Content.Shared.Actions.Components;
+using Content.Shared.Hands;
+using Content.Shared.Inventory.Events;
+using Robust.Shared.GameObjects;
+using System;
+
+namespace Content.Shared.Actions
+{
+ ///
+ /// Evicts action states with expired cooldowns.
+ ///
+ public sealed class SharedActionSystem : EntitySystem
+ {
+ private const float CooldownCheckIntervalSeconds = 10;
+ private float _timeSinceCooldownCheck;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ UpdatesOutsidePrediction = true;
+ SubscribeLocalEvent(OnGotEquipped);
+ SubscribeLocalEvent(OnHandEquipped);
+ SubscribeLocalEvent((uid, comp, _) => OnUnequipped(uid, comp));
+ SubscribeLocalEvent((uid, comp, _) => OnUnequipped(uid, comp));
+ }
+
+ private void OnGotEquipped(EntityUid uid, ItemActionsComponent component, GotEquippedEvent args)
+ {
+ if (!TryComp(args.Equipee, out SharedActionsComponent? actionsComponent))
+ return;
+
+ component.Holder = args.Equipee;
+ component.HolderActionsComponent = actionsComponent;
+ component.IsEquipped = true;
+ component.GrantOrUpdateAllToHolder();
+ }
+
+ private void OnHandEquipped(EntityUid uid, ItemActionsComponent component, EquippedHandEvent args)
+ {
+ if (!TryComp(args.User, out SharedActionsComponent? actionsComponent))
+ return;
+
+ component.Holder = args.User;
+ component.HolderActionsComponent = actionsComponent;
+ component.IsEquipped = true;
+ component.InHand = args.Hand;
+ component.GrantOrUpdateAllToHolder();
+ }
+
+ private void OnUnequipped(EntityUid uid, ItemActionsComponent component)
+ {
+ component.RevokeAllFromHolder();
+ component.Holder = null;
+ component.HolderActionsComponent = null;
+ component.IsEquipped = false;
+ component.InHand = null;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ _timeSinceCooldownCheck += frameTime;
+ if (_timeSinceCooldownCheck < CooldownCheckIntervalSeconds) return;
+
+ foreach (var comp in EntityManager.EntityQuery(false))
+ {
+ comp.ExpireCooldowns();
+ }
+ _timeSinceCooldownCheck -= CooldownCheckIntervalSeconds;
+ }
+ }
+}
diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs
deleted file mode 100644
index 458c9b8bc6..0000000000
--- a/Content.Shared/Actions/SharedActionsSystem.cs
+++ /dev/null
@@ -1,459 +0,0 @@
-using Content.Shared.ActionBlocker;
-using Content.Shared.Actions.ActionTypes;
-using Content.Shared.Administration.Logs;
-using Content.Shared.Database;
-using Content.Shared.Hands;
-using Content.Shared.Interaction;
-using Content.Shared.Inventory.Events;
-using Content.Shared.Popups;
-using Robust.Shared.Audio;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
-using Robust.Shared.Timing;
-using System.Linq;
-
-namespace Content.Shared.Actions;
-
-public abstract class SharedActionsSystem : EntitySystem
-{
- [Dependency] private readonly SharedAdminLogSystem _logSystem = default!;
- [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
- [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
- [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
- [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
- [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
- [Dependency] protected readonly IGameTiming GameTiming = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnDidEquip);
- SubscribeLocalEvent(OnHandEquipped);
- SubscribeLocalEvent(OnDidUnequip);
- SubscribeLocalEvent(OnHandUnequipped);
-
- SubscribeLocalEvent(GetState);
- SubscribeLocalEvent(OnCanGetState);
-
- SubscribeAllEvent(OnActionRequest);
- }
-
- #region ComponentStateManagement
- protected virtual void Dirty(ActionType action)
- {
- if (action.AttachedEntity == null)
- return;
-
- if (!TryComp(action.AttachedEntity, out ActionsComponent? comp))
- {
- action.AttachedEntity = null;
- return;
- }
-
- Dirty(comp);
- }
-
- public void SetToggled(ActionType action, bool toggled)
- {
- if (action.Toggled == toggled)
- return;
-
- action.Toggled = toggled;
- Dirty(action);
- }
-
- public void SetEnabled(ActionType action, bool enabled)
- {
- if (action.Enabled == enabled)
- return;
-
- action.Enabled = enabled;
- Dirty(action);
- }
-
- public void SetCharges(ActionType action, int? charges)
- {
- if (action.Charges == charges)
- return;
-
- action.Charges = charges;
- Dirty(action);
- }
-
- private void GetState(EntityUid uid, ActionsComponent component, ref ComponentGetState args)
- {
- args.State = new ActionsComponentState(component.Actions.ToList());
- }
-
- private void OnCanGetState(EntityUid uid, ActionsComponent component, ref ComponentGetStateAttemptEvent args)
- {
- // Only send action state data to the relevant player.
- if (args.Player.AttachedEntity != uid)
- args.Cancelled = true;
- }
- #endregion
-
- #region Execution
- ///
- /// When receiving a request to perform an action, this validates whether the action is allowed. If it is, it
- /// will raise the relevant
- ///
- private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArgs args)
- {
- if (args.SenderSession.AttachedEntity is not EntityUid user)
- return;
-
- if (!TryComp(user, out ActionsComponent? component))
- return;
-
- // Does the user actually have the requested action?
- if (!component.Actions.TryGetValue(ev.Action, out var act))
- {
- _logSystem.Add(LogType.Action,
- $"{ToPrettyString(user):user} attempted to perform an action that they do not have: {ev.Action.Name}.");
- return;
- }
-
- if (!act.Enabled)
- return;
-
- var curTime = GameTiming.CurTime;
- if (act.Cooldown.HasValue && act.Cooldown.Value.End > curTime)
- return;
-
- PerformActionEvent? performEvent = null;
-
- // Validate request by checking action blockers and the like:
- var name = Loc.GetString(act.Name);
-
- switch (act)
- {
- case EntityTargetAction entityAction:
-
- if (ev.EntityTarget is not EntityUid { Valid: true } entityTarget)
- {
- Logger.Error($"Attempted to perform an entity-targeted action without a target! Action: {entityAction.Name}");
- return;
- }
-
- _rotateToFaceSystem.TryFaceCoordinates(user, Transform(entityTarget).WorldPosition);
-
- if (!ValidateEntityTarget(user, entityTarget, entityAction))
- return;
-
- if (act.Provider == null)
- _logSystem.Add(LogType.Action,
- $"{ToPrettyString(user):user} is performing the {name:action} action targeted at {ToPrettyString(entityTarget):target}.");
- else
- _logSystem.Add(LogType.Action,
- $"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(act.Provider.Value):provider}) targeted at {ToPrettyString(entityTarget):target}.");
-
- if (entityAction.Event != null)
- {
- entityAction.Event.Target = entityTarget;
- performEvent = entityAction.Event;
- }
-
- break;
-
- case WorldTargetAction worldAction:
-
- if (ev.MapTarget is not MapCoordinates mapTarget)
- {
- Logger.Error($"Attempted to perform a map-targeted action without a target! Action: {worldAction.Name}");
- return;
- }
-
- _rotateToFaceSystem.TryFaceCoordinates(user, mapTarget.Position);
-
- if (!ValidateWorldTarget(user, mapTarget, worldAction))
- return;
-
- if (act.Provider == null)
- _logSystem.Add(LogType.Action,
- $"{ToPrettyString(user):user} is performing the {name:action} action targeted at {mapTarget:target}.");
- else
- _logSystem.Add(LogType.Action,
- $"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(act.Provider.Value):provider}) targeted at {mapTarget:target}.");
-
- if (worldAction.Event != null)
- {
- worldAction.Event.Target = mapTarget;
- performEvent = worldAction.Event;
- }
-
- break;
-
- case InstantAction instantAction:
-
- if (act.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
- return;
-
- if (act.Provider == null)
- _logSystem.Add(LogType.Action,
- $"{ToPrettyString(user):user} is performing the {name:action} action.");
- else
- _logSystem.Add(LogType.Action,
- $"{ToPrettyString(user):user} is performing the {name:action} action provided by {ToPrettyString(act.Provider.Value):provider}.");
-
- performEvent = instantAction.Event;
- break;
- }
-
- if (performEvent != null)
- performEvent.Performer = user;
-
- // All checks passed. Perform the action!
- PerformAction(component, act, performEvent, curTime);
- }
-
- public bool ValidateEntityTarget(EntityUid user, EntityUid target, EntityTargetAction action)
- {
- if (!target.IsValid() || Deleted(target))
- return false;
-
- if (action.Whitelist != null && !action.Whitelist.IsValid(target, EntityManager))
- return false;
-
- if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, target))
- return false;
-
- if (user == target)
- return action.CanTargetSelf;
-
- if (!action.CheckCanAccess)
- {
- // even if we don't check for obstructions, we may still need to check the range.
- var xform = Transform(user);
- var targetXform = Transform(target);
-
- if (xform.MapID != targetXform.MapID)
- return false;
-
- if (action.Range <= 0)
- return true;
-
- return (xform.WorldPosition - targetXform.WorldPosition).Length <= action.Range;
- }
-
- if (_interactionSystem.InRangeUnobstructed(user, target, range: action.Range)
- && _containerSystem.IsInSameOrParentContainer(user, target))
- {
- return true;
- }
-
- return _interactionSystem.CanAccessViaStorage(user, target);
- }
-
- public bool ValidateWorldTarget(EntityUid user, MapCoordinates coords, WorldTargetAction action)
- {
- if (coords == MapCoordinates.Nullspace)
- return false;
-
- if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
- return false;
-
- if (!action.CheckCanAccess)
- {
- // even if we don't check for obstructions, we may still need to check the range.
- var xform = Transform(user);
-
- if (xform.MapID != coords.MapId)
- return false;
-
- if (action.Range <= 0)
- return true;
-
- return (xform.WorldPosition - coords.Position).Length <= action.Range;
- }
-
- return _interactionSystem.InRangeUnobstructed(user, coords, range: action.Range);
- }
-
- protected void PerformAction(ActionsComponent component, ActionType action, PerformActionEvent? actionEvent, TimeSpan curTime)
- {
- var handled = false;
-
- var toggledBefore = action.Toggled;
-
- if (actionEvent != null)
- {
- // This here is required because of client-side prediction (RaisePredictiveEvent results in event re-use).
- actionEvent.Handled = false;
-
- if (action.Provider == null)
- RaiseLocalEvent(component.Owner, (object) actionEvent, broadcast: true);
- else
- RaiseLocalEvent(action.Provider.Value, (object) actionEvent, broadcast: true);
-
- handled = actionEvent.Handled;
- }
-
- // Execute convenience functionality (pop-ups, sound, speech)
- handled |= PerformBasicActions(component.Owner, action);
-
- if (!handled)
- return; // no interaction occurred.
-
- // reduce charges, start cooldown, and mark as dirty (if required).
-
- var dirty = toggledBefore == action.Toggled;
-
- if (action.Charges != null)
- {
- dirty = true;
- action.Charges--;
- if (action.Charges == 0)
- action.Enabled = false;
- }
-
- action.Cooldown = null;
- if (action.UseDelay != null)
- {
- dirty = true;
- action.Cooldown = (curTime, curTime + action.UseDelay.Value);
- }
-
- if (dirty)
- Dirty(component);
- }
-
- ///
- /// Execute convenience functionality for actions (pop-ups, sound, speech)
- ///
- protected virtual bool PerformBasicActions(EntityUid performer, ActionType action)
- {
- if (action.Sound == null && string.IsNullOrWhiteSpace(action.Popup))
- return false;
-
- var filter = Filter.Pvs(performer).RemoveWhereAttachedEntity(e => e == performer);
-
- if (action.Sound != null)
- SoundSystem.Play(filter, action.Sound.GetSound(), performer, action.AudioParams);
-
- if (string.IsNullOrWhiteSpace(action.Popup))
- return true;
-
- var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
- ? Loc.GetString(action.Popup)
- : Loc.GetString(action.Popup + action.PopupToggleSuffix);
-
- _popupSystem.PopupEntity(msg, performer, filter);
-
- return true;
- }
- #endregion
-
- #region AddRemoveActions
- ///
- /// Add an action to an action component. If the entity has no action component, this will give them one.
- ///
- /// Entity to receive the actions
- /// The action to add
- /// The entity that enables these actions (e.g., flashlight). May be null (innate actions).
- public void AddAction(EntityUid uid, ActionType action, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
- {
- comp ??= EnsureComp(uid);
- action.Provider = provider;
- action.AttachedEntity = comp.Owner;
- AddActionInternal(comp, action);
-
- // for client-exclusive actions, the client shouldn't mark the comp as dirty. Otherwise that just leads to
- // unnecessary prediction resetting and state handling.
- if (dirty)
- Dirty(comp);
- }
-
- protected virtual void AddActionInternal(ActionsComponent comp, ActionType action)
- {
- comp.Actions.Add(action);
- }
-
- ///
- /// Add actions to an action component. If the entity has no action component, this will give them one.
- ///
- /// Entity to receive the actions
- /// The actions to add
- /// The entity that enables these actions (e.g., flashlight). May be null (innate actions).
- public virtual void AddActions(EntityUid uid, IEnumerable actions, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
- {
- comp ??= EnsureComp(uid);
-
- foreach (var action in actions)
- {
- AddAction(uid, action, provider, comp, false);
- }
-
- if (dirty)
- Dirty(comp);
- }
-
- ///
- /// Remove any actions that were enabled by some other entity. Useful when unequiping items that grant actions.
- ///
- public void RemoveProvidedActions(EntityUid uid, EntityUid provider, ActionsComponent? comp = null)
- {
- if (!Resolve(uid, ref comp, false))
- return;
-
- var provided = comp.Actions.Where(act => act.Provider == provider).ToList();
-
- RemoveActions(uid, provided, comp);
- }
-
- public virtual void RemoveActions(EntityUid uid, IEnumerable actions, ActionsComponent? comp = null, bool dirty = true)
- {
- if (!Resolve(uid, ref comp, false))
- return;
-
- foreach (var action in actions)
- {
- comp.Actions.Remove(action);
- action.AttachedEntity = null;
- }
-
- if (dirty)
- Dirty(comp);
- }
-
- public void RemoveAction(EntityUid uid, ActionType action, ActionsComponent? comp = null)
- => RemoveActions(uid, new[] { action }, comp);
- #endregion
-
- #region EquipHandlers
- private void OnDidEquip(EntityUid uid, ActionsComponent component, DidEquipEvent args)
- {
- var ev = new GetActionsEvent();
- RaiseLocalEvent(args.Equipment, ev, false);
-
- if (ev.Actions.Count == 0)
- return;
-
- AddActions(args.Equipee, ev.Actions, args.Equipment, component);
- }
-
- private void OnHandEquipped(EntityUid uid, ActionsComponent component, DidEquipHandEvent args)
- {
- var ev = new GetActionsEvent();
- RaiseLocalEvent(args.Equipped, ev, false);
-
- if (ev.Actions.Count == 0)
- return;
-
- AddActions(args.User, ev.Actions, args.Equipped, component);
- }
-
- private void OnDidUnequip(EntityUid uid, ActionsComponent component, DidUnequipEvent args)
- {
- RemoveProvidedActions(uid, args.Equipment, component);
- }
-
- private void OnHandUnequipped(EntityUid uid, ActionsComponent component, DidUnequipHandEvent args)
- {
- RemoveProvidedActions(uid, args.Unequipped, component);
- }
- #endregion
-}
diff --git a/Content.Shared/Clothing/SharedMagbootsComponent.cs b/Content.Shared/Clothing/SharedMagbootsComponent.cs
index b7073b40da..aeee4ec0e6 100644
--- a/Content.Shared/Clothing/SharedMagbootsComponent.cs
+++ b/Content.Shared/Clothing/SharedMagbootsComponent.cs
@@ -1,24 +1,22 @@
-using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
+using System;
+using Content.Shared.Movement.Components;
using Content.Shared.Movement.EntitySystems;
using Robust.Shared.Containers;
+using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
namespace Content.Shared.Clothing
{
[NetworkedComponent()]
public abstract class SharedMagbootsComponent : Component
{
- [DataField("toggleAction", required: true)]
- public InstantAction ToggleAction = default!;
-
public abstract bool On { get; set; }
protected void OnChanged()
{
- EntitySystem.Get().SetToggled(ToggleAction, On);
-
// inventory system will automatically hook into the event raised by this and update accordingly
if (Owner.TryGetContainer(out var container))
{
diff --git a/Content.Shared/Clothing/SharedMagbootsSystem.cs b/Content.Shared/Clothing/SharedMagbootsSystem.cs
deleted file mode 100644
index 21e132d1ff..0000000000
--- a/Content.Shared/Clothing/SharedMagbootsSystem.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Content.Shared.Actions;
-using Content.Shared.Toggleable;
-
-namespace Content.Shared.Clothing;
-
-public abstract class SharedMagbootsSystem : EntitySystem
-{
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnGetActions);
- SubscribeLocalEvent(OnToggleAction);
- }
-
- private void OnGetActions(EntityUid uid, SharedMagbootsComponent component, GetActionsEvent args)
- {
- args.Actions.Add(component.ToggleAction);
- }
-
- private void OnToggleAction(EntityUid uid, SharedMagbootsComponent component, ToggleActionEvent args)
- {
- if (args.Handled)
- return;
-
- component.On = !component.On;
-
- args.Handled = true;
- }
-}
diff --git a/Content.Shared/CombatMode/SharedCombatModeComponent.cs b/Content.Shared/CombatMode/SharedCombatModeComponent.cs
index a52d1fbc97..3a5e961ae1 100644
--- a/Content.Shared/CombatMode/SharedCombatModeComponent.cs
+++ b/Content.Shared/CombatMode/SharedCombatModeComponent.cs
@@ -1,11 +1,10 @@
using System;
-using Content.Shared.Actions;
-using Content.Shared.Actions.ActionTypes;
-using Content.Shared.Sound;
using Content.Shared.Targeting;
+using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
+using Robust.Shared.Players;
using Robust.Shared.Serialization;
-using Robust.Shared.Utility;
+using Robust.Shared.ViewVariables;
namespace Content.Shared.CombatMode
{
@@ -15,50 +14,6 @@ namespace Content.Shared.CombatMode
private bool _isInCombatMode;
private TargetingZone _activeZone;
- [DataField("disarmFailChance")]
- public readonly float DisarmFailChance = 0.4f;
-
- [DataField("pushChance")]
- public readonly float DisarmPushChance = 0.4f;
-
- [DataField("disarmFailSound")]
- public readonly SoundSpecifier DisarmFailSound = new SoundPathSpecifier("/Audio/Weapons/punchmiss.ogg");
-
- [DataField("disarmSuccessSound")]
- public readonly SoundSpecifier DisarmSuccessSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
-
- // These are chonky default definitions for combat actions. But its a pain to add a yaml version of this for
- // every entity that wants combat mode, especially given that they're currently all identical... so ummm.. yeah.
- [DataField("disarmAction")]
- public readonly EntityTargetAction DisarmAction = new()
- {
- Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/disarmOff.png")),
- IconOn = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/disarm.png")),
- Name = "action-name-disarm",
- Description = "action-description-disarm",
- Repeat = true,
- UseDelay = TimeSpan.FromSeconds(1.5f),
- InteractOnMiss = true,
- Event = new DisarmActionEvent(),
- CanTargetSelf = false,
- Whitelist = new()
- {
- Components = new[] { "Hands", "StatusEffects" },
- },
- };
-
- [DataField("combatToggleAction")]
- public readonly InstantAction CombatToggleAction = new()
- {
- Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/harmOff.png")),
- IconOn = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/harm.png")),
- UserPopup = "action-popup-combat",
- PopupToggleSuffix = "-disabling",
- Name = "action-name-combat",
- Description = "action-description-combat",
- Event = new ToggleCombatActionEvent(),
- };
-
[ViewVariables(VVAccess.ReadWrite)]
public virtual bool IsInCombatMode
{
@@ -67,7 +22,6 @@ namespace Content.Shared.CombatMode
{
if (_isInCombatMode == value) return;
_isInCombatMode = value;
- EntitySystem.Get().SetToggled(CombatToggleAction, _isInCombatMode);
Dirty();
// Regenerate physics contacts -> Can probably just selectively check
diff --git a/Content.Shared/CombatMode/SharedCombatModeSystem.cs b/Content.Shared/CombatMode/SharedCombatModeSystem.cs
index 3092314847..3cdb3231d0 100644
--- a/Content.Shared/CombatMode/SharedCombatModeSystem.cs
+++ b/Content.Shared/CombatMode/SharedCombatModeSystem.cs
@@ -1,42 +1,15 @@
-using Content.Shared.Actions;
+using Robust.Shared.GameObjects;
namespace Content.Shared.CombatMode
{
public abstract class SharedCombatModeSystem : EntitySystem
{
- [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
-
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent(CombatModeActiveHandler);
SubscribeLocalEvent(CombatModeActiveHandler);
-
- SubscribeLocalEvent(OnStartup);
- SubscribeLocalEvent(OnShutdown);
- SubscribeLocalEvent(OnActionPerform);
- }
-
- private void OnStartup(EntityUid uid, SharedCombatModeComponent component, ComponentStartup args)
- {
- _actionsSystem.AddAction(uid, component.CombatToggleAction, null);
- _actionsSystem.AddAction(uid, component.DisarmAction, null);
- }
-
- private void OnShutdown(EntityUid uid, SharedCombatModeComponent component, ComponentShutdown args)
- {
- _actionsSystem.RemoveAction(uid, component.CombatToggleAction);
- _actionsSystem.RemoveAction(uid, component.DisarmAction);
- }
-
- private void OnActionPerform(EntityUid uid, SharedCombatModeComponent component, ToggleCombatActionEvent args)
- {
- if (args.Handled)
- return;
-
- component.IsInCombatMode = !component.IsInCombatMode;
- args.Handled = true;
}
private void CombatModeActiveHandler(CombatModeSystemMessages.SetCombatModeActiveMessage ev, EntitySessionEventArgs eventArgs)
@@ -51,7 +24,4 @@ namespace Content.Shared.CombatMode
combatModeComponent.IsInCombatMode = ev.Active;
}
}
-
- public sealed class ToggleCombatActionEvent : PerformActionEvent { }
- public sealed class DisarmActionEvent : PerformEntityTargetActionEvent { }
}
diff --git a/Content.Shared/Hands/HandEvents.cs b/Content.Shared/Hands/HandEvents.cs
index df29474cc5..1ecba28b93 100644
--- a/Content.Shared/Hands/HandEvents.cs
+++ b/Content.Shared/Hands/HandEvents.cs
@@ -104,7 +104,7 @@ namespace Content.Shared.Hands
/// Raised when putting an entity into a hand slot
///
[PublicAPI]
- public abstract class EquippedHandEvent : HandledEntityEventArgs
+ public sealed class EquippedHandEvent : HandledEntityEventArgs
{
///
/// Entity that equipped the item.
@@ -133,7 +133,7 @@ namespace Content.Shared.Hands
/// Raised when removing an entity from an inventory slot.
///
[PublicAPI]
- public abstract class UnequippedHandEvent : HandledEntityEventArgs
+ public sealed class UnequippedHandEvent : HandledEntityEventArgs
{
///
/// Entity that equipped the item.
@@ -157,24 +157,4 @@ namespace Content.Shared.Hands
Hand = hand;
}
}
-
- public sealed class GotEquippedHandEvent : EquippedHandEvent
- {
- public GotEquippedHandEvent(EntityUid user, EntityUid unequipped, Hand hand) : base(user, unequipped, hand) { }
- }
-
- public sealed class GotUnequippedHandEvent : UnequippedHandEvent
- {
- public GotUnequippedHandEvent(EntityUid user, EntityUid unequipped, Hand hand) : base(user, unequipped, hand) { }
- }
-
- public sealed class DidEquipHandEvent : EquippedHandEvent
- {
- public DidEquipHandEvent(EntityUid user, EntityUid unequipped, Hand hand) : base(user, unequipped, hand) { }
- }
-
- public sealed class DidUnequipHandEvent : UnequippedHandEvent
- {
- public DidUnequipHandEvent(EntityUid user, EntityUid unequipped, Hand hand) : base(user, unequipped, hand) { }
- }
}
diff --git a/Content.Shared/Hands/SharedHandsSystem.cs b/Content.Shared/Hands/SharedHandsSystem.cs
index 151095bac4..83804fb8e0 100644
--- a/Content.Shared/Hands/SharedHandsSystem.cs
+++ b/Content.Shared/Hands/SharedHandsSystem.cs
@@ -92,11 +92,10 @@ namespace Content.Shared.Hands
hands.Dirty();
- var gotUnequipped = new GotUnequippedHandEvent(uid, entity, hand);
- RaiseLocalEvent(entity, gotUnequipped, false);
-
- var didUnequip = new DidUnequipHandEvent(uid, entity, hand);
- RaiseLocalEvent(uid, didUnequip);
+ var unequippedHandMessage = new UnequippedHandEvent(uid, entity, hand);
+ RaiseLocalEvent(entity, unequippedHandMessage);
+ if (unequippedHandMessage.Handled)
+ return;
if (hand.Name == hands.ActiveHand)
RaiseLocalEvent(entity, new HandDeselectedEvent(uid, entity), false);
@@ -124,15 +123,11 @@ namespace Content.Shared.Hands
hands.Dirty();
- var didEquip = new DidEquipHandEvent(uid, entity, hand);
- RaiseLocalEvent(uid, didEquip, false);
+ var equippedHandMessage = new EquippedHandEvent(uid, entity, hand);
+ RaiseLocalEvent(entity, equippedHandMessage);
- var gotEquipped = new GotEquippedHandEvent(uid, entity, hand);
- RaiseLocalEvent(entity, gotEquipped);
-
- // TODO this should REALLY be a cancellable thing, not a handled event.
// If one of the interactions resulted in the item being dropped, return early.
- if (gotEquipped.Handled)
+ if (equippedHandMessage.Handled)
return;
if (hand.Name == hands.ActiveHand)
diff --git a/Content.Shared/Light/Component/SharedHandheldLightComponent.cs b/Content.Shared/Light/Component/SharedHandheldLightComponent.cs
index 5ede7633c8..7f15efc399 100644
--- a/Content.Shared/Light/Component/SharedHandheldLightComponent.cs
+++ b/Content.Shared/Light/Component/SharedHandheldLightComponent.cs
@@ -1,4 +1,5 @@
-using Content.Shared.Actions.ActionTypes;
+using System;
+using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
@@ -7,9 +8,6 @@ namespace Content.Shared.Light.Component
[NetworkedComponent]
public abstract class SharedHandheldLightComponent : Robust.Shared.GameObjects.Component
{
- [DataField("toggleAction", required: true)]
- public InstantAction ToggleAction = default!;
-
public const int StatusLevels = 6;
[Serializable, NetSerializable]
diff --git a/Content.Shared/PAI/PAIComponent.cs b/Content.Shared/PAI/PAIComponent.cs
index 62aea5c35e..eb86912f59 100644
--- a/Content.Shared/PAI/PAIComponent.cs
+++ b/Content.Shared/PAI/PAIComponent.cs
@@ -1,5 +1,6 @@
-using Content.Shared.Actions.ActionTypes;
+using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
namespace Content.Shared.PAI
{
@@ -16,8 +17,6 @@ namespace Content.Shared.PAI
[RegisterComponent, NetworkedComponent]
public sealed class PAIComponent : Component
{
- [DataField("midiAction", required: true, serverOnly: true)] // server only, as it uses a server-BUI event !type
- public InstantAction? MidiAction;
}
}
diff --git a/Content.Shared/Toggleable/ToggleActionEvent.cs b/Content.Shared/Toggleable/ToggleActionEvent.cs
deleted file mode 100644
index 31055b6c3f..0000000000
--- a/Content.Shared/Toggleable/ToggleActionEvent.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Content.Shared.Actions;
-
-namespace Content.Shared.Toggleable;
-
-///
-/// Generic action-event for toggle-able components.
-///
-public sealed class ToggleActionEvent : PerformActionEvent { }
diff --git a/Content.Shared/Whitelist/EntityWhitelist.cs b/Content.Shared/Whitelist/EntityWhitelist.cs
index e3ee4adcc8..96bd3080e2 100644
--- a/Content.Shared/Whitelist/EntityWhitelist.cs
+++ b/Content.Shared/Whitelist/EntityWhitelist.cs
@@ -25,7 +25,6 @@ namespace Content.Shared.Whitelist
/// - AsteroidRock
///
[DataDefinition]
- [Serializable, NetSerializable]
public sealed class EntityWhitelist : ISerializationHooks
{
///
@@ -33,7 +32,6 @@ namespace Content.Shared.Whitelist
///
[DataField("components")] public string[]? Components = null;
- [NonSerialized]
private List? _registrations = null;
///
diff --git a/Resources/Locale/en-US/actions/actions/combat-mode.ftl b/Resources/Locale/en-US/actions/actions/combat-mode.ftl
index 0dad773258..20bb5d22d9 100644
--- a/Resources/Locale/en-US/actions/actions/combat-mode.ftl
+++ b/Resources/Locale/en-US/actions/actions/combat-mode.ftl
@@ -1,6 +1,2 @@
-action-name-combat = [color=red]Combat Mode[/color]
-action-description-combat = Enter combat mode.
-
-
-action-popup-combat = Combat mode enabled!
-action-popup-combat-disabling = Combat mode disabled.
\ No newline at end of file
+hud-combat-enabled = Combat mode enabled!
+hud-combat-disabled = Combat mode disabled.
\ No newline at end of file
diff --git a/Resources/Locale/en-US/actions/actions/disarm-action.ftl b/Resources/Locale/en-US/actions/actions/disarm-action.ftl
index 2625ec336e..17ddf1c994 100644
--- a/Resources/Locale/en-US/actions/actions/disarm-action.ftl
+++ b/Resources/Locale/en-US/actions/actions/disarm-action.ftl
@@ -1,5 +1,2 @@
disarm-action-popup-message-other-clients = {$performerName} fails to disarm {$targetName}!
-disarm-action-popup-message-cursor = You fail to disarm {$targetName}!
-
-action-name-disarm = [color=red]Disarm[/color]
-action-description-disarm = Attempt to [color=red]disarm[/color] someone.
+disarm-action-popup-message-cursor = You fail to disarm {$targetName}!
\ No newline at end of file
diff --git a/Resources/Locale/en-US/actions/actions/ghost.ftl b/Resources/Locale/en-US/actions/actions/ghost.ftl
deleted file mode 100644
index f10c559adf..0000000000
--- a/Resources/Locale/en-US/actions/actions/ghost.ftl
+++ /dev/null
@@ -1,2 +0,0 @@
-action-name-boo = Boo!
-action-description-boo = Scare your crew members because of boredom!
\ No newline at end of file
diff --git a/Resources/Locale/en-US/actions/actions/internals.ftl b/Resources/Locale/en-US/actions/actions/internals.ftl
deleted file mode 100644
index cc24d80037..0000000000
--- a/Resources/Locale/en-US/actions/actions/internals.ftl
+++ /dev/null
@@ -1,2 +0,0 @@
-action-name-internals-toggle = Toggle Internals
-action-description-internals-toggle = Breathe from the equipped gas tank. Also requires equipped breath mask.
\ No newline at end of file
diff --git a/Resources/Locale/en-US/actions/actions/light.ftl b/Resources/Locale/en-US/actions/actions/light.ftl
deleted file mode 100644
index e883910870..0000000000
--- a/Resources/Locale/en-US/actions/actions/light.ftl
+++ /dev/null
@@ -1,2 +0,0 @@
-action-name-toggle-light = Toggle Light
-action-description-toggle-light = Turn the light on and off.
\ No newline at end of file
diff --git a/Resources/Locale/en-US/actions/actions/mapping.ftl b/Resources/Locale/en-US/actions/actions/mapping.ftl
deleted file mode 100644
index ce6c4d9e56..0000000000
--- a/Resources/Locale/en-US/actions/actions/mapping.ftl
+++ /dev/null
@@ -1 +0,0 @@
-action-name-mapping-erase = Erase Entity
\ No newline at end of file
diff --git a/Resources/Locale/en-US/actions/actions/pai.ftl b/Resources/Locale/en-US/actions/actions/pai.ftl
deleted file mode 100644
index 4be31f28c7..0000000000
--- a/Resources/Locale/en-US/actions/actions/pai.ftl
+++ /dev/null
@@ -1,2 +0,0 @@
-action-name-pai-play-midi = Play MIDI
-action-description-pai-play-midi = Open your portable MIDI interface to soothe your owner.
\ No newline at end of file
diff --git a/Resources/Locale/en-US/actions/actions/vocal.ftl b/Resources/Locale/en-US/actions/actions/vocal.ftl
deleted file mode 100644
index ced5c04565..0000000000
--- a/Resources/Locale/en-US/actions/actions/vocal.ftl
+++ /dev/null
@@ -1 +0,0 @@
-action-name-scream = Scream
\ No newline at end of file
diff --git a/Resources/Locale/en-US/actions/ui/actionslot.ftl b/Resources/Locale/en-US/actions/ui/actionslot.ftl
deleted file mode 100644
index 332054f10e..0000000000
--- a/Resources/Locale/en-US/actions/ui/actionslot.ftl
+++ /dev/null
@@ -1,2 +0,0 @@
-ui-actionslot-charges = Uses left: {$charges}
-
diff --git a/Resources/Locale/en-US/guardian/guardian.ftl b/Resources/Locale/en-US/guardian/guardian.ftl
index 48c14dc0e1..cf7ec019bc 100644
--- a/Resources/Locale/en-US/guardian/guardian.ftl
+++ b/Resources/Locale/en-US/guardian/guardian.ftl
@@ -2,6 +2,7 @@
## Guardian host specific
guardian-created = You feel... Haunted.
+guardian-missing-invalid-action = There is no guardian under your control!
guardian-already-present-invalid-creation = You are NOT re-living that haunting experience!
guardian-no-actions-invalid-creation = You don't have the ability to host a guardian!
guardian-activator-empty-invalid-creation = The injector is spent.
@@ -10,11 +11,6 @@ guardian-activator-invalid-target = Only humans can be injected! # Change this o
guardian-no-soul = Your guardian has no soul.
guardian-available = Your guardian now has a soul.
-# Guardian action
-
-action-name-guardian = Toggle guardian manifestation
-action-description-guardian = Either manifests the guardian or recalls it back into your body
-
## Guardian entity specific
guardian-entity-recall = The guardian vanishes into thin air!
diff --git a/Resources/Locale/en-US/magboot/actions.ftl b/Resources/Locale/en-US/magboot/actions.ftl
deleted file mode 100644
index 7f8f55fefb..0000000000
--- a/Resources/Locale/en-US/magboot/actions.ftl
+++ /dev/null
@@ -1,2 +0,0 @@
-action-name-magboot-toggle = Toggle Magboots
-action-decription-magboot-toggle = Toggles the magboots on and off.
\ No newline at end of file
diff --git a/Resources/Prototypes/Actions/actions.yml b/Resources/Prototypes/Actions/actions.yml
new file mode 100644
index 0000000000..0ad1aca925
--- /dev/null
+++ b/Resources/Prototypes/Actions/actions.yml
@@ -0,0 +1,147 @@
+- type: action
+ actionType: CombatMode
+ icon: Interface/Actions/harmOff.png
+ iconOn: Interface/Actions/harm.png
+ name: "[color=red]Combat Mode[/color]"
+ description: "Enter combat mode."
+ filters:
+ - human
+ behaviorType: Toggle
+ behavior: !type:CombatMode
+
+- type: action
+ actionType: Disarm
+ icon: Interface/Actions/disarmOff.png
+ iconOn: Interface/Actions/disarm.png
+ name: "[color=red]Disarm[/color]"
+ description: "Attempt to [color=red]disarm[/color] someone."
+ filters:
+ - human
+ behaviorType: TargetEntity
+ repeat: true
+ behavior: !type:DisarmAction { }
+
+- type: action
+ actionType: HumanScream
+ icon: Interface/Actions/scream.png
+ name: "Scream"
+ filters:
+ - human
+ behaviorType: Instant
+ behavior: !type:ScreamAction
+ cooldown: 10
+ male:
+ collection: MaleScreams
+ female:
+ collection: FemaleScreams
+ wilhelm:
+ path: /Audio/Voice/Human/wilhelm_scream.ogg
+
+- type: action
+ actionType: VoxScream
+ icon: Interface/Actions/scream.png
+ name: "Scream"
+ filters:
+ - vox
+ behaviorType: Instant
+ behavior: !type:ScreamAction
+ cooldown: 10
+ male:
+ path: /Audio/Voice/Vox/shriek1.ogg
+ female:
+ path: /Audio/Voice/Vox/shriek1.ogg
+ wilhelm:
+ path: /Audio/Voice/Human/wilhelm_scream.ogg
+
+- type: action
+ actionType: GhostBoo
+ icon: Interface/Actions/scream.png
+ name: "Boo"
+ description: "Scare your crew members because of boredom!"
+ filters:
+ - ghost
+ behaviorType: Instant
+ behavior: !type:GhostBoo
+ radius: 3
+ cooldown: 120
+ maxTargets: 3
+
+- type: action
+ actionType: PAIMidi
+ icon: Interface/Actions/pai-midi.png
+ name: "Play MIDI"
+ description: "Open your portable MIDI interface to soothe your owner."
+ filters:
+ - pai
+ - music
+ behaviorType: Instant
+ behavior: !type:PAIMidi
+
+- type: action
+ actionType: DebugInstant
+ icon: Interface/Alerts/human_health.rsi/health1.png
+ name: "[color=red]Debug Instant[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ requires: "Requires blah blah"
+ filters:
+ - debug
+ behaviorType: Instant
+ behavior: !type:DebugInstant
+ message: Instant action was used!
+
+- type: action
+ actionType: DebugToggle
+ icon: Interface/Alerts/human_health.rsi/health3.png
+ name: "[color=red]Debug Toggle[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ requires: "Requires blah blah"
+ filters:
+ - debug
+ behaviorType: Toggle
+ behavior: !type:DebugToggle
+ messageOn: Toggled on!
+ messageOff: Toggled off!
+
+- type: action
+ actionType: DebugTargetPoint
+ icon: Interface/Alerts/human_health.rsi/health4.png
+ name: "[color=red]Debug Target Position[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ filters:
+ - debug
+ behaviorType: TargetPoint
+ behavior: !type:DebugTargetPoint { }
+
+- type: action
+ actionType: DebugTargetPointRepeat
+ icon: Interface/Alerts/human_health.rsi/health2.png
+ name: "[color=red]Repeating Debug Target Position[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ filters:
+ - debug
+ behaviorType: TargetPoint
+ repeat: true
+ behavior: !type:DebugTargetPoint { }
+
+- type: action
+ actionType: DebugTargetEntity
+ icon: Interface/Alerts/human_health.rsi/health6.png
+ name: "[color=red]Debug Target Entity[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ filters:
+ - debug
+ behaviorType: TargetEntity
+ behavior: !type:DebugTargetEntity { }
+
+- type: action
+ actionType: DebugTargetEntityRepeat
+ icon: Interface/Alerts/human_health.rsi/health5.png
+ name: "[color=red]Repeating Debug Target Entity[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ filters:
+ - debug
+ behaviorType: TargetEntity
+ repeat: true
+ behavior: !type:DebugTargetEntity { }
+
+
diff --git a/Resources/Prototypes/Actions/guardian_actions.yml b/Resources/Prototypes/Actions/guardian_actions.yml
new file mode 100644
index 0000000000..1725cf62b8
--- /dev/null
+++ b/Resources/Prototypes/Actions/guardian_actions.yml
@@ -0,0 +1,12 @@
+#This is the action the host gets to control the guardian
+- type: action
+ actionType: ManifestGuardian
+ icon: Interface/Actions/manifest.png
+ name: "Toggle guardian manifestation"
+ description: "Either manifests the guardian or recalls it back into your body"
+ filters:
+ - guardian
+ behaviorType: Instant
+ behavior: !type:ToggleGuardianAction
+ cooldown: 2
+
diff --git a/Resources/Prototypes/Actions/item_actions.yml b/Resources/Prototypes/Actions/item_actions.yml
new file mode 100644
index 0000000000..d2eb257e33
--- /dev/null
+++ b/Resources/Prototypes/Actions/item_actions.yml
@@ -0,0 +1,125 @@
+- type: itemAction
+ actionType: ToggleInternals
+ icon: Interface/Actions/internal0.png
+ iconOn: Interface/Actions/internal1.png
+ name: "Toggle Internals"
+ description: "Breathe from the equipped gas tank."
+ requires: "Requires equipped breath mask and gas tank"
+ filters:
+ - common
+ - atmos
+ keywords:
+ - gas
+ - tank
+ - breath
+ behaviorType: Toggle
+ behavior: !type:ToggleInternalsAction { }
+
+- type: itemAction
+ actionType: ToggleLight
+ icon: Objects/Tools/flashlight.rsi/flashlight.png
+ iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png
+ name: "Toggle Light"
+ description: "Turn the light on."
+ filters:
+ - tools
+ keywords:
+ - lantern
+ - lamp
+ behaviorType: Toggle
+ behavior: !type:ToggleLightAction { }
+
+- type: itemAction
+ actionType: DebugInstant
+ icon: Interface/Alerts/human_health.rsi/health1.png
+ iconStyle: BigAction
+ name: "[color=red]Debug Item Instant[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ requires: "Requires blah blah"
+ filters:
+ - debug
+ behaviorType: Instant
+ behavior: !type:DebugInstant
+ message: Instant action was used!
+ cooldown: 10
+
+- type: itemAction
+ actionType: DebugToggle
+ iconStyle: BigItem
+ icon: Interface/Alerts/human_health.rsi/health3.png
+ name: "[color=red]Debug Item Toggle[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ requires: "Requires blah blah"
+ filters:
+ - debug
+ behaviorType: Toggle
+ behavior: !type:DebugToggle
+ messageOn: Toggled on!
+ messageOff: Toggled off!
+
+- type: itemAction
+ actionType: DebugTargetPoint
+ iconStyle: NoItem
+ icon: Interface/Alerts/human_health.rsi/health4.png
+ name: "[color=red]Debug Item Target Position[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ filters:
+ - debug
+ behaviorType: TargetPoint
+ behavior: !type:DebugTargetPoint { }
+
+- type: itemAction
+ actionType: DebugTargetPointRepeat
+ iconStyle: BigAction
+ icon: Interface/Alerts/human_health.rsi/health2.png
+ name: "[color=red]Repeating Debug Item Target Position[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ filters:
+ - debug
+ behaviorType: TargetPoint
+ repeat: true
+ behavior: !type:DebugTargetPoint { }
+
+- type: itemAction
+ actionType: DebugTargetEntity
+ iconStyle: BigAction
+ icon: Interface/Alerts/human_health.rsi/health6.png
+ name: "[color=red]Debug Item Target Entity[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ filters:
+ - debug
+ behaviorType: TargetEntity
+ behavior: !type:DebugTargetEntity { }
+
+- type: itemAction
+ actionType: DebugTargetEntityRepeat
+ icon: Interface/Alerts/human_health.rsi/health5.png
+ name: "[color=red]Repeating Debug Item Target Entity[/color]"
+ description: "This is a [color=red]debug message[/color]."
+ filters:
+ - debug
+ behaviorType: TargetEntity
+ repeat: true
+ behavior: !type:DebugTargetEntity { }
+
+- type: entity
+ name: item action example
+ parent: BaseItem
+ id: ItemActionExample
+ description: for testing item actions
+ suffix: DEBUG
+ components:
+ - type: Tag
+ tags:
+ - Debug
+ - type: Sprite
+ sprite: Objects/Fun/bikehorn.rsi
+ state: icon
+ - type: ItemActions
+ actions:
+ - actionType: DebugInstant
+ - actionType: DebugToggle
+ - actionType: DebugTargetPoint
+ - actionType: DebugTargetPointRepeat
+ - actionType: DebugTargetEntity
+ - actionType: DebugTargetEntityRepeat
diff --git a/Resources/Prototypes/Actions/magboots.yml b/Resources/Prototypes/Actions/magboots.yml
new file mode 100644
index 0000000000..1002510087
--- /dev/null
+++ b/Resources/Prototypes/Actions/magboots.yml
@@ -0,0 +1,13 @@
+- type: itemAction
+ actionType: ToggleMagboots
+ icon: Clothing/Shoes/Boots/magboots.rsi/icon.png
+ iconOn: Clothing/Shoes/Boots/magboots.rsi/icon-on.png
+ name: "Toggle Magboots"
+ description: "Turn your magboots on."
+ filters:
+ - tools
+ keywords:
+ - atmos
+ - air
+ behaviorType: Toggle
+ behavior: !type:ToggleMagbootsAction { }
diff --git a/Resources/Prototypes/Actions/spells.yml b/Resources/Prototypes/Actions/spells.yml
new file mode 100644
index 0000000000..f7a93149b8
--- /dev/null
+++ b/Resources/Prototypes/Actions/spells.yml
@@ -0,0 +1,14 @@
+- type: action
+ actionType: SpellPie
+ icon: Objects/Consumable/Food/Baked/pie.rsi/plain.png
+ name: "Pie"
+ filters:
+ - spells
+ description: "Give me a pie, I dare you!"
+ behaviorType: Instant
+ behavior: !type:GiveItemSpell
+ spellItem: FoodPieBananaCream
+ castMessage: I NEED A PIE!
+ cooldown: 15
+ castSound:
+ path: /Audio/Items/bikehorn.ogg
diff --git a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
index ec450d63db..315bf334ef 100644
--- a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
+++ b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
@@ -63,13 +63,12 @@
- type: Appearance
visuals:
- type: FlashLightVisualizer
+ - type: ItemActions
+ actions:
+ - actionType: ToggleLight
+ # right now there is no power system for hardsuits
+ # so hardsuit helmet just have small battery inside
- type: HandheldLight
- toggleAction:
- name: action-name-toggle-light
- description: action-description-toggle-light
- icon: Objects/Tools/flashlight.rsi/flashlight.png
- iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png
- event: !type:ToggleActionEvent
- type: PowerCellSlot
cellSlot:
startingItem: PowerCellHardsuitHelmet # self recharging
diff --git a/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml b/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml
index f100c5087e..46f9f317bc 100644
--- a/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml
+++ b/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml
@@ -19,12 +19,9 @@
visuals:
- type: FlashLightVisualizer
- type: HandheldLight
- toggleAction:
- name: action-name-toggle-light
- description: action-description-toggle-light
- icon: Objects/Tools/flashlight.rsi/flashlight.png
- iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png
- event: !type:ToggleActionEvent
+ - type: ItemActions
+ actions:
+ - actionType: ToggleLight
- type: PowerCellSlot
cellSlot:
startingItem: PowerCellSmallHigh
diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml
index efaa070122..e0e0ddd562 100644
--- a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml
+++ b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml
@@ -10,13 +10,9 @@
- type: Clothing
sprite: Clothing/Shoes/Boots/magboots.rsi
- type: Magboots
- toggleAction:
- icon: Clothing/Shoes/Boots/magboots.rsi/icon.png
- iconOn: Clothing/Shoes/Boots/magboots.rsi/icon-on.png
- name: action-name-magboot-toggle
- description: action-decription-magboot-toggle
- itemIconStyle: NoItem
- event: !type:ToggleActionEvent
+ - type: ItemActions
+ actions:
+ - actionType: ToggleMagboots
- type: entity
parent: ClothingShoesBootsMag
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
index e13bf3fd14..8fabc0c9b7 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
@@ -646,15 +646,8 @@
tags:
- Trash
- type: Recyclable
- - type: Actions
- # TODO: Remove CombatMode when Prototype Composition is added
- - type: CombatMode
- combatToggleAction:
- enabled: false
- autoPopulate: false
- disarmAction:
- enabled: false
- autoPopulate: false
+ - type: Actions # TODO: Remove CombatMode when Prototype Composition is added
+ innateActions: []
- type: entity
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml
index f7f2cbafe5..58408d5e20 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml
@@ -121,6 +121,8 @@
sprite: Mobs/Effects/onfire.rsi
normalState: Generic_mob_burning
- type: Actions
+ innateActions:
+ - CombatMode
- type: DoAfter
- type: Climbing
- type: Flashable
diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml
index 53906909d8..9c2e842e18 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml
@@ -13,6 +13,9 @@
- type: DoAfter
- type: CombatMode
- type: Actions
+ innateActions:
+ - CombatMode
+ - GhostBoo
- type: PlayerInputMover
- type: Physics
bodyType: Kinematic
diff --git a/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml
index ee78226f03..d8ea943b26 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml
@@ -13,6 +13,10 @@
- type: PlayerInputMover
- type: Alerts
- type: Actions
+ innateActions:
+ - CombatMode
+ - Disarm
+ - HumanScream
- type: Eye
- type: CameraRecoil
- type: Examiner
diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml
index 47d7e30803..8ed853e942 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml
@@ -68,6 +68,8 @@
types:
Blunt: 22
- type: Actions
+ innateActions:
+ - CombatMode
- type: Guardian
- type: NameIdentifier
group: Holoparasite
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml
index 36f70b2499..fffca1673f 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/human.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml
@@ -20,6 +20,10 @@
Asphyxiation: -1.5
- type: Alerts
- type: Actions
+ innateActions:
+ - CombatMode
+ - Disarm
+ - HumanScream
- type: Eye
- type: CameraRecoil
- type: Examiner
diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml
index d9e5e0b728..c5a90eadd8 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml
@@ -45,3 +45,6 @@
baseSprintSpeed: 14
baseWalkSpeed: 7
- type: MovementIgnoreGravity
+ - type: Actions
+ innateActions:
+ - GhostBoo
diff --git a/Resources/Prototypes/Entities/Mobs/Player/slime.yml b/Resources/Prototypes/Entities/Mobs/Player/slime.yml
index 5f7f01696e..34b22801ac 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/slime.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/slime.yml
@@ -11,6 +11,10 @@
- type: PlayerInputMover
- type: Alerts
- type: Actions
+ innateActions:
+ - CombatMode
+ - Disarm
+ - HumanScream
- type: Eye
- type: CameraRecoil
- type: Examiner
diff --git a/Resources/Prototypes/Entities/Mobs/Player/vox.yml b/Resources/Prototypes/Entities/Mobs/Player/vox.yml
index da533f0385..9ff33beab3 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/vox.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/vox.yml
@@ -10,12 +10,12 @@
context: "human"
- type: Alerts
- type: Actions
+ innateActions:
+ - VoxScream
+ - Disarm
- type: Eye
- type: CameraRecoil
- type: Examiner
- - type: Vocal
- maleScream: /Audio/Voice/Vox/shriek1.ogg
- femaleScream: /Audio/Voice/Vox/shriek1.ogg
- type: AiFactionTag
factions:
- NanoTrasen
diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml
index 012edab8ba..9ae64f27bb 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/human.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml
@@ -288,7 +288,6 @@
# - type: Recyclable Turns out turning off recycler safeties without considering the instagib is a bad idea
# safe: false
- type: Speech
- - type: Vocal
- type: Emoting
- type: Grammar
attributes:
diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
index f1a6943e63..3105e946ef 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
@@ -22,12 +22,6 @@
- idcard
- Belt
- type: UnpoweredFlashlight
- toggleAction:
- name: action-name-toggle-light
- description: action-description-toggle-light
- icon: Objects/Tools/flashlight.rsi/flashlight.png
- iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png
- event: !type:ToggleActionEvent
- type: PointLight
enabled: false
radius: 2.5
diff --git a/Resources/Prototypes/Entities/Objects/Fun/pai.yml b/Resources/Prototypes/Entities/Objects/Fun/pai.yml
index 54a81dc8ab..aab82ce023 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/pai.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/pai.yml
@@ -25,17 +25,12 @@
- type: Input
context: "human"
- type: PAI
- midiAction:
- name: action-name-pai-play-midi
- checkCanInteract: false
- icon: Interface/Actions/pai-midi.png
- description: action-description-pai-play-midi
- event: !type:OpenUiActionEvent
- key: enum.InstrumentUiKey.Key
- type: Examiner
- type: GhostRadio
- type: DoAfter
- type: Actions
+ innateActions:
+ - PAIMidi
# This has to be installed because otherwise they're not "alive",
# so they can ghost and come back.
# Note that the personal AI never "dies".
diff --git a/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml b/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml
index 8a422b132d..2c8cf1c26e 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml
@@ -5,13 +5,10 @@
abstract: true
components:
- type: HandheldLight
- toggleAction:
- name: action-name-toggle-light
- description: action-description-toggle-light
- icon: Objects/Tools/flashlight.rsi/flashlight.png
- iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png
- event: !type:ToggleActionEvent
- type: PowerCellSlot
+ - type: ItemActions
+ actions:
+ - actionType: ToggleLight
- type: Sprite
sprite: Objects/Misc/Lights/lights.rsi
netsync: false
diff --git a/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml b/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml
index eb987c752c..eef703450a 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml
@@ -8,12 +8,9 @@
tags:
- Flashlight
- type: HandheldLight
- toggleAction:
- name: action-name-toggle-light
- description: action-description-toggle-light
- icon: Objects/Tools/flashlight.rsi/flashlight.png
- iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png
- event: !type:ToggleActionEvent
+ - type: ItemActions
+ actions:
+ - actionType: ToggleLight
- type: PowerCellSlot
cellSlot:
startingItem: PowerCellSmallHigh
diff --git a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml
index 994624e7b3..67d4ee317f 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml
@@ -15,12 +15,9 @@
sprite: Objects/Tanks/generic.rsi
QuickEquip: false
- type: GasTank
- toggleAction:
- name: action-name-internals-toggle
- description: action-description-internals-toggle
- icon: Interface/Actions/internal0.png
- iconOn: Interface/Actions/internal1.png
- event: !type:ToggleActionEvent
+ - type: ItemActions
+ actions:
+ - actionType: ToggleInternals
- type: entity
parent: GasTankBase
diff --git a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml
index 3c96a8f58d..c644120d0e 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml
@@ -5,12 +5,9 @@
description: The holy light guides the way.
components:
- type: HandheldLight
- toggleAction:
- name: action-name-toggle-light
- description: action-description-toggle-light
- icon: Objects/Tools/flashlight.rsi/flashlight.png
- iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png
- event: !type:ToggleActionEvent
+ - type: ItemActions
+ actions:
+ - actionType: ToggleLight
- type: Sprite
sprite: Objects/Tools/lantern.rsi
layers:
diff --git a/Resources/Prototypes/Reagents/pyrotechnic.yml b/Resources/Prototypes/Reagents/pyrotechnic.yml
index 3003caaf6b..e47609d663 100644
--- a/Resources/Prototypes/Reagents/pyrotechnic.yml
+++ b/Resources/Prototypes/Reagents/pyrotechnic.yml
@@ -112,8 +112,9 @@
- !type:FlammableReaction
multiplier: 0.3
- !type:Ignite
- - !type:Scream
+ - !type:DoAction
probability: 0.2
+ action: HumanScream
- !type:PopupMessage
messages: [ "clf3-it-burns", "clf3-get-away" ]
probability: 0.3
diff --git a/Resources/Prototypes/Reagents/toxins.yml b/Resources/Prototypes/Reagents/toxins.yml
index bd442809b4..64c54d8cc1 100644
--- a/Resources/Prototypes/Reagents/toxins.yml
+++ b/Resources/Prototypes/Reagents/toxins.yml
@@ -44,8 +44,9 @@
damage:
groups:
Caustic: 0.5
- - !type:Scream
+ - !type:DoAction
probability: 0.3
+ action: HumanScream
metabolisms:
Poison:
effects:
@@ -77,8 +78,9 @@
damage:
groups:
Caustic: 0.3
- - !type:Scream
+ - !type:DoAction
probability: 0.2
+ action: HumanScream
metabolisms:
Poison:
effects:
@@ -117,8 +119,9 @@
damage:
groups:
Caustic: 0.1
- - !type:Scream
+ - !type:DoAction
probability: 0.1
+ action: HumanScream
metabolisms:
Poison:
effects: