From ff7d4ed9f6dc8628a2e22dff766d9d73babb3df8 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sat, 26 Feb 2022 18:24:08 +1300 Subject: [PATCH] Patched Actions Rework (#6899) * Rejig Actions * fix merge errors * lambda-b-gon * fix PAI, add innate actions * Revert "fix PAI, add innate actions" This reverts commit 4b501ac083e979e31ebd98d7b98077e0dbdd344b. * Just fix by making nullable. if only require: true actually did something somehow. * Make AddActions() ensure an actions component and misc comments * misc cleanup * Limit range even when not checking for obstructions * remove old guardian code * rename function and make EntityUid nullable * fix magboot bug * fix action search menu * make targeting toggle all equivalent actions * fix combat popups (enabling <-> disabling) * fix networking * Allow action locking * prevent telepathy --- Content.Client/Actions/ActionEvents.cs | 11 + Content.Client/Actions/ActionsSystem.cs | 616 ++++++++++++++++- .../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 | 228 ++----- 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 | 276 ++++++++ .../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, 3156 insertions(+), 5166 deletions(-) create mode 100644 Content.Client/Actions/ActionEvents.cs delete mode 100644 Content.Client/Actions/Assignments/ActionAssignment.cs delete mode 100644 Content.Client/Actions/ClientActionsComponent.cs create mode 100644 Content.Client/Outline/TargetOutlineSystem.cs delete mode 100644 Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs delete mode 100644 Content.Server/Actions/Actions/CombatMode.cs delete mode 100644 Content.Server/Actions/Actions/DebugInstant.cs delete mode 100644 Content.Server/Actions/Actions/DebugTargetEntity.cs delete mode 100644 Content.Server/Actions/Actions/DebugTargetPoint.cs delete mode 100644 Content.Server/Actions/Actions/DebugToggle.cs delete mode 100644 Content.Server/Actions/Actions/DisarmAction.cs delete mode 100644 Content.Server/Actions/Actions/GhostBoo.cs delete mode 100644 Content.Server/Actions/Actions/GuardianToggleAction.cs delete mode 100644 Content.Server/Actions/Actions/PAIMidi.cs delete mode 100644 Content.Server/Actions/Actions/ScreamAction.cs create mode 100644 Content.Server/Actions/ActionsSystem.cs delete mode 100644 Content.Server/Actions/Commands/CooldownAction.cs delete mode 100644 Content.Server/Actions/Commands/GrantAction.cs delete mode 100644 Content.Server/Actions/Commands/RevokeAction.cs delete mode 100644 Content.Server/Actions/ServerActionsComponent.cs delete mode 100644 Content.Server/Actions/Spells/GiveItemSpell.cs delete mode 100644 Content.Server/Chemistry/ReagentEffects/DoAction.cs create mode 100644 Content.Server/Chemistry/ReagentEffects/Scream.cs create mode 100644 Content.Server/Speech/Components/VocalComponent.cs create mode 100644 Content.Server/Speech/VocalSystem.cs create mode 100644 Content.Server/UserInterface/OpenUiActionEvent.cs create mode 100644 Content.Shared/Actions/ActionEvents.cs delete mode 100644 Content.Shared/Actions/ActionManager.cs delete mode 100644 Content.Shared/Actions/ActionType.cs create mode 100644 Content.Shared/Actions/ActionTypes/ActionType.cs create mode 100644 Content.Shared/Actions/ActionTypes/InstantAction.cs create mode 100644 Content.Shared/Actions/ActionTypes/TargetedAction.cs create mode 100644 Content.Shared/Actions/ActionsComponent.cs delete mode 100644 Content.Shared/Actions/Behaviors/IActionBehavior.cs delete mode 100644 Content.Shared/Actions/Behaviors/IInstantAction.cs delete mode 100644 Content.Shared/Actions/Behaviors/ITargetEntityAction.cs delete mode 100644 Content.Shared/Actions/Behaviors/ITargetEntityItemAction.cs delete mode 100644 Content.Shared/Actions/Behaviors/ITargetPointAction.cs delete mode 100644 Content.Shared/Actions/Behaviors/ITargetPointItemAction.cs delete mode 100644 Content.Shared/Actions/Behaviors/IToggleAction.cs delete mode 100644 Content.Shared/Actions/Behaviors/Item/IInstantItemAction.cs delete mode 100644 Content.Shared/Actions/Behaviors/Item/IItemActionBehavior.cs delete mode 100644 Content.Shared/Actions/Behaviors/Item/IToggleItemAction.cs delete mode 100644 Content.Shared/Actions/Components/ItemActionsComponent.cs delete mode 100644 Content.Shared/Actions/Components/SharedActionsComponent.cs delete mode 100644 Content.Shared/Actions/IActionAttempt.cs delete mode 100644 Content.Shared/Actions/Prototypes/ActionPrototype.cs delete mode 100644 Content.Shared/Actions/Prototypes/BaseActionPrototype.cs delete mode 100644 Content.Shared/Actions/Prototypes/ItemActionPrototype.cs delete mode 100644 Content.Shared/Actions/SharedActionSystem.cs create mode 100644 Content.Shared/Actions/SharedActionsSystem.cs create mode 100644 Content.Shared/Clothing/SharedMagbootsSystem.cs create mode 100644 Content.Shared/Toggleable/ToggleActionEvent.cs create mode 100644 Resources/Locale/en-US/actions/actions/ghost.ftl create mode 100644 Resources/Locale/en-US/actions/actions/internals.ftl create mode 100644 Resources/Locale/en-US/actions/actions/light.ftl create mode 100644 Resources/Locale/en-US/actions/actions/mapping.ftl create mode 100644 Resources/Locale/en-US/actions/actions/pai.ftl create mode 100644 Resources/Locale/en-US/actions/actions/vocal.ftl create mode 100644 Resources/Locale/en-US/actions/ui/actionslot.ftl create mode 100644 Resources/Locale/en-US/magboot/actions.ftl delete mode 100644 Resources/Prototypes/Actions/actions.yml delete mode 100644 Resources/Prototypes/Actions/guardian_actions.yml delete mode 100644 Resources/Prototypes/Actions/item_actions.yml delete mode 100644 Resources/Prototypes/Actions/magboots.yml delete mode 100644 Resources/Prototypes/Actions/spells.yml diff --git a/Content.Client/Actions/ActionEvents.cs b/Content.Client/Actions/ActionEvents.cs new file mode 100644 index 0000000000..36739b0dbe --- /dev/null +++ b/Content.Client/Actions/ActionEvents.cs @@ -0,0 +1,11 @@ +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 b5201fcd62..8fe2c5af68 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -1,20 +1,64 @@ +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.Shared.GameObjects; +using Robust.Client.UserInterface; +using Robust.Client.Utility; +using Robust.Shared.Audio; +using Robust.Shared.ContentPack; +using Robust.Shared.GameStates; using Robust.Shared.Input; using Robust.Shared.Input.Binding; -using Robust.Shared.IoC; +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; namespace Content.Client.Actions { [UsedImplicitly] - public sealed class ActionsSystem : EntitySystem + public sealed class ActionsSystem : SharedActionsSystem { + [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() { @@ -64,12 +108,207 @@ 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), + .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true), typeof(ConstructionSystem), typeof(DragDropSystem)) + .BindBefore(EngineKeyFunctions.UIRightClick, new PointerInputCmdHandler(TargetingCancel, outsidePrediction: true)) .Register(); - SubscribeLocalEvent((_, component, _) => component.PlayerAttached()); - SubscribeLocalEvent((_, component, _) => component.PlayerDetached()); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(HandleState); + } + + protected override void Dirty(ActionType action) + { + // Should only ever receive component states for attached player's component. + // --> lets not bother unnecessarily dirtying and prediction-resetting actions for other players. + if (action.AttachedEntity != _playerManager.LocalPlayer?.ControlledEntity) + return; + + base.Dirty(action); + UIDirty = true; + } + + private void HandleState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args) + { + // Client only needs to care about local player. + if (uid != _playerManager.LocalPlayer?.ControlledEntity) + return; + + 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); + if (act.AutoRemove && !(Ui?.Locked ?? false)) + 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 AddAction(EntityUid uid, ActionType action, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true) + { + if (uid != _playerManager.LocalPlayer?.ControlledEntity) + return; + + if (!Resolve(uid, ref comp, false)) + return; + + base.AddAction(uid, action, provider, comp, dirty); + UIDirty = true; + } + + public override void RemoveActions(EntityUid uid, IEnumerable actions, ActionsComponent? comp = null, bool dirty = true) + { + if (uid != _playerManager.LocalPlayer?.ControlledEntity) + return; + + if (!Resolve(uid, ref comp, false)) + return; + + base.RemoveActions(uid, actions, comp, dirty); + + foreach (var act in actions) + { + if (act.AutoRemove && !(Ui?.Locked ?? false)) + 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 (action.AutoRemove && !Ui.Locked && !Ui.Component.Actions.Contains(action)) + 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; } public override void Shutdown() @@ -85,9 +324,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; - actionsComponent.HandleHotbarKeybind(slot, args); + HandleHotbarKeybind(slot, args); return true; }, false); } @@ -98,28 +337,363 @@ 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; - actionsComponent.HandleChangeHotbarKeybind(hotbar, args); + HandleChangeHotbarKeybind(hotbar, args); return true; }, false); } - private bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args) - { - var playerEntity = _playerManager.LocalPlayer?.ControlledEntity; - if (!EntityManager.TryGetComponent(playerEntity, out var actionsComponent)) return false; - - return actionsComponent.TargetingOnUse(args); - } - private void ToggleActionsMenu() { - var playerEntity = _playerManager.LocalPlayer?.ControlledEntity; - if (!EntityManager.TryGetComponent(playerEntity, out var actionsComponent)) return; + Ui?.ToggleActionsMenu(); + } - actionsComponent.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; + + // 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; + } + } + + private bool TryTargetWorld(in PointerInputCmdHandler.PointerInputCmdArgs args, WorldTargetAction action, EntityUid user, ActionsComponent actionComp) + { + var coords = args.Coordinates.ToMap(EntityManager); + + 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(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; } } } diff --git a/Content.Client/Actions/Assignments/ActionAssignment.cs b/Content.Client/Actions/Assignments/ActionAssignment.cs deleted file mode 100644 index 91e1e7725c..0000000000 --- a/Content.Client/Actions/Assignments/ActionAssignment.cs +++ /dev/null @@ -1,90 +0,0 @@ -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 a7e3e7d8aa..5c9c178eb1 100644 --- a/Content.Client/Actions/Assignments/ActionAssignments.cs +++ b/Content.Client/Actions/Assignments/ActionAssignments.cs @@ -1,7 +1,5 @@ using Content.Shared.Actions; -using Content.Shared.Actions.Components; -using Robust.Shared.GameObjects; -using System.Collections.Generic; +using Content.Shared.Actions.ActionTypes; using System.Linq; namespace Content.Client.Actions.Assignments @@ -9,6 +7,7 @@ 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), @@ -19,7 +18,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 ActionAssignment?[,] _slots; + private readonly ActionType?[,] _slots; /// /// Hotbar and slot assignment for each action type (slot index 0 corresponds to the one labeled "1", @@ -28,14 +27,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) /// - private readonly Dictionary> _assignments; + [DataField("assignments")] + public readonly Dictionary> Assignments = new(); /// /// Actions which have been manually cleared by the user, thus should not /// auto-populate. /// - private readonly HashSet _preventAutoPopulate = new(); - private readonly Dictionary> _preventAutoPopulateItem = new(); + public readonly SortedSet PreventAutoPopulate = new(); private readonly byte _numHotbars; private readonly byte _numSlots; @@ -44,105 +43,25 @@ namespace Content.Client.Actions.Assignments { _numHotbars = numHotbars; _numSlots = numSlots; - _assignments = new Dictionary>(); - _slots = new ActionAssignment?[numHotbars, numSlots]; + _slots = new ActionType?[numHotbars, numSlots]; } - /// - /// 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) + public bool Remove(ActionType action) => Replace(action, null); + + internal bool Replace(ActionType action, ActionType? newAction) { - // 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) + if (!Assignments.Remove(action, out var assigns)) + return false; + + if (newAction != null) + Assignments[newAction] = assigns; + + foreach (var (bar, slot) in assigns) { - var assignment = ActionAssignment.For(actionState.Key); - if (actionState.Value.Enabled && !_assignments.ContainsKey(assignment)) - { - AutoPopulate(assignment, currentHotbar, false); - } + _slots[bar, slot] = newAction; } - 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); - } + return true; } /// @@ -151,18 +70,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, ActionAssignment actionType) + public void AssignSlot(byte hotbar, byte slot, ActionType 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; } } @@ -183,40 +102,21 @@ namespace Content.Client.Actions.Assignments // (keeping in mind something can be assigned multiple slots) var currentAction = _slots[hotbar, slot]; - if (!currentAction.HasValue) - { + if (currentAction == null) return; - } if (preventAutoPopulate) - { - var assignment = currentAction.Value; + PreventAutoPopulate.Add(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]; + var assignmentList = Assignments[currentAction]; assignmentList = assignmentList.Where(a => a.Hotbar != hotbar || a.Slot != slot).ToList(); if (!assignmentList.Any()) { - _assignments.Remove(currentAction.Value); + Assignments.Remove(currentAction); } else { - _assignments[currentAction.Value] = assignmentList; + Assignments[currentAction] = assignmentList; } _slots[hotbar, slot] = null; @@ -231,45 +131,10 @@ 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(ActionAssignment toAssign, byte currentHotbar, bool force = true) + public void AutoPopulate(ActionType toAssign, byte currentHotbar, bool force = true) { - if (ShouldPreventAutoPopulate(toAssign, force)) - { + if (!force && PreventAutoPopulate.Contains(toAssign)) 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++) { @@ -277,21 +142,10 @@ 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; } @@ -299,36 +153,15 @@ 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 ActionAssignment? this[in byte hotbar, in byte slot] => _slots[hotbar, slot]; + public ActionType? this[in byte hotbar, in byte slot] => _slots[hotbar, slot]; /// true if we have the assignment assigned to some slot - public bool HasAssignment(ActionAssignment assignment) + public bool HasAssignment(ActionType assignment) { - return _assignments.ContainsKey(assignment); + return Assignments.ContainsKey(assignment); } } } diff --git a/Content.Client/Actions/ClientActionsComponent.cs b/Content.Client/Actions/ClientActionsComponent.cs deleted file mode 100644 index 1032a63ae4..0000000000 --- a/Content.Client/Actions/ClientActionsComponent.cs +++ /dev/null @@ -1,252 +0,0 @@ -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 392e67873a..319c682299 100644 --- a/Content.Client/Actions/UI/ActionAlertTooltip.cs +++ b/Content.Client/Actions/UI/ActionAlertTooltip.cs @@ -1,5 +1,7 @@ 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; @@ -89,7 +91,7 @@ namespace Content.Client.Actions.UI { var duration = Cooldown.Value.End - Cooldown.Value.Start; _cooldownLabel.SetMessage(FormattedMessage.FromMarkup( - $"[color=#a10505]{duration.Seconds} sec cooldown ({timeLeft.Seconds + 1} sec remaining)[/color]")); + $"[color=#a10505]{(int) duration.TotalSeconds} sec cooldown ({(int) timeLeft.TotalSeconds + 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 b3140a32b5..edb341a644 100644 --- a/Content.Client/Actions/UI/ActionMenu.cs +++ b/Content.Client/Actions/UI/ActionMenu.cs @@ -1,22 +1,16 @@ -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.Prototypes; +using Content.Shared.Actions.ActionTypes; 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; @@ -29,28 +23,35 @@ namespace Content.Client.Actions.UI /// public sealed class ActionMenu : DefaultWindow { - 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"; + // 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 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; @@ -60,17 +61,16 @@ namespace Content.Client.Actions.UI private readonly TextureRect _dragShadow; private readonly IGameHud _gameHud; private readonly DragDropHelper _dragDropHelper; + private readonly IEntityManager _entMan; - - public ActionMenu(ClientActionsComponent actionsComponent, ActionsUI actionsUI) + public ActionMenu(ActionsUI actionsUI) { - _actionsComponent = actionsComponent; _actionsUI = actionsUI; - _actionManager = IoCManager.Resolve(); _gameHud = IoCManager.Resolve(); + _entMan = IoCManager.Resolve(); Title = Loc.GetString("ui-actionmenu-title"); - MinSize = (300, 300); + MinSize = (320, 300); Contents.AddChild(new BoxContainer { @@ -115,27 +115,14 @@ namespace Content.Client.Actions.UI } }); - // 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)) + foreach (var tag in _filters) { _filterButton.AddItem( CultureInfo.CurrentCulture.TextInfo.ToTitleCase(tag), tag); } + // default to showing all actions. + _filterButton.SelectKey(AllFilter); + UpdateFilterLabel(); _dragShadow = new TextureRect @@ -157,13 +144,6 @@ 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() @@ -199,7 +179,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; @@ -244,47 +224,7 @@ namespace Content.Client.Actions.UI return; } - // 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.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, _dragDropHelper.Dragged.Action); _actionsUI.UpdateUI(); } @@ -300,19 +240,8 @@ 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(); } @@ -341,7 +270,7 @@ namespace Content.Client.Actions.UI return; } - var matchingActions = _actionManager.EnumerateActions() + var matchingActions = _actionsUI.Component.Actions .Where(a => MatchesSearchCriteria(a, search, _filterButton.SelectedKeys)); PopulateActions(matchingActions); @@ -361,7 +290,7 @@ namespace Content.Client.Actions.UI } } - private bool MatchesSearchCriteria(BaseActionPrototype action, string standardizedSearch, + private bool MatchesSearchCriteria(ActionType action, string standardizedSearch, IReadOnlyList selectedFilterTags) { // check filter tag match first - each action must contain all filter tags currently selected. @@ -377,56 +306,32 @@ 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; } - return false; + // search by provider name + if (action.Provider == null || action.Provider == _actionsUI.Component.Owner) + return false; + var name = _entMan.GetComponent(action.Provider.Value).EntityName; + return Standardize(name).Contains(standardizedSearch); } - 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) + private bool ActionMatchesFilterTag(ActionType action, string tag) { return tag switch { - 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) + 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 }; } - /// /// Standardized form is all lowercase, no non-alphanumeric characters (converted to whitespace), /// trimmed, 1 space max per whitespace gap, @@ -472,16 +377,15 @@ namespace Content.Client.Actions.UI return newText.ToString(); } - private void PopulateActions(IEnumerable actions) + private void PopulateActions(IEnumerable actions) { ClearList(); - _actionList = actions.ToArray(); - foreach (var action in _actionList.OrderBy(act => act.Name.ToString())) + foreach (var action in actions) { - var actionItem = new ActionMenuItem(action, OnItemFocusExited); + var actionItem = new ActionMenuItem(_actionsUI, action, OnItemFocusExited); _resultsGrid.Children.Add(actionItem); - actionItem.SetActionState(_actionsComponent.IsGranted(action)); + actionItem.SetActionState(action.Enabled); actionItem.OnButtonDown += OnItemButtonDown; actionItem.OnButtonUp += OnItemButtonUp; actionItem.OnPressed += OnItemPressed; @@ -496,7 +400,6 @@ namespace Content.Client.Actions.UI ((ActionMenuItem) actionItem).OnPressed -= OnItemPressed; } _resultsGrid.Children.Clear(); - _actionList = EmptyActionList; } /// @@ -508,8 +411,10 @@ namespace Content.Client.Actions.UI foreach (var actionItem in _resultsGrid.Children) { var actionMenuItem = ((ActionMenuItem) actionItem); - actionMenuItem.SetActionState(_actionsComponent.IsGranted(actionMenuItem.Action)); + actionMenuItem.SetActionState(actionMenuItem.Action.Enabled); } + + SearchAndDisplay(); } protected override void FrameUpdate(FrameEventArgs args) diff --git a/Content.Client/Actions/UI/ActionMenuItem.cs b/Content.Client/Actions/UI/ActionMenuItem.cs index 03293e03f5..d82801d304 100644 --- a/Content.Client/Actions/UI/ActionMenuItem.cs +++ b/Content.Client/Actions/UI/ActionMenuItem.cs @@ -1,12 +1,20 @@ -using System; +using System; using Content.Client.Stylesheets; -using Content.Shared.Actions.Prototypes; +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; 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. /// @@ -16,28 +24,165 @@ namespace Content.Client.Actions.UI // quickly explore what each action is private const float CustomTooltipDelay = 0.2f; - public BaseActionPrototype Action { get; private set; } + private readonly TextureRect _bigActionIcon; + private readonly TextureRect _smallActionIcon; + private readonly SpriteView _smallItemSpriteView; + private readonly SpriteView _bigItemSpriteView; + + public ActionType Action; private Action _onControlFocusExited; - public ActionMenuItem(BaseActionPrototype action, Action onControlFocusExited) + private readonly ActionsUI _actionsUI; + + public ActionMenuItem(ActionsUI actionsUI, ActionType action, Action onControlFocusExited) { - _onControlFocusExited = onControlFocusExited; + _actionsUI = actionsUI; Action = action; + _onControlFocusExited = onControlFocusExited; MinSize = (64, 64); VerticalAlignment = VAlignment.Top; - AddChild(new TextureRect + _bigActionIcon = new TextureRect { HorizontalExpand = true, VerticalExpand = true, Stretch = TextureRect.StretchMode.Scale, - Texture = action.Icon.Frame0() + 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), }); + 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() @@ -48,7 +193,15 @@ namespace Content.Client.Actions.UI private Control SupplyTooltip(Control? sender) { - return new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires); + 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; } /// @@ -71,6 +224,5 @@ namespace Content.Client.Actions.UI } } } - } } diff --git a/Content.Client/Actions/UI/ActionSlot.cs b/Content.Client/Actions/UI/ActionSlot.cs index 77f0ac4c07..6c2f8a7950 100644 --- a/Content.Client/Actions/UI/ActionSlot.cs +++ b/Content.Client/Actions/UI/ActionSlot.cs @@ -1,20 +1,14 @@ -using System; +using System; using Content.Client.Cooldown; using Content.Client.Stylesheets; using Content.Shared.Actions; -using Content.Shared.Actions.Components; -using Content.Shared.Actions.Prototypes; -using Content.Shared.Inventory; +using Content.Shared.Actions.ActionTypes; 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; @@ -37,48 +31,7 @@ namespace Content.Client.Actions.UI /// /// Current action in this slot. /// - 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(); - } - } + public ActionType? Action { get; private set; } /// /// 1-10 corresponding to the number label on the slot (10 is labeled as 0) @@ -86,25 +39,6 @@ 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; @@ -114,8 +48,6 @@ 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; @@ -124,9 +56,8 @@ 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, ClientActionsComponent actionsComponent, byte slotIndex) + public ActionSlot(ActionsUI actionsUI, ActionMenu actionMenu, byte slotIndex) { - _actionsComponent = actionsComponent; _actionsUI = actionsUI; _actionMenu = actionMenu; _gameTiming = IoCManager.Resolve(); @@ -170,7 +101,8 @@ namespace Content.Client.Actions.UI { HorizontalAlignment = HAlignment.Right, VerticalAlignment = VAlignment.Bottom, - Visible = false + Visible = false, + OverrideDirection = Direction.South, }; _cooldownGraphic = new CooldownGraphic {Progress = 0, Visible = false}; @@ -219,24 +151,24 @@ namespace Content.Client.Actions.UI private Control? SupplyTooltip(Control sender) { - return Action == null ? null : - new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires) {Cooldown = Cooldown}; - } + if (Action == null) + return null; - /// - /// Action attempt for performing the action in the slot - /// - public IActionAttempt? ActionAttempt() - { - IActionAttempt? attempt = Action switch + string? extra = null; + if (Action.Charges != null) { - 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; + 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; } protected override void MouseEntered() @@ -245,9 +177,9 @@ namespace Content.Client.Actions.UI _beingHovered = true; DrawModeChanged(); - if (Action is not ItemActionPrototype) return; - if (Item == null) return; - _actionsComponent.HighlightItemSlot(Item.Value); + + if (Action?.Provider != null) + _actionsUI.System.HighlightItemSlot(Action.Provider.Value); } protected override void MouseExited() @@ -256,32 +188,55 @@ namespace Content.Client.Actions.UI _beingHovered = false; CancelPress(); DrawModeChanged(); - _actionsComponent.StopHighlightingItemSlots(); + _actionsUI.System.StopHighlightingItemSlot(); } protected override void KeyBindDown(GUIBoundKeyEventArgs args) { base.KeyBindDown(args); - if (args.Function == EngineKeyFunctions.UIRightClick) + if (Action == null) { - if (!_actionsUI.Locked && !_actionsUI.DragDropHelper.IsDragging && !_actionMenu.IsDragging) - { - _actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true); - _actionsUI.StopTargeting(); - _actionsUI.UpdateUI(); - } + // No action for this slot. Maybe the user is trying to add a mapping action? + _actionsUI.System.TryFillSlot(_actionsUI.SelectedHotbar, SlotIndex); return; } // only handle clicks, and can't do anything to this if no assignment - if (args.Function != EngineKeyFunctions.UIClick || !HasAssignment) + 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) return; - // might turn into a drag or a full press if released - Depress(true); - _actionsUI.DragDropHelper.MouseDown(this); - DrawModeChanged(); + 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(); } protected override void KeyBindUp(GUIBoundKeyEventArgs args) @@ -299,21 +254,21 @@ namespace Content.Client.Actions.UI { // finish the drag, swap the 2 slots var fromIdx = SlotIndex; - var fromAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, fromIdx]; + var fromAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, fromIdx]; var toIdx = targetSlot.SlotIndex; - var toAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, toIdx]; + var toAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, toIdx]; if (fromIdx == toIdx) return; - if (!fromAssignment.HasValue) return; + if (fromAssignment == null) return; - _actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment.Value); - if (toAssignment.HasValue) + _actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment); + if (toAssignment != null) { - _actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment.Value); + _actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment); } else { - _actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false); + _actionsUI.System.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false); } _actionsUI.UpdateUI(); } @@ -348,66 +303,19 @@ namespace Content.Client.Actions.UI /// /// Press this button down. If it was depressed and now set to not depressed, will - /// trigger the action. Only has an effect if CanUseAction. + /// trigger the action. /// public void Depress(bool depress) { // action can still be toggled if it's allowed to stay selected - if (!CanUseAction) return; - + if (Action == null || !Action.Enabled) return; if (_depressed && !depress) { // fire the action - // no left-click interaction with it on cooldown or revoked - _actionsComponent.AttemptAction(this); + _actionsUI.System.OnSlotPressed(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()); } /// @@ -415,18 +323,12 @@ namespace Content.Client.Actions.UI /// /// action to assign /// item the action is provided by - /// whether action should initially appear enable or disabled - public void Assign(ItemActionPrototype action, EntityUid item, bool actionEnabled) + public void Assign(ActionType action) { // already assigned - if (Action != null && Action == action && Item == item) return; + if (Action != null && Action == action) return; Action = action; - Item = item; - _depressed = false; - ToggledOn = false; - ActionEnabled = false; - Cooldown = null; HideTooltip(); UpdateIcons(); DrawModeChanged(); @@ -438,12 +340,9 @@ namespace Content.Client.Actions.UI /// public void Clear() { - if (!HasAssignment) return; + if (Action == null) return; Action = null; - Item = default; - ToggledOn = false; _depressed = false; - Cooldown = null; HideTooltip(); UpdateIcons(); DrawModeChanged(); @@ -453,12 +352,8 @@ namespace Content.Client.Actions.UI /// /// Display the action in this slot (if there is one) as enabled /// - public void EnableAction() + public void Enable() { - if (ActionEnabled || !HasAssignment) return; - - ActionEnabled = true; - _depressed = false; DrawModeChanged(); _number.SetMessage(SlotNumberLabel()); } @@ -467,11 +362,8 @@ namespace Content.Client.Actions.UI /// Display the action in this slot (if there is one) as disabled. /// The slot is still clickable. /// - public void DisableAction() + public void Disable() { - if (!ActionEnabled || !HasAssignment) return; - - ActionEnabled = false; _depressed = false; DrawModeChanged(); _number.SetMessage(SlotNumberLabel()); @@ -481,70 +373,56 @@ namespace Content.Client.Actions.UI { if (SlotNumber > 10) return FormattedMessage.FromMarkup(""); var number = Loc.GetString(SlotNumber == 10 ? "0" : SlotNumber.ToString()); - var color = (ActionEnabled || !HasAssignment) ? EnabledColor : DisabledColor; + var color = (Action == null || Action.Enabled) ? EnabledColor : DisabledColor; return FormattedMessage.FromMarkup("[color=" + color + "]" + number + "[/color]"); } - private void UpdateIcons() + public void UpdateIcons() { - if (!HasAssignment) + UpdateItemIcon(); + + if (Action == null) { SetActionIcon(null); - SetItemIcon(null); return; } - if (HasToggleSprite && ToggledOn && Action != null) - { + if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != 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 - { - SetItemIcon(null); - } + SetActionIcon(Action.Icon?.Frame0()); } private void SetActionIcon(Texture? texture) { - if (texture == null || !HasAssignment) + 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 { - 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; - } - + _bigActionIcon.Texture = texture; + _bigActionIcon.Modulate = Action.IconColor; + _bigActionIcon.Visible = true; + _smallActionIcon.Texture = null; + _smallActionIcon.Visible = false; } } - private void SetItemIcon(ISpriteComponent? sprite) + private void UpdateItemIcon() { - if (sprite == null || !HasAssignment) + if (Action?.Provider == null || !IoCManager.Resolve().TryGetComponent(Action.Provider.Value, out SpriteComponent sprite)) { _bigItemSpriteView.Visible = false; _bigItemSpriteView.Sprite = null; @@ -553,70 +431,48 @@ namespace Content.Client.Actions.UI } else { - if (Action is ItemActionPrototype actionPrototype) + switch (Action.ItemIconStyle) { - 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; - } - } + case ItemActionIconStyle.BigItem: + _bigItemSpriteView.Visible = true; + _bigItemSpriteView.Sprite = sprite; + _smallItemSpriteView.Visible = false; + _smallItemSpriteView.Sprite = null; + break; + case ItemActionIconStyle.BigAction: - } - else - { - _bigItemSpriteView.Visible = false; - _bigItemSpriteView.Sprite = null; - _smallItemSpriteView.Visible = false; - _smallItemSpriteView.Sprite = null; - } + _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; + } } } - - private void DrawModeChanged() + public 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 (!HasAssignment) + if (Action == null) { 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) @@ -625,47 +481,50 @@ namespace Content.Client.Actions.UI return; } - // if it's toggled on, always show the toggled on style (currently same as depressed style) - if (ToggledOn) + if (Action.Toggled || _actionsUI.SelectingTargetFor == this) { // when there's a toggle sprite, we're showing that sprite instead of highlighting this slot - SetOnlyStylePseudoClass(HasToggleSprite ? ContainerButton.StylePseudoClassNormal : + SetOnlyStylePseudoClass(Action.IconOn != null ? ContainerButton.StylePseudoClassNormal : ContainerButton.StylePseudoClassPressed); return; } - - if (!ActionEnabled) + if (!Action.Enabled) { SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled); return; } - SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal); } - - protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); - if (!Cooldown.HasValue) + if (Action == null || Action.Cooldown == null || !Action.Enabled) { _cooldownGraphic.Visible = false; _cooldownGraphic.Progress = 0; return; } - var duration = Cooldown.Value.End - Cooldown.Value.Start; + var cooldown = Action.Cooldown.Value; + var duration = cooldown.End - cooldown.Start; var curTime = _gameTiming.CurTime; var length = duration.TotalSeconds; - var progress = (curTime - Cooldown.Value.Start).TotalSeconds / length; - var ratio = (progress <= 1 ? (1 - progress) : (curTime - Cooldown.Value.End).TotalSeconds * -5); + var progress = (curTime - cooldown.Start).TotalSeconds / length; + var ratio = (progress <= 1 ? (1 - progress) : (curTime - cooldown.End).TotalSeconds * -5); _cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1); - _cooldownGraphic.Visible = ratio > -1f; + if (ratio > -1f) + _cooldownGraphic.Visible = true; + else + { + _cooldownGraphic.Visible = false; + Action.Cooldown = null; + DrawModeChanged(); + } } } } diff --git a/Content.Client/Actions/UI/ActionsUI.cs b/Content.Client/Actions/UI/ActionsUI.cs index 260a6656d0..ed2a1531be 100644 --- a/Content.Client/Actions/UI/ActionsUI.cs +++ b/Content.Client/Actions/UI/ActionsUI.cs @@ -1,23 +1,18 @@ -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.Prototypes; +using Content.Shared.Actions.ActionTypes; 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 @@ -27,13 +22,16 @@ namespace Content.Client.Actions.UI /// public sealed class ActionsUI : Container { + private const float DragDeadZone = 10f; private const float CustomTooltipDelay = 0.4f; - private readonly ClientActionsComponent _actionsComponent; - private readonly ActionManager _actionManager; - private readonly IEntityManager _entityManager; - private readonly IGameTiming _gameTiming; + internal readonly ActionsSystem System; private readonly IGameHud _gameHud; + /// + /// The action component of the currently attached entity. + /// + public readonly ActionsComponent Component; + private readonly ActionSlot[] _slots; private readonly GridContainer _slotContainer; @@ -75,15 +73,13 @@ namespace Content.Client.Actions.UI /// public IEnumerable Slots => _slots; - public ActionsUI(ClientActionsComponent actionsComponent) + public ActionsUI(ActionsSystem system, ActionsComponent component) { SetValue(LayoutContainer.DebugProperty, true); - _actionsComponent = actionsComponent; - _actionManager = IoCManager.Resolve(); - _entityManager = IoCManager.Resolve(); - _gameTiming = IoCManager.Resolve(); + System = system; + Component = component; _gameHud = IoCManager.Resolve(); - _menu = new ActionMenu(_actionsComponent, this); + _menu = new ActionMenu(this); LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.End); LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Constrain); @@ -196,7 +192,7 @@ namespace Content.Client.Actions.UI _loadoutContainer.AddChild(nextHotbarIcon); _loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 }); - _slots = new ActionSlot[ClientActionsComponent.Slots]; + _slots = new ActionSlot[ActionsSystem.Slots]; _dragShadow = new TextureRect { @@ -207,14 +203,14 @@ namespace Content.Client.Actions.UI }; UserInterfaceManager.PopupRoot.AddChild(_dragShadow); - for (byte i = 0; i < ClientActionsComponent.Slots; i++) + for (byte i = 0; i < ActionsSystem.Slots; i++) { - var slot = new ActionSlot(this, _menu, actionsComponent, i); + var slot = new ActionSlot(this, _menu, i); _slotContainer.AddChild(slot); _slots[i] = slot; } - DragDropHelper = new DragDropHelper(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag); + DragDropHelper = new DragDropHelper(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag, DragDeadZone); MinSize = (10, 400); } @@ -285,157 +281,61 @@ namespace Content.Client.Actions.UI foreach (var actionSlot in Slots) { - var assignedActionType = _actionsComponent.Assignments[SelectedHotbar, actionSlot.SlotIndex]; - if (!assignedActionType.HasValue) + var action = System.Assignments[SelectedHotbar, actionSlot.SlotIndex]; + + if (action == null) { + if (SelectingTargetFor == actionSlot) + StopTargeting(true); actionSlot.Clear(); continue; } - if (assignedActionType.Value.TryGetAction(out var actionType)) + if (Component.Actions.TryGetValue(action, out var actualAction)) { - 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(); + UpdateActionSlot(actualAction, actionSlot); + continue; } + + // Action not in the actions component, but in the assignment list. + // This is either an action that doesn't auto-clear from the menu, or the action menu was locked. + // Show the old action, but make sure it is disabled; + action.Enabled = false; + action.Toggled = false; + + // If we enable the item-sprite, and if the item-sprite has a visual toggle, then the player will be + // able to know whether the item is toggled, even if it is not in their LOS (but in PVS). And for things + // like PDA sprites, the player can even see whether the action's item is currently inside of their PVS. + // SO unless theres some way of "freezing" a sprite-view, we just have to disable it. + action.ItemIconStyle = ItemActionIconStyle.NoItem; + + UpdateActionSlot(action, actionSlot); } } - private void UpdateActionSlot(ActionType actionType, ActionSlot actionSlot, ActionAssignment? assignedActionType) + private void UpdateActionSlot(ActionType action, ActionSlot actionSlot) { - if (_actionManager.TryGet(actionType, out var action)) - { - actionSlot.Assign(action, true); - } - else - { - Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType); - actionSlot.Clear(); - return; - } + actionSlot.Assign(action); - if (!_actionsComponent.TryGetActionState(actionType, out var actionState) || !actionState.Enabled) + if (!action.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.DisableAction(); - actionSlot.Cooldown = null; + actionSlot.Disable(); } else { - // 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.Enable(); } - // check if we need to toggle it - if (action.BehaviorType == BehaviorType.Toggle) - { - actionSlot.ToggledOn = actionState.ToggledOn; - } + actionSlot.UpdateIcons(); + actionSlot.DrawModeChanged(); } - 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 @@ -445,11 +345,11 @@ namespace Content.Client.Actions.UI var rightness = args.RelativePosition.X / _loadoutContainer.Width; if (rightness > 0.5) { - ChangeHotbar((byte) ((SelectedHotbar + 1) % ClientActionsComponent.Hotbars)); + ChangeHotbar((byte) ((SelectedHotbar + 1) % ActionsSystem.Hotbars)); } else { - var newBar = SelectedHotbar == 0 ? ClientActionsComponent.Hotbars - 1 : SelectedHotbar - 1; + var newBar = SelectedHotbar == 0 ? ActionsSystem.Hotbars - 1 : SelectedHotbar - 1; ChangeHotbar((byte) newBar); } } @@ -483,29 +383,35 @@ 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; - // show it as toggled on to indicate we are currently selecting a target for it - if (!actionSlot.ToggledOn) - { - actionSlot.ToggledOn = true; - } + if (actionSlot.Action is TargetedAction targetAction) + System.StartTargeting(targetAction); + + UpdateUI(); } /// /// Switch out of targeting mode if currently selecting target for an action /// - public void StopTargeting() + public void StopTargeting(bool updating = false) { - if (SelectingTargetFor == null) return; - if (SelectingTargetFor.ToggledOn) - { - SelectingTargetFor.ToggledOn = false; - } + if (SelectingTargetFor == null) + return; + 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) @@ -513,7 +419,6 @@ namespace Content.Client.Actions.UI ToggleActionsMenu(); } - private void OnToggleActionsMenuTopButton(bool open) { if (open == _menu.IsOpen) return; @@ -543,7 +448,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; @@ -574,6 +479,7 @@ 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 d16b70fc4b..46905601c5 100644 --- a/Content.Client/Clothing/MagbootsSystem.cs +++ b/Content.Client/Clothing/MagbootsSystem.cs @@ -1,10 +1,9 @@ using Content.Shared.Clothing; using Content.Shared.Movement.EntitySystems; -using Robust.Shared.GameObjects; namespace Content.Client.Clothing { - public sealed class MagbootsSystem : EntitySystem + public sealed class MagbootsSystem : SharedMagbootsSystem { public override void Initialize() { diff --git a/Content.Client/DragDrop/DragDropSystem.cs b/Content.Client/DragDrop/DragDropSystem.cs index bf7f9b8911..f362f2a778 100644 --- a/Content.Client/DragDrop/DragDropSystem.cs +++ b/Content.Client/DragDrop/DragDropSystem.cs @@ -204,7 +204,7 @@ namespace Content.Client.DragDrop } HighlightTargets(); - _outline.Enabled = false; + _outline.SetEnabled(false); // drag initiated return true; @@ -257,7 +257,7 @@ namespace Content.Client.DragDrop EntityManager.DeleteEntity(_dragShadow); } - _outline.Enabled = true; + _outline.SetEnabled(true); _dragShadow = default; _draggables.Clear(); _dragger = default; @@ -353,6 +353,7 @@ namespace Content.Client.DragDrop return false; } + // TODO make this just use TargetOutlineSystem private void HighlightTargets() { if (_dragDropHelper.Dragged == default || Deleted(_dragDropHelper.Dragged) || @@ -370,7 +371,7 @@ namespace Content.Client.DragDrop RemoveHighlights(); // find possible targets on screen even if not reachable - // TODO: Duplicated in SpriteSystem + // TODO: Duplicated in SpriteSystem and TargetOutlineSystem. Should probably be cached somewhere for a frame? 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 941d319df0..9feac603db 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -101,6 +101,9 @@ namespace Content.Client.Entry prototypes.RegisterIgnore("salvageMap"); prototypes.RegisterIgnore("gamePreset"); prototypes.RegisterIgnore("gameRule"); + prototypes.RegisterIgnore("worldSpell"); + prototypes.RegisterIgnore("entitySpell"); + prototypes.RegisterIgnore("instantSpell"); ClientContentIoC.Register(); @@ -189,7 +192,6 @@ 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 0c758af678..fa31384c4a 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -16,6 +16,7 @@ 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 b6a6abcc07..01bae5802d 100644 --- a/Content.Client/Hands/ShowHandItemOverlay.cs +++ b/Content.Client/Hands/ShowHandItemOverlay.cs @@ -5,9 +5,6 @@ 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 { @@ -16,11 +13,15 @@ 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); @@ -43,15 +44,25 @@ namespace Content.Client.Hands protected override void Draw(in OverlayDrawArgs args) { - var sys = EntitySystem.Get(); - var handEntity = sys.GetActiveHandEntity(); - - if (handEntity == null || !_cfg.GetCVar(CCVars.HudHeldItemShow) || !IoCManager.Resolve().HasComponent(handEntity)) + if (!_cfg.GetCVar(CCVars.HudHeldItemShow)) return; var screen = args.ScreenHandle; - var halfSize = _renderBackbuffer.Size / 2; + 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, () => @@ -59,11 +70,7 @@ 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 dbb02a7974..71751a6822 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -36,7 +36,6 @@ 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 a9847377b2..de5751ff62 100644 --- a/Content.Client/Outline/InteractionOutlineSystem.cs +++ b/Content.Client/Outline/InteractionOutlineSystem.cs @@ -24,14 +24,74 @@ public sealed class InteractionOutlineSystem : EntitySystem [Dependency] private readonly IUserInterfaceManager _uiManager = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; - public bool Enabled = true; + /// + /// 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; 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) @@ -81,16 +141,6 @@ 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 new file mode 100644 index 0000000000..98ef20c0de --- /dev/null +++ b/Content.Client/Outline/TargetOutlineSystem.cs @@ -0,0 +1,179 @@ +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 deleted file mode 100644 index 34801d65a9..0000000000 --- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs +++ /dev/null @@ -1,389 +0,0 @@ -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 9602cbabb2..c63a36eda3 100644 --- a/Content.Server/Act/IDisarmedAct.cs +++ b/Content.Server/Act/IDisarmedAct.cs @@ -1,30 +1,6 @@ -using System; -using Robust.Shared.Analyzers; -using Robust.Shared.GameObjects; - namespace Content.Server.Act { - /// - /// 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 + public sealed class DisarmedEvent : HandledEntityEventArgs { /// /// The entity being disarmed. diff --git a/Content.Server/Actions/Actions/CombatMode.cs b/Content.Server/Actions/Actions/CombatMode.cs deleted file mode 100644 index 9c628ad015..0000000000 --- a/Content.Server/Actions/Actions/CombatMode.cs +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index f5ef59ceeb..0000000000 --- a/Content.Server/Actions/Actions/DebugInstant.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 6f9ca4beaa..0000000000 --- a/Content.Server/Actions/Actions/DebugTargetEntity.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index f6013868d3..0000000000 --- a/Content.Server/Actions/Actions/DebugTargetPoint.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 69d297079c..0000000000 --- a/Content.Server/Actions/Actions/DebugToggle.cs +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index dd49625da2..0000000000 --- a/Content.Server/Actions/Actions/DisarmAction.cs +++ /dev/null @@ -1,126 +0,0 @@ -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 deleted file mode 100644 index 190fc05be4..0000000000 --- a/Content.Server/Actions/Actions/GhostBoo.cs +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 33b2b4b492..0000000000 --- a/Content.Server/Actions/Actions/GuardianToggleAction.cs +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 10e08b2731..0000000000 --- a/Content.Server/Actions/Actions/PAIMidi.cs +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index b86842ad40..0000000000 --- a/Content.Server/Actions/Actions/ScreamAction.cs +++ /dev/null @@ -1,71 +0,0 @@ -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 new file mode 100644 index 0000000000..0b15b5770b --- /dev/null +++ b/Content.Server/Actions/ActionsSystem.cs @@ -0,0 +1,40 @@ +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 deleted file mode 100644 index dfd9cb4f41..0000000000 --- a/Content.Server/Actions/Commands/CooldownAction.cs +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index 09d8fe4b7d..0000000000 --- a/Content.Server/Actions/Commands/GrantAction.cs +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 39db78d88d..0000000000 --- a/Content.Server/Actions/Commands/RevokeAction.cs +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 703e276174..0000000000 --- a/Content.Server/Actions/ServerActionsComponent.cs +++ /dev/null @@ -1,215 +0,0 @@ -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 deleted file mode 100644 index a57f5b5783..0000000000 --- a/Content.Server/Actions/Spells/GiveItemSpell.cs +++ /dev/null @@ -1,77 +0,0 @@ -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 54f725a7fd..c4b042a261 100644 --- a/Content.Server/Atmos/Components/GasTankComponent.cs +++ b/Content.Server/Atmos/Components/GasTankComponent.cs @@ -1,30 +1,21 @@ -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.Behaviors.Item; -using Content.Shared.Actions.Components; +using Content.Shared.Actions.ActionTypes; 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 { @@ -90,6 +81,9 @@ namespace Content.Server.Atmos.Components [DataField("tankFragmentScale")] public float TankFragmentScale { get; set; } = 10 * Atmospherics.OneAtmosphere; + [DataField("toggleAction", required: true)] + public InstantAction ToggleAction = new(); + protected override void Initialize() { base.Initialize(); @@ -165,6 +159,7 @@ namespace Content.Server.Atmos.Components var internals = GetInternalsComponent(); if (internals == null) return; IsConnected = internals.TryConnectTank(Owner); + EntitySystem.Get().SetToggled(ToggleAction, IsConnected); UpdateUserInterface(); } @@ -172,6 +167,7 @@ namespace Content.Server.Atmos.Components { if (!IsConnected) return; IsConnected = false; + EntitySystem.Get().SetToggled(ToggleAction, false); GetInternalsComponent(owner)?.DisconnectTank(); UpdateUserInterface(); } @@ -187,9 +183,6 @@ 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) @@ -309,22 +302,4 @@ 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 bc8d7af6ca..af969e75b3 100644 --- a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs @@ -1,10 +1,9 @@ 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 { @@ -20,6 +19,23 @@ 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 deleted file mode 100644 index 12f8b3c509..0000000000 --- a/Content.Server/Chemistry/ReagentEffects/DoAction.cs +++ /dev/null @@ -1,34 +0,0 @@ -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 new file mode 100644 index 0000000000..27cb88851a --- /dev/null +++ b/Content.Server/Chemistry/ReagentEffects/Scream.cs @@ -0,0 +1,15 @@ +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 418f071019..65d075a678 100644 --- a/Content.Server/Clothing/Components/MagbootsComponent.cs +++ b/Content.Server/Clothing/Components/MagbootsComponent.cs @@ -1,6 +1,4 @@ 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; @@ -38,8 +36,6 @@ 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)) @@ -49,14 +45,9 @@ namespace Content.Server.Clothing.Components } } - public void Toggle(EntityUid user) - { - On = !On; - } - void IActivate.Activate(ActivateEventArgs eventArgs) { - Toggle(eventArgs.User); + On = !On; } public override ComponentState GetComponentState() @@ -64,18 +55,4 @@ namespace Content.Server.Clothing.Components 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 c5252ebec0..8e14b61da3 100644 --- a/Content.Server/Clothing/MagbootsSystem.cs +++ b/Content.Server/Clothing/MagbootsSystem.cs @@ -1,18 +1,17 @@ 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 : EntitySystem + public sealed class MagbootsSystem : SharedMagbootsSystem { [Dependency] private readonly AlertsSystem _alertsSystem = default!; diff --git a/Content.Server/CombatMode/CombatModeSystem.cs b/Content.Server/CombatMode/CombatModeSystem.cs index cbb595c84f..47a57dc9fb 100644 --- a/Content.Server/CombatMode/CombatModeSystem.cs +++ b/Content.Server/CombatMode/CombatModeSystem.cs @@ -1,10 +1,82 @@ -using Content.Shared.CombatMode; +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 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 13d9e6d616..0d4b9125dd 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -113,7 +113,6 @@ 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 5752451106..6142b9e82b 100644 --- a/Content.Server/Ghost/Components/GhostComponent.cs +++ b/Content.Server/Ghost/Components/GhostComponent.cs @@ -1,6 +1,7 @@ -using System; +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; using Content.Shared.Ghost; -using Robust.Shared.GameObjects; +using Robust.Shared.Utility; namespace Content.Server.Ghost.Components { @@ -9,5 +10,23 @@ 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 a31854723e..893ff986a0 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -7,6 +7,7 @@ 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; @@ -15,10 +16,6 @@ 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 @@ -30,7 +27,9 @@ 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() @@ -51,6 +50,30 @@ 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) @@ -78,6 +101,8 @@ namespace Content.Server.Ghost } component.TimeOfDeath = _gameTiming.RealTime; + + _actions.AddAction(uid, component.Action, null); } private void OnGhostShutdown(EntityUid uid, GhostComponent component, ComponentShutdown args) @@ -98,6 +123,8 @@ 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 9771087e21..dfd108d523 100644 --- a/Content.Server/Guardian/GuardianHostComponent.cs +++ b/Content.Server/Guardian/GuardianHostComponent.cs @@ -1,6 +1,7 @@ +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; using Robust.Shared.Containers; -using Robust.Shared.GameObjects; -using Robust.Shared.ViewVariables; +using Robust.Shared.Utility; namespace Content.Server.Guardian { @@ -22,5 +23,17 @@ 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 0a01eedf48..c766e3c967 100644 --- a/Content.Server/Guardian/GuardianSystem.cs +++ b/Content.Server/Guardian/GuardianSystem.cs @@ -1,9 +1,7 @@ -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; @@ -13,10 +11,6 @@ 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; @@ -30,6 +24,7 @@ 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() { @@ -50,9 +45,22 @@ 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; @@ -74,12 +82,14 @@ 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) @@ -151,9 +161,6 @@ namespace Content.Server.Guardian return; } - // Can't work without actions - EntityManager.EnsureComponent(target); - if (component.Injecting) return; component.Injecting = true; @@ -175,8 +182,7 @@ namespace Content.Server.Guardian comp.Used || !TryComp(ev.User, out var hands) || !hands.IsHolding(comp.Owner) || - HasComp(ev.Target) || - !TryComp(ev.Target, out var actions)) + HasComp(ev.Target)) { comp.Injecting = false; return; @@ -194,8 +200,6 @@ 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 99c1f2ccb9..d89ef55e46 100644 --- a/Content.Server/Hands/Components/HandsComponent.cs +++ b/Content.Server/Hands/Components/HandsComponent.cs @@ -13,27 +13,19 @@ 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, IDisarmedAct + public sealed class HandsComponent : SharedHandsComponent, IBodyPartAdded, IBodyPartRemoved #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) @@ -62,33 +54,10 @@ namespace Content.Server.Hands.Components RemoveHand(args.Slot); } - 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() + public 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 b20f039ca4..cc892205d9 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -1,9 +1,12 @@ 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; @@ -18,6 +21,7 @@ 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; @@ -38,6 +42,7 @@ 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() { @@ -48,6 +53,7 @@ namespace Content.Server.Hands.Systems SubscribeNetworkEvent(HandleInteractUsingInHand); SubscribeNetworkEvent(HandleUseInHand); SubscribeNetworkEvent(HandleMoveItemFromHand); + SubscribeLocalEvent(OnDisarmed, before: new[] { typeof(StunSystem) }); SubscribeLocalEvent(HandlePullAttempt); SubscribeLocalEvent(HandlePullStarted); @@ -76,6 +82,26 @@ 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 e9d57e70e6..6a7640880e 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -35,7 +35,6 @@ 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 a24dba0fd2..5e7477f854 100644 --- a/Content.Server/Light/Components/HandheldLightComponent.cs +++ b/Content.Server/Light/Components/HandheldLightComponent.cs @@ -2,7 +2,6 @@ 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; @@ -48,17 +47,4 @@ 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 dcfe866498..50ba53117b 100644 --- a/Content.Server/Light/Components/UnpoweredFlashlightComponent.cs +++ b/Content.Server/Light/Components/UnpoweredFlashlightComponent.cs @@ -1,7 +1,5 @@ +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 { @@ -16,5 +14,8 @@ 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 = new(); } } diff --git a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs index 2b462bb696..2bbcf5770c 100644 --- a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs +++ b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs @@ -1,25 +1,19 @@ -using System.Collections.Generic; -using Content.Server.Clothing.Components; +using Content.Server.Actions; 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; @@ -30,6 +24,7 @@ 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. @@ -47,6 +42,27 @@ 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) @@ -155,7 +171,6 @@ namespace Content.Server.Light.EntitySystems SetState(component, false); component.Activated = false; - UpdateLightAction(component); _activeLights.Remove(component); component.LastLevel = null; component.Dirty(EntityManager); @@ -174,7 +189,6 @@ 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; } @@ -185,12 +199,10 @@ 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); @@ -217,13 +229,8 @@ namespace Content.Server.Light.EntitySystems { item.EquippedPrefix = on ? "on" : "off"; } - } - private void UpdateLightAction(HandheldLightComponent component) - { - if (!EntityManager.TryGetComponent(component.Owner, out ItemActionsComponent? actions)) return; - - actions.Toggle(ItemActionType.ToggleLight, component.Activated); + _actionSystem.SetToggled(component.ToggleAction, on); } public void TryUpdate(HandheldLightComponent component, float frameTime) diff --git a/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs b/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs index a8aabcb31a..682e387da8 100644 --- a/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs +++ b/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs @@ -1,24 +1,41 @@ 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) @@ -49,7 +66,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 eccdd40908..24462c89c0 100644 --- a/Content.Server/PAI/PAISystem.cs +++ b/Content.Server/PAI/PAISystem.cs @@ -13,6 +13,7 @@ using Robust.Shared.Log; using Robust.Shared.GameObjects; using Robust.Shared.Localization; using Robust.Shared.Player; +using Content.Shared.Actions; namespace Content.Server.PAI { @@ -20,6 +21,7 @@ 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() { @@ -30,6 +32,21 @@ 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 new file mode 100644 index 0000000000..6f84290d33 --- /dev/null +++ b/Content.Server/Speech/Components/VocalComponent.cs @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000000..39ae115f4f --- /dev/null +++ b/Content.Server/Speech/VocalSystem.cs @@ -0,0 +1,86 @@ +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 a9cc74eea2..8a5e22bcc2 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, DisarmedActEvent args) + private void OnDisarmed(EntityUid uid, StatusEffectsComponent status, DisarmedEvent args) { if (args.Handled || !_random.Prob(args.PushProbability)) return; diff --git a/Content.Server/UserInterface/ActivatableUISystem.cs b/Content.Server/UserInterface/ActivatableUISystem.cs index eb9959aeff..f88cb94bd9 100644 --- a/Content.Server/UserInterface/ActivatableUISystem.cs +++ b/Content.Server/UserInterface/ActivatableUISystem.cs @@ -1,5 +1,7 @@ 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; @@ -21,12 +23,29 @@ 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 new file mode 100644 index 0000000000..e7ea6174b7 --- /dev/null +++ b/Content.Server/UserInterface/OpenUiActionEvent.cs @@ -0,0 +1,23 @@ +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 d993bc7eba..31ee3065dc 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, UnequippedHandEvent args) + private void OnItemLeaveHand(EntityUid uid, WieldableComponent component, GotUnequippedHandEvent args) { if (!component.Wielded || component.Owner != args.Unequipped) return; diff --git a/Content.Shared.Database/LogType.cs b/Content.Shared.Database/LogType.cs index e98818d52a..24990cbea8 100644 --- a/Content.Shared.Database/LogType.cs +++ b/Content.Shared.Database/LogType.cs @@ -63,5 +63,6 @@ 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 new file mode 100644 index 0000000000..0a28d470b1 --- /dev/null +++ b/Content.Shared/Actions/ActionEvents.cs @@ -0,0 +1,61 @@ +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 deleted file mode 100644 index 8166f79dc2..0000000000 --- a/Content.Shared/Actions/ActionManager.cs +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index 4eb19eaf34..0000000000 --- a/Content.Shared/Actions/ActionType.cs +++ /dev/null @@ -1,41 +0,0 @@ -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 new file mode 100644 index 0000000000..e0d30d7659 --- /dev/null +++ b/Content.Shared/Actions/ActionTypes/ActionType.cs @@ -0,0 +1,276 @@ +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; + + + /// + /// Whether or not to automatically remove this action to the action bar when it becomes unavailable. + /// + [DataField("autoRemove")] + public bool AutoRemove = 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 = FormattedMessage.RemoveMarkup(Loc.GetString(Name)); + var otherName = FormattedMessage.RemoveMarkup(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; + AutoRemove = toClone.AutoRemove; + 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 new file mode 100644 index 0000000000..28a8b607f4 --- /dev/null +++ b/Content.Shared/Actions/ActionTypes/InstantAction.cs @@ -0,0 +1,41 @@ +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 new file mode 100644 index 0000000000..a5ecd581d6 --- /dev/null +++ b/Content.Shared/Actions/ActionTypes/TargetedAction.cs @@ -0,0 +1,153 @@ +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 new file mode 100644 index 0000000000..8c969228b2 --- /dev/null +++ b/Content.Shared/Actions/ActionsComponent.cs @@ -0,0 +1,46 @@ +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 deleted file mode 100644 index 304db1620e..0000000000 --- a/Content.Shared/Actions/Behaviors/IActionBehavior.cs +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 7d4f88d005..0000000000 --- a/Content.Shared/Actions/Behaviors/IInstantAction.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 629acc0713..0000000000 --- a/Content.Shared/Actions/Behaviors/ITargetEntityAction.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 9bf07df15b..0000000000 --- a/Content.Shared/Actions/Behaviors/ITargetEntityItemAction.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 2b329df310..0000000000 --- a/Content.Shared/Actions/Behaviors/ITargetPointAction.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 050b055f83..0000000000 --- a/Content.Shared/Actions/Behaviors/ITargetPointItemAction.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index c1263c5fd4..0000000000 --- a/Content.Shared/Actions/Behaviors/IToggleAction.cs +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 8adce4ce9a..0000000000 --- a/Content.Shared/Actions/Behaviors/Item/IInstantItemAction.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index d185eb6281..0000000000 --- a/Content.Shared/Actions/Behaviors/Item/IItemActionBehavior.cs +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 953de58a47..0000000000 --- a/Content.Shared/Actions/Behaviors/Item/IToggleItemAction.cs +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index b80b87bbb2..0000000000 --- a/Content.Shared/Actions/Components/ItemActionsComponent.cs +++ /dev/null @@ -1,203 +0,0 @@ -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 deleted file mode 100644 index 5fb0ecf68d..0000000000 --- a/Content.Shared/Actions/Components/SharedActionsComponent.cs +++ /dev/null @@ -1,641 +0,0 @@ -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 deleted file mode 100644 index ab3726057f..0000000000 --- a/Content.Shared/Actions/IActionAttempt.cs +++ /dev/null @@ -1,226 +0,0 @@ -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 deleted file mode 100644 index 6288e0f35e..0000000000 --- a/Content.Shared/Actions/Prototypes/ActionPrototype.cs +++ /dev/null @@ -1,102 +0,0 @@ -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 deleted file mode 100644 index 671e40c3ac..0000000000 --- a/Content.Shared/Actions/Prototypes/BaseActionPrototype.cs +++ /dev/null @@ -1,169 +0,0 @@ -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 deleted file mode 100644 index 6a3d40358e..0000000000 --- a/Content.Shared/Actions/Prototypes/ItemActionPrototype.cs +++ /dev/null @@ -1,125 +0,0 @@ -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 deleted file mode 100644 index df54099f83..0000000000 --- a/Content.Shared/Actions/SharedActionSystem.cs +++ /dev/null @@ -1,74 +0,0 @@ -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 new file mode 100644 index 0000000000..05a8726777 --- /dev/null +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -0,0 +1,459 @@ +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 virtual 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 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 aeee4ec0e6..6a6bb4cf7a 100644 --- a/Content.Shared/Clothing/SharedMagbootsComponent.cs +++ b/Content.Shared/Clothing/SharedMagbootsComponent.cs @@ -1,22 +1,24 @@ -using System; -using Content.Shared.Movement.Components; +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; 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 = new(); + 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 new file mode 100644 index 0000000000..21e132d1ff --- /dev/null +++ b/Content.Shared/Clothing/SharedMagbootsSystem.cs @@ -0,0 +1,30 @@ +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 3a5e961ae1..a52d1fbc97 100644 --- a/Content.Shared/CombatMode/SharedCombatModeComponent.cs +++ b/Content.Shared/CombatMode/SharedCombatModeComponent.cs @@ -1,10 +1,11 @@ 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.ViewVariables; +using Robust.Shared.Utility; namespace Content.Shared.CombatMode { @@ -14,6 +15,50 @@ 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 { @@ -22,6 +67,7 @@ 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 3cdb3231d0..3092314847 100644 --- a/Content.Shared/CombatMode/SharedCombatModeSystem.cs +++ b/Content.Shared/CombatMode/SharedCombatModeSystem.cs @@ -1,15 +1,42 @@ -using Robust.Shared.GameObjects; +using Content.Shared.Actions; 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) @@ -24,4 +51,7 @@ 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 1ecba28b93..df29474cc5 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 sealed class EquippedHandEvent : HandledEntityEventArgs + public abstract 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 sealed class UnequippedHandEvent : HandledEntityEventArgs + public abstract class UnequippedHandEvent : HandledEntityEventArgs { /// /// Entity that equipped the item. @@ -157,4 +157,24 @@ 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 83804fb8e0..151095bac4 100644 --- a/Content.Shared/Hands/SharedHandsSystem.cs +++ b/Content.Shared/Hands/SharedHandsSystem.cs @@ -92,10 +92,11 @@ namespace Content.Shared.Hands hands.Dirty(); - var unequippedHandMessage = new UnequippedHandEvent(uid, entity, hand); - RaiseLocalEvent(entity, unequippedHandMessage); - if (unequippedHandMessage.Handled) - return; + var gotUnequipped = new GotUnequippedHandEvent(uid, entity, hand); + RaiseLocalEvent(entity, gotUnequipped, false); + + var didUnequip = new DidUnequipHandEvent(uid, entity, hand); + RaiseLocalEvent(uid, didUnequip); if (hand.Name == hands.ActiveHand) RaiseLocalEvent(entity, new HandDeselectedEvent(uid, entity), false); @@ -123,11 +124,15 @@ namespace Content.Shared.Hands hands.Dirty(); - var equippedHandMessage = new EquippedHandEvent(uid, entity, hand); - RaiseLocalEvent(entity, equippedHandMessage); + var didEquip = new DidEquipHandEvent(uid, entity, hand); + RaiseLocalEvent(uid, didEquip, false); + 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 (equippedHandMessage.Handled) + if (gotEquipped.Handled) return; if (hand.Name == hands.ActiveHand) diff --git a/Content.Shared/Light/Component/SharedHandheldLightComponent.cs b/Content.Shared/Light/Component/SharedHandheldLightComponent.cs index 7f15efc399..5ede7633c8 100644 --- a/Content.Shared/Light/Component/SharedHandheldLightComponent.cs +++ b/Content.Shared/Light/Component/SharedHandheldLightComponent.cs @@ -1,5 +1,4 @@ -using System; -using Robust.Shared.GameObjects; +using Content.Shared.Actions.ActionTypes; using Robust.Shared.GameStates; using Robust.Shared.Serialization; @@ -8,6 +7,9 @@ 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 eb86912f59..62aea5c35e 100644 --- a/Content.Shared/PAI/PAIComponent.cs +++ b/Content.Shared/PAI/PAIComponent.cs @@ -1,6 +1,5 @@ -using Robust.Shared.GameObjects; +using Content.Shared.Actions.ActionTypes; using Robust.Shared.GameStates; -using Robust.Shared.Serialization; namespace Content.Shared.PAI { @@ -17,6 +16,8 @@ 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 new file mode 100644 index 0000000000..31055b6c3f --- /dev/null +++ b/Content.Shared/Toggleable/ToggleActionEvent.cs @@ -0,0 +1,8 @@ +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 96bd3080e2..e3ee4adcc8 100644 --- a/Content.Shared/Whitelist/EntityWhitelist.cs +++ b/Content.Shared/Whitelist/EntityWhitelist.cs @@ -25,6 +25,7 @@ namespace Content.Shared.Whitelist /// - AsteroidRock /// [DataDefinition] + [Serializable, NetSerializable] public sealed class EntityWhitelist : ISerializationHooks { /// @@ -32,6 +33,7 @@ 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 20bb5d22d9..0dad773258 100644 --- a/Resources/Locale/en-US/actions/actions/combat-mode.ftl +++ b/Resources/Locale/en-US/actions/actions/combat-mode.ftl @@ -1,2 +1,6 @@ -hud-combat-enabled = Combat mode enabled! -hud-combat-disabled = Combat mode disabled. \ No newline at end of file +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 diff --git a/Resources/Locale/en-US/actions/actions/disarm-action.ftl b/Resources/Locale/en-US/actions/actions/disarm-action.ftl index 17ddf1c994..2625ec336e 100644 --- a/Resources/Locale/en-US/actions/actions/disarm-action.ftl +++ b/Resources/Locale/en-US/actions/actions/disarm-action.ftl @@ -1,2 +1,5 @@ disarm-action-popup-message-other-clients = {$performerName} fails to disarm {$targetName}! -disarm-action-popup-message-cursor = You fail to disarm {$targetName}! \ No newline at end of file +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. diff --git a/Resources/Locale/en-US/actions/actions/ghost.ftl b/Resources/Locale/en-US/actions/actions/ghost.ftl new file mode 100644 index 0000000000..f10c559adf --- /dev/null +++ b/Resources/Locale/en-US/actions/actions/ghost.ftl @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000000..cc24d80037 --- /dev/null +++ b/Resources/Locale/en-US/actions/actions/internals.ftl @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000000..e883910870 --- /dev/null +++ b/Resources/Locale/en-US/actions/actions/light.ftl @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000000..ce6c4d9e56 --- /dev/null +++ b/Resources/Locale/en-US/actions/actions/mapping.ftl @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000000..4be31f28c7 --- /dev/null +++ b/Resources/Locale/en-US/actions/actions/pai.ftl @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000000..ced5c04565 --- /dev/null +++ b/Resources/Locale/en-US/actions/actions/vocal.ftl @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000000..332054f10e --- /dev/null +++ b/Resources/Locale/en-US/actions/ui/actionslot.ftl @@ -0,0 +1,2 @@ +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 cf7ec019bc..48c14dc0e1 100644 --- a/Resources/Locale/en-US/guardian/guardian.ftl +++ b/Resources/Locale/en-US/guardian/guardian.ftl @@ -2,7 +2,6 @@ ## 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. @@ -11,6 +10,11 @@ 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 new file mode 100644 index 0000000000..7f8f55fefb --- /dev/null +++ b/Resources/Locale/en-US/magboot/actions.ftl @@ -0,0 +1,2 @@ +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 deleted file mode 100644 index 0ad1aca925..0000000000 --- a/Resources/Prototypes/Actions/actions.yml +++ /dev/null @@ -1,147 +0,0 @@ -- 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 deleted file mode 100644 index 1725cf62b8..0000000000 --- a/Resources/Prototypes/Actions/guardian_actions.yml +++ /dev/null @@ -1,12 +0,0 @@ -#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 deleted file mode 100644 index d2eb257e33..0000000000 --- a/Resources/Prototypes/Actions/item_actions.yml +++ /dev/null @@ -1,125 +0,0 @@ -- 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 deleted file mode 100644 index 1002510087..0000000000 --- a/Resources/Prototypes/Actions/magboots.yml +++ /dev/null @@ -1,13 +0,0 @@ -- 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 deleted file mode 100644 index f7a93149b8..0000000000 --- a/Resources/Prototypes/Actions/spells.yml +++ /dev/null @@ -1,14 +0,0 @@ -- 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 315bf334ef..ec450d63db 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml @@ -63,12 +63,13 @@ - 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 46f9f317bc..f100c5087e 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml @@ -19,9 +19,12 @@ visuals: - type: FlashLightVisualizer - type: HandheldLight - - type: ItemActions - actions: - - actionType: ToggleLight + 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: PowerCellSmallHigh diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml index e0e0ddd562..efaa070122 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml @@ -10,9 +10,13 @@ - type: Clothing sprite: Clothing/Shoes/Boots/magboots.rsi - type: Magboots - - type: ItemActions - actions: - - actionType: ToggleMagboots + 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: entity parent: ClothingShoesBootsMag diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 8bfb235801..126a7010ce 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -643,8 +643,15 @@ tags: - Trash - type: Recyclable - - type: Actions # TODO: Remove CombatMode when Prototype Composition is added - innateActions: [] + - type: Actions + # TODO: Remove CombatMode when Prototype Composition is added + - type: CombatMode + combatToggleAction: + enabled: false + autoPopulate: false + disarmAction: + enabled: false + autoPopulate: false - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index 58408d5e20..f7f2cbafe5 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -121,8 +121,6 @@ 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 9c2e842e18..53906909d8 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -13,9 +13,6 @@ - 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 d8ea943b26..ee78226f03 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml @@ -13,10 +13,6 @@ - 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 8ed853e942..47d7e30803 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml @@ -68,8 +68,6 @@ 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 fffca1673f..36f70b2499 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -20,10 +20,6 @@ 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 c5a90eadd8..d9e5e0b728 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -45,6 +45,3 @@ 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 34b22801ac..5f7f01696e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/slime.yml @@ -11,10 +11,6 @@ - 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 9ff33beab3..da533f0385 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 9ae64f27bb..012edab8ba 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -288,6 +288,7 @@ # - 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 50fc424059..2430d80f5f 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -22,6 +22,12 @@ - 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 aab82ce023..54a81dc8ab 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/pai.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/pai.yml @@ -25,12 +25,17 @@ - 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 2c8cf1c26e..8a422b132d 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml @@ -5,10 +5,13 @@ 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 eef703450a..eb987c752c 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml @@ -8,9 +8,12 @@ tags: - Flashlight - type: HandheldLight - - type: ItemActions - actions: - - actionType: ToggleLight + 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: PowerCellSmallHigh diff --git a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml index 67d4ee317f..994624e7b3 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml @@ -15,9 +15,12 @@ sprite: Objects/Tanks/generic.rsi QuickEquip: false - type: GasTank - - type: ItemActions - actions: - - actionType: ToggleInternals + 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: entity parent: GasTankBase diff --git a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml index c644120d0e..3c96a8f58d 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml @@ -5,9 +5,12 @@ description: The holy light guides the way. components: - type: HandheldLight - - type: ItemActions - actions: - - actionType: ToggleLight + 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: Sprite sprite: Objects/Tools/lantern.rsi layers: diff --git a/Resources/Prototypes/Reagents/pyrotechnic.yml b/Resources/Prototypes/Reagents/pyrotechnic.yml index e47609d663..3003caaf6b 100644 --- a/Resources/Prototypes/Reagents/pyrotechnic.yml +++ b/Resources/Prototypes/Reagents/pyrotechnic.yml @@ -112,9 +112,8 @@ - !type:FlammableReaction multiplier: 0.3 - !type:Ignite - - !type:DoAction + - !type:Scream 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 64c54d8cc1..bd442809b4 100644 --- a/Resources/Prototypes/Reagents/toxins.yml +++ b/Resources/Prototypes/Reagents/toxins.yml @@ -44,9 +44,8 @@ damage: groups: Caustic: 0.5 - - !type:DoAction + - !type:Scream probability: 0.3 - action: HumanScream metabolisms: Poison: effects: @@ -78,9 +77,8 @@ damage: groups: Caustic: 0.3 - - !type:DoAction + - !type:Scream probability: 0.2 - action: HumanScream metabolisms: Poison: effects: @@ -119,9 +117,8 @@ damage: groups: Caustic: 0.1 - - !type:DoAction + - !type:Scream probability: 0.1 - action: HumanScream metabolisms: Poison: effects: