fish-station/Content.Shared/Actions/SharedActionsSystem.cs
Vigers Ray 939076ff99 Merge remote-tracking branch 'space-wizards/master'
# Conflicts:
#	.github/CODEOWNERS
#	.github/workflows/publish.yml
#	Content.Client/Administration/UI/BanPanel/BanPanel.xaml
#	Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
#	Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml.cs
#	Content.Client/Entry/EntryPoint.cs
#	Content.Client/Hands/Systems/HandsSystem.cs
#	Content.Client/Humanoid/HumanoidAppearanceSystem.cs
#	Content.Client/Humanoid/SingleMarkingPicker.xaml.cs
#	Content.Client/IoC/ClientContentIoC.cs
#	Content.Client/Lobby/LobbyState.cs
#	Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
#	Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs
#	Content.Client/Lobby/UI/LobbyGui.xaml
#	Content.Client/NetworkConfigurator/NetworkConfiguratorConfigurationMenu.xaml
#	Content.Client/Options/UI/EscapeMenu.xaml
#	Content.Client/Stealth/StealthSystem.cs
#	Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorWindow.xaml.cs
#	Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
#	Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs
#	Content.Client/Weapons/Ranged/Systems/GunSystem.cs
#	Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
#	Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
#	Content.IntegrationTests/Tests/PostMapInitTest.cs
#	Content.Server/Administration/Commands/AdminWhoCommand.cs
#	Content.Server/Administration/Commands/PardonCommand.cs
#	Content.Server/Administration/Commands/RoleUnbanCommand.cs
#	Content.Server/Administration/Commands/SetOutfitCommand.cs
#	Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
#	Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
#	Content.Server/AlertLevel/Commands/SetAlertLevelCommand.cs
#	Content.Server/Body/Components/BloodstreamComponent.cs
#	Content.Server/Body/Systems/BloodstreamSystem.cs
#	Content.Server/Body/Systems/MetabolizerSystem.cs
#	Content.Server/Body/Systems/RespiratorSystem.cs
#	Content.Server/Chat/Managers/ChatSanitizationManager.cs
#	Content.Server/Chat/Systems/ChatSystem.Emote.cs
#	Content.Server/Chat/Systems/ChatSystem.cs
#	Content.Server/CrewManifest/CrewManifestSystem.cs
#	Content.Server/Emp/EmpSystem.cs
#	Content.Server/Explosion/EntitySystems/TriggerSystem.cs
#	Content.Server/Fax/FaxSystem.cs
#	Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs
#	Content.Server/GameTicking/Commands/ForceMapCommand.cs
#	Content.Server/GameTicking/Commands/ForcePresetCommand.cs
#	Content.Server/GameTicking/Presets/GamePresetPrototype.cs
#	Content.Server/GameTicking/Rules/ChangelingRuleSystem.cs
#	Content.Server/GameTicking/Rules/Components/ChangelingRuleComponent.cs
#	Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
#	Content.Server/Ghost/Roles/GhostRoleSystem.cs
#	Content.Server/Implants/Components/ScramImplantComponent.cs
#	Content.Server/Implants/SubdermalImplantSystem.cs
#	Content.Server/Materials/ProduceMaterialExtractorSystem.cs
#	Content.Server/Medical/HealthAnalyzerSystem.cs
#	Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs
#	Content.Server/NPC/Systems/NPCSystem.cs
#	Content.Server/Polymorph/Systems/PolymorphSystem.cs
#	Content.Server/Radio/EntitySystems/HeadsetSystem.cs
#	Content.Server/Repairable/RepairableSystem.cs
#	Content.Server/Revenant/EntitySystems/RevenantSystem.cs
#	Content.Server/RoundEnd/RoundEndSystem.cs
#	Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
#	Content.Server/Speech/EntitySystems/FrenchAccentSystem.cs
#	Content.Server/Speech/EntitySystems/FrontalLispSystem.cs
#	Content.Server/Speech/EntitySystems/LizardAccentSystem.cs
#	Content.Server/Speech/EntitySystems/MothAccentSystem.cs
#	Content.Server/Speech/EntitySystems/OwOAccentSystem.cs
#	Content.Server/Station/Systems/StationSpawningSystem.cs
#	Content.Server/StationEvents/Events/MeteorSwarmSystem.cs
#	Content.Server/Stunnable/Components/StunOnCollideComponent.cs
#	Content.Server/SurveillanceCamera/Components/SurveillanceCameraRouterComponent.cs
#	Content.Server/Weapons/Ranged/Systems/GunSystem.Cartridges.cs
#	Content.Server/Zombies/ZombieSystem.Transform.cs
#	Content.Server/Zombies/ZombieSystem.cs
#	Content.Shared/Actions/SharedActionsSystem.cs
#	Content.Shared/Bed/SharedBedSystem.cs
#	Content.Shared/Blocking/BlockingSystem.cs
#	Content.Shared/Body/Systems/SharedBodySystem.Parts.cs
#	Content.Shared/Chemistry/Components/SolutionRegenerationComponent.cs
#	Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs
#	Content.Shared/Construction/Conditions/TileNotBlocked.cs
#	Content.Shared/Cuffs/SharedCuffableSystem.cs
#	Content.Shared/Doors/Systems/SharedDoorSystem.cs
#	Content.Shared/Emoting/EmoteAttemptEvent.cs
#	Content.Shared/Eye/Blinding/Systems/BlindableSystem.cs
#	Content.Shared/Eye/VisibilityFlags.cs
#	Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs
#	Content.Shared/Humanoid/HumanoidCharacterAppearance.cs
#	Content.Shared/Interaction/SmartEquipSystem.cs
#	Content.Shared/Inventory/InventorySystem.Relay.cs
#	Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs
#	Content.Shared/Medical/Healing/HealingSystem.cs
#	Content.Shared/Medical/HealingComponent.cs
#	Content.Shared/Nutrition/EntitySystems/FoodSystem.cs
#	Content.Shared/Slippery/SlipperySystem.cs
#	Content.Shared/SprayPainter/SharedSprayPainterSystem.cs
#	Content.Shared/Standing/StandingStateComponent.cs
#	Content.Shared/Standing/StandingStateSystem.cs
#	Content.Shared/Station/SharedStationSpawningSystem.cs
#	Content.Shared/Stunnable/SharedStunSystem.cs
#	Content.Shared/SurveillanceCamera/SharedSurveillanceCameraMonitorSystem.cs
#	Content.Shared/VendingMachines/VendingMachineComponent.cs
#	Content.Shared/Warps/WarpPointSystem.cs
#	Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
#	Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
#	Content.Shared/Wieldable/SharedWieldableSystem.cs
#	Resources/Audio/Ambience/Antag/attributions.yml
#	Resources/Audio/Ambience/Antag/changeling_start.ogg
#	Resources/Audio/Effects/attributions.yml
#	Resources/Audio/Lobby/attributions.yml
#	Resources/Audio/Machines/attributions.yml
#	Resources/Audio/Weapons/Guns/Gunshots/attributions.yml
#	Resources/Locale/en-US/_strings/administration/antag.ftl
#	Resources/Locale/en-US/_strings/administration/commands/camera.ftl
#	Resources/Locale/en-US/_strings/administration/commands/dsay-command.ftl
#	Resources/Locale/en-US/_strings/administration/commands/follow-command.ftl
#	Resources/Locale/en-US/_strings/administration/smites.ftl
#	Resources/Locale/en-US/_strings/administration/ui/admin-camera-window.ftl
#	Resources/Locale/en-US/_strings/administration/ui/role-bans.ftl
#	Resources/Locale/en-US/_strings/alerts/alerts.ftl
#	Resources/Locale/en-US/_strings/atmos/gas-pressure-regulator-system.ftl
#	Resources/Locale/en-US/_strings/bonk/components/bonkable-component.ftl
#	Resources/Locale/en-US/_strings/commands/add-body-part-command.ftl
#	Resources/Locale/en-US/_strings/commands/admin-logs-command.ftl
#	Resources/Locale/en-US/_strings/commands/admin-who-command.ftl
#	Resources/Locale/en-US/_strings/commands/asay-command.ftl
#	Resources/Locale/en-US/_strings/commands/crew-manifest-command.ftl
#	Resources/Locale/en-US/_strings/commands/deadmin-command.ftl
#	Resources/Locale/en-US/_strings/commands/delete-component-command.ftl
#	Resources/Locale/en-US/_strings/commands/destroy-mechanism-command.ftl
#	Resources/Locale/en-US/_strings/commands/dirty-command.ftl
#	Resources/Locale/en-US/_strings/commands/dsay-command.ftl
#	Resources/Locale/en-US/_strings/commands/dump-reagent-guide-text-command.ftl
#	Resources/Locale/en-US/_strings/commands/eye-commands.ftl
#	Resources/Locale/en-US/_strings/commands/follow-command.ftl
#	Resources/Locale/en-US/_strings/commands/force-map-command.ftl
#	Resources/Locale/en-US/_strings/commands/force-preset-command.ftl
#	Resources/Locale/en-US/_strings/commands/ghost-kick-command.ftl
#	Resources/Locale/en-US/_strings/commands/ghost-visibility-commands.ftl
#	Resources/Locale/en-US/_strings/commands/go-lobby-command.ftl
#	Resources/Locale/en-US/_strings/commands/grid-drag-command.ftl
#	Resources/Locale/en-US/_strings/commands/is-afk-command.ftl
#	Resources/Locale/en-US/_strings/commands/makesentient-command.ftl
#	Resources/Locale/en-US/_strings/commands/map-commands.ftl
#	Resources/Locale/en-US/_strings/commands/mapping-client-side-setup-command.ftl
#	Resources/Locale/en-US/_strings/commands/mappingclientsidesetup-command.ftl
#	Resources/Locale/en-US/_strings/commands/me-command.ftl
#	Resources/Locale/en-US/_strings/commands/mind-info-command.ftl
#	Resources/Locale/en-US/_strings/commands/node-vis-commands.ftl
#	Resources/Locale/en-US/_strings/commands/npc-command.ftl
#	Resources/Locale/en-US/_strings/commands/nukecodes-command.ftl
#	Resources/Locale/en-US/_strings/commands/ooc-command.ftl
#	Resources/Locale/en-US/_strings/commands/pardon-command.ftl
#	Resources/Locale/en-US/_strings/commands/powerstat-command.ftl
#	Resources/Locale/en-US/_strings/commands/promote-host-command.ftl
#	Resources/Locale/en-US/_strings/commands/radiation-command.ftl
#	Resources/Locale/en-US/_strings/commands/readmin-command.ftl
#	Resources/Locale/en-US/_strings/commands/remove-extra-components-command.ftl
#	Resources/Locale/en-US/_strings/commands/role-commands.ftl
#	Resources/Locale/en-US/_strings/commands/say-command.ftl
#	Resources/Locale/en-US/_strings/commands/set-battery-percent-command.ftl
#	Resources/Locale/en-US/_strings/commands/show-ambient-command.ftl
#	Resources/Locale/en-US/_strings/commands/show-gun-spread-command.ftl
#	Resources/Locale/en-US/_strings/commands/show-htn-command.ftl
#	Resources/Locale/en-US/_strings/commands/showrules-command.ftl
#	Resources/Locale/en-US/_strings/commands/spawn-entity-list-command.ftl
#	Resources/Locale/en-US/_strings/commands/start-singularity-command.ftl
#	Resources/Locale/en-US/_strings/commands/tile-pry-command.ftl
#	Resources/Locale/en-US/_strings/commands/toggle-decals-command.ftl
#	Resources/Locale/en-US/_strings/commands/toggle-disallow-late-join-command.ftl
#	Resources/Locale/en-US/_strings/commands/unlocknode-command.ftl
#	Resources/Locale/en-US/_strings/commands/whisper-command.ftl
#	Resources/Locale/en-US/_strings/components/gas-pressure-regulator-component.ftl
#	Resources/Locale/en-US/_strings/engineer-painter/engineer-painter.ftl
#	Resources/Locale/en-US/_strings/game-ticking/forcemap-command.ftl
#	Resources/Locale/en-US/_strings/interaction/interaction-popup-component.ftl
#	Resources/Locale/en-US/_strings/markings/vox_tattoos.ftl
#	Resources/Locale/en-US/_strings/materials/material-extractor.ftl
#	Resources/Locale/en-US/_strings/movement/eye.ftl
#	Resources/Locale/en-US/_strings/nutrition/components/food-component.ftl
#	Resources/Locale/en-US/_strings/nutrition/components/ingestion-system.ftl
#	Resources/Locale/en-US/_strings/nutrition/components/sealable-component.ftl
#	Resources/Locale/en-US/_strings/radiation/radiation-command.ftl
#	Resources/Locale/en-US/_strings/reagents/meta/physical-desc.ftl
#	Resources/Locale/en-US/_strings/research/technologies.ftl
#	Resources/Locale/en-US/_strings/seeds/seeds.ftl
#	Resources/Locale/en-US/_strings/weapons/grenades/timer-trigger.ftl
#	Resources/Locale/en-US/_strings/weapons/grenades/voice-trigger.ftl
#	Resources/Locale/en-US/commands/mapping-client-side-setup-command.ftl
#	Resources/Locale/en-US/recipes/tags.ftl
#	Resources/Maps/_Sunrise/Station/bagel.yml
#	Resources/Maps/_Sunrise/Station/barratry.yml
#	Resources/Maps/_Sunrise/Station/box.yml
#	Resources/Maps/_Sunrise/Station/fland.yml
#	Resources/Maps/_Sunrise/Station/marathon.yml
#	Resources/Maps/_Sunrise/Station/oasis.yml
#	Resources/Maps/_Sunrise/Station/packed.yml
#	Resources/Maps/_Sunrise/Station/plasma.yml
#	Resources/Maps/amber.yml
#	Resources/Maps/centcomm.yml
#	Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/security.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/service.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/suit_storage.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/coffee.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/sustenance.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/winterdrobe.yml
#	Resources/Prototypes/Entities/Clothing/Belt/belts.yml
#	Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml
#	Resources/Prototypes/Entities/Clothing/Eyes/hud.yml
#	Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml
#	Resources/Prototypes/Entities/Clothing/Head/hats.yml
#	Resources/Prototypes/Entities/Clothing/Head/helmets.yml
#	Resources/Prototypes/Entities/Clothing/Head/hoods.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/softsuits.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml
#	Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
#	Resources/Prototypes/Entities/Mobs/NPCs/carp.yml
#	Resources/Prototypes/Entities/Mobs/NPCs/pets.yml
#	Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml
#	Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml
#	Resources/Prototypes/Entities/Mobs/Player/clone.yml
#	Resources/Prototypes/Entities/Mobs/Species/base.yml
#	Resources/Prototypes/Entities/Mobs/Species/dwarf.yml
#	Resources/Prototypes/Entities/Mobs/base.yml
#	Resources/Prototypes/Entities/Objects/Fun/pai.yml
#	Resources/Prototypes/Entities/Objects/Fun/toys.yml
#	Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
#	Resources/Prototypes/Entities/Objects/Shields/shields.yml
#	Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml
#	Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml
#	Resources/Prototypes/Entities/Objects/Tools/access_configurator.yml
#	Resources/Prototypes/Entities/Objects/Tools/matches.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Bombs/plastic.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Bombs/spider.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/caseless_rifle.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/light_rifle.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/magnum.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/grenade.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
#	Resources/Prototypes/Entities/Structures/Doors/airlock_groups.yml
#	Resources/Prototypes/Entities/Structures/Furniture/beds.yml
#	Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml
#	Resources/Prototypes/Entities/Structures/Power/debug_power.yml
#	Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml
#	Resources/Prototypes/Entities/Structures/Wallmounts/Misc/mirror.yml
#	Resources/Prototypes/GameRules/subgamemodes.yml
#	Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml
#	Resources/Prototypes/Reagents/Consumable/Drink/soda.yml
#	Resources/Prototypes/Reagents/medicine.yml
#	Resources/Prototypes/Recipes/Lathes/Packs/tiles.yml
#	Resources/Prototypes/Recipes/Lathes/categories.yml
#	Resources/Prototypes/Recipes/Reactions/chemicals.yml
#	Resources/Prototypes/Roles/Antags/changeling.yml
#	Resources/Prototypes/name_identifier_groups.yml
#	Resources/Prototypes/tool_qualities.yml
#	Resources/ServerInfo/Guidebook/Antagonist/Zombies.xml
#	Resources/ServerInfo/Guidebook/Cargo/CargoBounties.xml
#	Resources/ServerInfo/Guidebook/Command.xml
#	Resources/ServerInfo/Guidebook/Glossary.xml
#	Resources/ServerInfo/Guidebook/NewPlayer/YourFirstCharacter.xml
#	Resources/ServerInfo/Guidebook/Science/Robotics.xml
#	Resources/ServerInfo/Guidebook/Science/ScannersAndVessels.xml
#	Resources/ServerInfo/Guidebook/Science/Science.xml
#	Resources/ServerInfo/Guidebook/SpaceStation14.xml
#	Resources/ServerInfo/Guidebook/Survival.xml
#	Resources/Textures/Clothing/Head/Hardsuits/atmospherics.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/capspace.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/cybersun.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/engineering-white.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/engineering-white.rsi/on-equipped-HELMET.png
#	Resources/Textures/Clothing/Head/Hardsuits/engineering.rsi/icon.png
#	Resources/Textures/Clothing/Head/Hardsuits/engineering.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/engineering.rsi/on-equipped-HELMET.png
#	Resources/Textures/Clothing/Head/Hardsuits/medical.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/rd.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/salvage.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/security.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/spatiohelm.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/syndicate.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/syndiecommander.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/syndieelite.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/wizard.rsi/meta.json
#	Resources/Textures/Clothing/Head/Helmets/atmos_firehelmet.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Armor/captain_carapace.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Armor/security.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/atmospherics.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/capspace.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/cybersun.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/engineering-white.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/engineering.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/medical.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/paramed.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/rd.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/salvage.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/security.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/spatio.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/syndicate.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/syndiecommander.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/syndieelite.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/wizard.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Suits/atmos_firesuit.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Suits/eva.rsi/meta.json
#	Resources/Textures/Interface/Misc/job_icons.rsi/meta.json
#	Resources/Textures/Objects/Misc/Lights/lampgreen.rsi/meta.json
#	Resources/Textures/Objects/Specific/Janitorial/janitorial_cart.rsi/meta.json
#	Resources/Textures/Objects/Tools/spray_painter.rsi/meta.json
#	Resources/Textures/Objects/Weapons/Melee/baseball_bat.rsi/meta.json
#	Resources/engineCommandPerms.yml
#	Resources/keybinds.yml
#	Resources/migration.yml
2025-08-10 01:49:23 +03:00

1123 lines
39 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared._Sunrise.Antags.Abductor;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions.Components;
using Content.Shared.Actions.Events;
using Content.Shared.Administration.Logs;
using Content.Shared.Charges.Components;
using Content.Shared.Database;
using Content.Shared.Hands;
using Content.Shared.Interaction;
using Content.Shared.Inventory.Events;
using Content.Shared.Mind;
using Content.Shared.Rejuvenate;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Actions;
public abstract class SharedActionsSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming GameTiming = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly RotateToFaceSystem _rotateToFace = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private EntityQuery<ActionComponent> _actionQuery;
private EntityQuery<TargetActionComponent> _targetActionQuery;
private EntityQuery<ActionsComponent> _actionsQuery;
private EntityQuery<MindComponent> _mindQuery;
public override void Initialize()
{
base.Initialize();
_actionQuery = GetEntityQuery<ActionComponent>();
_actionsQuery = GetEntityQuery<ActionsComponent>();
_mindQuery = GetEntityQuery<MindComponent>();
SubscribeLocalEvent<ActionComponent, MapInitEvent>(OnActionMapInit);
SubscribeLocalEvent<ActionComponent, ComponentShutdown>(OnActionShutdown);
SubscribeLocalEvent<ActionsComponent, ActionComponentChangeEvent>(OnActionCompChange);
SubscribeLocalEvent<ActionsComponent, RelayedActionComponentChangeEvent>(OnRelayActionCompChange);
SubscribeLocalEvent<ActionsComponent, DidEquipEvent>(OnDidEquip);
SubscribeLocalEvent<ActionsComponent, DidEquipHandEvent>(OnHandEquipped);
SubscribeLocalEvent<ActionsComponent, DidUnequipEvent>(OnDidUnequip);
SubscribeLocalEvent<ActionsComponent, DidUnequipHandEvent>(OnHandUnequipped);
SubscribeLocalEvent<ActionsComponent, RejuvenateEvent>(OnRejuventate);
SubscribeLocalEvent<ActionsComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ActionsComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<ActionComponent, ActionValidateEvent>(OnValidate);
SubscribeLocalEvent<InstantActionComponent, ActionValidateEvent>(OnInstantValidate);
SubscribeLocalEvent<EntityTargetActionComponent, ActionValidateEvent>(OnEntityValidate);
SubscribeLocalEvent<WorldTargetActionComponent, ActionValidateEvent>(OnWorldValidate);
SubscribeLocalEvent<InstantActionComponent, ActionGetEventEvent>(OnInstantGetEvent);
SubscribeLocalEvent<EntityTargetActionComponent, ActionGetEventEvent>(OnEntityGetEvent);
SubscribeLocalEvent<WorldTargetActionComponent, ActionGetEventEvent>(OnWorldGetEvent);
SubscribeLocalEvent<InstantActionComponent, ActionSetEventEvent>(OnInstantSetEvent);
SubscribeLocalEvent<EntityTargetActionComponent, ActionSetEventEvent>(OnEntitySetEvent);
SubscribeLocalEvent<WorldTargetActionComponent, ActionSetEventEvent>(OnWorldSetEvent);
SubscribeLocalEvent<EntityTargetActionComponent, ActionSetTargetEvent>(OnEntitySetTarget);
SubscribeLocalEvent<WorldTargetActionComponent, ActionSetTargetEvent>(OnWorldSetTarget);
SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest);
}
private void OnActionMapInit(Entity<ActionComponent> ent, ref MapInitEvent args)
{
var comp = ent.Comp;
comp.OriginalIconColor = comp.IconColor;
DirtyField(ent, ent.Comp, nameof(ActionComponent.OriginalIconColor));
}
private void OnActionShutdown(Entity<ActionComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.AttachedEntity is {} user && !TerminatingOrDeleted(user))
RemoveAction(user, (ent, ent));
}
private void OnShutdown(Entity<ActionsComponent> ent, ref ComponentShutdown args)
{
foreach (var actionId in ent.Comp.Actions)
{
RemoveAction((ent, ent), actionId);
}
}
private void OnGetState(Entity<ActionsComponent> ent, ref ComponentGetState args)
{
args.State = new ActionsComponentState(GetNetEntitySet(ent.Comp.Actions));
}
/// <summary>
/// Resolving an action's <see cref="ActionComponent"/>, only returning a value if it exists and has it.
/// </summary>
public Entity<ActionComponent>? GetAction(Entity<ActionComponent?>? action, bool logError = true)
{
if (action is not {} ent || Deleted(ent))
return null;
if (!_actionQuery.Resolve(ent, ref ent.Comp, logError))
return null;
return (ent, ent.Comp);
}
public void SetCooldown(Entity<ActionComponent?>? action, TimeSpan start, TimeSpan end)
{
if (GetAction(action) is not {} ent)
return;
ent.Comp.Cooldown = new ActionCooldown
{
Start = start,
End = end
};
DirtyField(ent, ent.Comp, nameof(ActionComponent.Cooldown));
}
public void RemoveCooldown(Entity<ActionComponent?>? action)
{
if (GetAction(action) is not {} ent)
return;
ent.Comp.Cooldown = null;
DirtyField(ent, ent.Comp, nameof(ActionComponent.Cooldown));
}
/// <summary>
/// Starts a cooldown starting now, lasting for <c>cooldown</c> seconds.
/// </summary>
public void SetCooldown(Entity<ActionComponent?>? action, TimeSpan cooldown)
{
var start = GameTiming.CurTime;
SetCooldown(action, start, start + cooldown);
}
public void ClearCooldown(Entity<ActionComponent?>? action)
{
if (GetAction(action) is not {} ent)
return;
if (ent.Comp.Cooldown is not {} cooldown)
return;
ent.Comp.Cooldown = new ActionCooldown
{
Start = cooldown.Start,
End = GameTiming.CurTime
};
DirtyField(ent, ent.Comp, nameof(ActionComponent.Cooldown));
}
/// <summary>
/// Sets the cooldown for this action only if it is bigger than the one it already has.
/// </summary>
public void SetIfBiggerCooldown(Entity<ActionComponent?>? action, TimeSpan cooldown)
{
if (GetAction(action) is not {} ent || cooldown < TimeSpan.Zero)
return;
var start = GameTiming.CurTime;
var end = start + cooldown;
if (ent.Comp.Cooldown?.End > end)
return;
SetCooldown((ent, ent), start, end);
}
/// <summary>
/// Set an action's cooldown to its use delay, if it has one.
/// If there is no set use delay this does nothing.
/// </summary>
public void StartUseDelay(Entity<ActionComponent?>? action)
{
if (GetAction(action) is not {} ent || ent.Comp.UseDelay is not {} delay)
return;
SetCooldown((ent, ent), delay);
}
public void SetUseDelay(Entity<ActionComponent?>? action, TimeSpan? delay)
{
if (GetAction(action) is not {} ent || ent.Comp.UseDelay == delay)
return;
ent.Comp.UseDelay = delay;
UpdateAction(ent);
DirtyField(ent, ent.Comp, nameof(ActionComponent.UseDelay));
}
public void ReduceUseDelay(Entity<ActionComponent?>? action, TimeSpan? lowerDelay)
{
if (GetAction(action) is not {} ent)
return;
if (ent.Comp.UseDelay != null && lowerDelay != null)
ent.Comp.UseDelay -= lowerDelay;
if (ent.Comp.UseDelay < TimeSpan.Zero)
ent.Comp.UseDelay = null;
UpdateAction(ent);
DirtyField(ent, ent.Comp, nameof(ActionComponent.UseDelay));
}
private void OnRejuventate(Entity<ActionsComponent> ent, ref RejuvenateEvent args)
{
foreach (var act in ent.Comp.Actions)
{
ClearCooldown(act);
}
}
#region ComponentStateManagement
public virtual void UpdateAction(Entity<ActionComponent> ent)
{
// See client-side code.
}
public void SetToggled(Entity<ActionComponent?>? action, bool toggled)
{
if (GetAction(action) is not {} ent || ent.Comp.Toggled == toggled)
return;
ent.Comp.Toggled = toggled;
UpdateAction(ent);
DirtyField(ent, ent.Comp, nameof(ActionComponent.Toggled));
}
public void SetEnabled(Entity<ActionComponent?>? action, bool enabled)
{
if (GetAction(action) is not {} ent || ent.Comp.Enabled == enabled)
return;
ent.Comp.Enabled = enabled;
UpdateAction(ent);
DirtyField(ent, ent.Comp, nameof(ActionComponent.Enabled));
}
#endregion
#region Execution
/// <summary>
/// When receiving a request to perform an action, this validates whether the action is allowed. If it is, it
/// will raise the relevant <see cref="InstantActionEvent"/>
/// </summary>
private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not { } user)
return;
if (!_actionsQuery.TryComp(user, out var component))
return;
var actionEnt = GetEntity(ev.Action);
if (!TryComp(actionEnt, out MetaDataComponent? metaData))
return;
var name = Name(actionEnt, metaData);
// Does the user actually have the requested action?
if (!component.Actions.Contains(actionEnt))
{
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} attempted to perform an action that they do not have: {name}.");
return;
}
if (GetAction(actionEnt) is not {} action)
return;
DebugTools.Assert(action.Comp.AttachedEntity == user);
if (!action.Comp.Enabled)
return;
var curTime = GameTiming.CurTime;
if (IsCooldownActive(action, curTime))
return;
// check for action use prevention
// TODO: make code below use this event with a dedicated component
var attemptEv = new ActionAttemptEvent(user);
RaiseLocalEvent(action, ref attemptEv);
if (attemptEv.Cancelled)
return;
// Validate request by checking action blockers and the like
var provider = action.Comp.Container ?? user;
var validateEv = new ActionValidateEvent()
{
Input = ev,
User = user,
Provider = provider
};
RaiseLocalEvent(action, ref validateEv);
if (validateEv.Invalid)
return;
// All checks passed. Perform the action!
PerformAction((user, component), action);
}
private void OnValidate(Entity<ActionComponent> ent, ref ActionValidateEvent args)
{
if ((ent.Comp.CheckConsciousness && !_actionBlocker.CanConsciouslyPerformAction(args.User))
|| (ent.Comp.CheckCanInteract && !_actionBlocker.CanInteract(args.User, null)))
args.Invalid = true;
}
private void OnInstantValidate(Entity<InstantActionComponent> ent, ref ActionValidateEvent args)
{
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(args.User):user} is performing the {Name(ent):action} action provided by {ToPrettyString(args.Provider):provider}.");
}
private void OnEntityValidate(Entity<EntityTargetActionComponent> ent, ref ActionValidateEvent args)
{
// let WorldTargetAction handle it
if (ent.Comp.Event is not {} ev)
{
DebugTools.Assert(HasComp<WorldTargetActionComponent>(ent), $"Entity-world targeting action {ToPrettyString(ent)} requires WorldTargetActionComponent");
return;
}
if (args.Input.EntityTarget is not {} netTarget)
{
args.Invalid = true;
return;
}
var user = args.User;
var target = GetEntity(netTarget);
var targetWorldPos = _transform.GetWorldPosition(target);
if (ent.Comp.RotateOnUse)
_rotateToFace.TryFaceCoordinates(user, targetWorldPos);
if (!ValidateEntityTarget(user, target, ent))
return;
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} is performing the {Name(ent):action} action (provided by {ToPrettyString(args.Provider):provider}) targeted at {ToPrettyString(target):target}.");
ev.Target = target;
}
private void OnWorldValidate(Entity<WorldTargetActionComponent> ent, ref ActionValidateEvent args)
{
if (args.Input.EntityCoordinatesTarget is not { } netTarget)
{
args.Invalid = true;
return;
}
var user = args.User;
var target = GetCoordinates(netTarget);
if (ent.Comp.RotateOnUse)
_rotateToFace.TryFaceCoordinates(user, _transform.ToMapCoordinates(target).Position);
if (!ValidateWorldTarget(user, target, ent))
return;
// if the client specified an entity it needs to be valid
var targetEntity = GetEntity(args.Input.EntityTarget);
if (targetEntity != null && (
!TryComp<EntityTargetActionComponent>(ent, out var entTarget) ||
!ValidateEntityTarget(user, targetEntity.Value, (ent, entTarget))))
{
args.Invalid = true;
return;
}
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} is performing the {Name(ent):action} action (provided by {args.Provider}) targeting {targetEntity} at {target:target}.");
if (ent.Comp.Event is {} ev)
{
ev.Target = target;
ev.Entity = targetEntity;
}
}
public bool ValidateEntityTarget(EntityUid user, EntityUid target, Entity<EntityTargetActionComponent> ent)
{
var (uid, comp) = ent;
if (!target.IsValid() || Deleted(target))
return false;
if (_whitelist.IsWhitelistFail(comp.Whitelist, target))
return false;
if (_whitelist.IsBlacklistPass(comp.Blacklist, target))
return false;
if (_actionQuery.Comp(uid).CheckCanInteract && !_actionBlocker.CanInteract(user, target))
return false;
if (user == target)
return comp.CanTargetSelf;
// Sunrise-start
if (HasComp<AbductorComponent>(target))
return true;
// Sunrise-end
var targetAction = Comp<TargetActionComponent>(uid);
// not using the ValidateBaseTarget logic since its raycast fails if the target is e.g. a wall
if (targetAction.CheckCanAccess)
return _interaction.InRangeAndAccessible(user, target, targetAction.Range, targetAction.AccessMask);
// Just check normal in range, allowing <= 0 range to mean infinite range.
if (targetAction.Range > 0
&& !_transform.InRange(user, target, targetAction.Range))
return false;
// Sunrise-Start
if (targetAction.IgnoreContainer)
return true;
// Sunrise-End
// If checkCanAccess isn't set, we allow targeting things in containers
return _interaction.IsAccessible(user, target);
}
public bool ValidateWorldTarget(EntityUid user, EntityCoordinates target, Entity<WorldTargetActionComponent> ent)
{
var targetAction = Comp<TargetActionComponent>(ent);
return ValidateBaseTarget(user, target, (ent, targetAction));
}
private bool ValidateBaseTarget(EntityUid user, EntityCoordinates coords, Entity<TargetActionComponent> ent)
{
var comp = ent.Comp;
if (comp.CheckCanAccess)
return _interaction.InRangeUnobstructed(user, coords, range: comp.Range);
// Sunrise-start
if (HasComp<AbductorAgentComponent>(user) || HasComp<AbductorScientistComponent>(user))
return true;
// Sunrise-end
// even if we don't check for obstructions, we may still need to check the range.
var xform = Transform(user);
if (xform.MapID != _transform.GetMapId(coords))
return false;
if (comp.Range <= 0)
return true;
return _transform.InRange(coords, xform.Coordinates, comp.Range);
}
private void OnInstantGetEvent(Entity<InstantActionComponent> ent, ref ActionGetEventEvent args)
{
if (ent.Comp.Event is {} ev)
args.Event = ev;
}
private void OnEntityGetEvent(Entity<EntityTargetActionComponent> ent, ref ActionGetEventEvent args)
{
if (ent.Comp.Event is {} ev)
args.Event = ev;
}
private void OnWorldGetEvent(Entity<WorldTargetActionComponent> ent, ref ActionGetEventEvent args)
{
if (ent.Comp.Event is {} ev)
args.Event = ev;
}
private void OnInstantSetEvent(Entity<InstantActionComponent> ent, ref ActionSetEventEvent args)
{
if (args.Event is InstantActionEvent ev)
{
ent.Comp.Event = ev;
args.Handled = true;
}
}
private void OnEntitySetEvent(Entity<EntityTargetActionComponent> ent, ref ActionSetEventEvent args)
{
if (args.Event is EntityTargetActionEvent ev)
{
ent.Comp.Event = ev;
args.Handled = true;
}
}
private void OnWorldSetEvent(Entity<WorldTargetActionComponent> ent, ref ActionSetEventEvent args)
{
if (args.Event is WorldTargetActionEvent ev)
{
ent.Comp.Event = ev;
args.Handled = true;
}
}
private void OnEntitySetTarget(Entity<EntityTargetActionComponent> ent, ref ActionSetTargetEvent args)
{
if (ent.Comp.Event is {} ev)
{
ev.Target = args.Target;
args.Handled = true;
}
}
private void OnWorldSetTarget(Entity<WorldTargetActionComponent> ent, ref ActionSetTargetEvent args)
{
if (ent.Comp.Event is {} ev)
{
ev.Target = Transform(args.Target).Coordinates;
// only set Entity if the action also has EntityTargetAction
ev.Entity = HasComp<EntityTargetActionComponent>(ent) ? args.Target : null;
args.Handled = true;
}
}
/// <summary>
/// Perform an action, bypassing validation checks.
/// </summary>
/// <param name="performer">The entity performing the action</param>
/// <param name="action">The action being performed</param>
/// <param name="actionEvent">An event override to perform. If null, uses <see cref="GetEvent"/></param>
/// <param name="predicted">If false, prevents playing the action's sound on the client</param>
public void PerformAction(Entity<ActionsComponent?> performer, Entity<ActionComponent> action, BaseActionEvent? actionEvent = null, bool predicted = true)
{
var handled = false;
var toggledBefore = action.Comp.Toggled;
// Note that attached entity and attached container are allowed to be null here.
if (action.Comp.AttachedEntity != null && action.Comp.AttachedEntity != performer)
{
Log.Error($"{ToPrettyString(performer)} is attempting to perform an action {ToPrettyString(action)} that is attached to another entity {ToPrettyString(action.Comp.AttachedEntity)}");
return;
}
actionEvent ??= GetEvent(action);
if (actionEvent is not {} ev)
return;
ev.Performer = performer;
// This here is required because of client-side prediction (RaisePredictiveEvent results in event re-use).
ev.Handled = false;
var target = performer.Owner;
ev.Performer = performer;
ev.Action = action;
if (!action.Comp.RaiseOnUser && action.Comp.Container is {} container && !_mindQuery.HasComp(container))
target = container;
if (action.Comp.RaiseOnAction)
target = action;
RaiseLocalEvent(target, (object) ev, broadcast: true);
handled = ev.Handled;
if (!handled)
return; // no interaction occurred.
// play sound, reduce charges, start cooldown
if (ev?.Toggle == true)
SetToggled((action, action), !action.Comp.Toggled);
_audio.PlayPredicted(action.Comp.Sound, performer, predicted ? performer : null);
// TODO: move to ActionCooldown ActionPerformedEvent?
RemoveCooldown((action, action));
StartUseDelay((action, action));
UpdateAction(action);
var performed = new ActionPerformedEvent(performer);
RaiseLocalEvent(action, ref performed);
}
#endregion
#region AddRemoveActions
public EntityUid? AddAction(EntityUid performer,
[ForbidLiteral] string? actionPrototypeId,
EntityUid container = default,
ActionsComponent? component = null)
{
EntityUid? actionId = null;
AddAction(performer, ref actionId, out _, actionPrototypeId, container, component);
return actionId;
}
/// <summary>
/// Adds an action to an action holder. If the given entity does not exist, it will attempt to spawn one.
/// If the holder has no actions component, this will give them one.
/// </summary>
/// <param name="performer">Entity to receive the actions</param>
/// <param name="actionId">Action entity to add</param>
/// <param name="component">The <see cref="performer"/>'s action component of </param>
/// <param name="actionPrototypeId">The action entity prototype id to use if <see cref="actionId"/> is invalid.</param>
/// <param name="container">The entity that contains/enables this action (e.g., flashlight).</param>
public bool AddAction(EntityUid performer,
[NotNullWhen(true)] ref EntityUid? actionId,
[ForbidLiteral] string? actionPrototypeId,
EntityUid container = default,
ActionsComponent? component = null)
{
return AddAction(performer, ref actionId, out _, actionPrototypeId, container, component);
}
/// <inheritdoc cref="AddAction(Robust.Shared.GameObjects.EntityUid,ref System.Nullable{Robust.Shared.GameObjects.EntityUid},string?,Robust.Shared.GameObjects.EntityUid,ActionsComponent?)"/>
public bool AddAction(EntityUid performer,
[NotNullWhen(true)] ref EntityUid? actionId,
[NotNullWhen(true)] out ActionComponent? action,
[ForbidLiteral] string? actionPrototypeId,
EntityUid container = default,
ActionsComponent? component = null)
{
if (!container.IsValid())
container = performer;
if (!_actionContainer.EnsureAction(container, ref actionId, out action, actionPrototypeId))
return false;
return AddActionDirect((performer, component), (actionId.Value, action));
}
/// <summary>
/// Adds a pre-existing action.
/// </summary>
public bool AddAction(Entity<ActionsComponent?> performer,
Entity<ActionComponent?> action,
Entity<ActionsContainerComponent?> container)
{
if (GetAction(action) is not {} ent)
return false;
if (ent.Comp.Container != container.Owner
|| !Resolve(container, ref container.Comp)
|| !container.Comp.Container.Contains(ent))
{
Log.Error($"Attempted to add an action with an invalid container: {ToPrettyString(ent)}");
return false;
}
return AddActionDirect(performer, (ent, ent));
}
/// <summary>
/// Adds a pre-existing action. This also bypasses the requirement that the given action must be stored in a
/// valid action container.
/// </summary>
public bool AddActionDirect(Entity<ActionsComponent?> performer,
Entity<ActionComponent?>? action)
{
if (GetAction(action) is not {} ent)
return false;
DebugTools.Assert(ent.Comp.Container == null ||
(TryComp(ent.Comp.Container, out ActionsContainerComponent? containerComp)
&& containerComp.Container.Contains(ent)));
if (ent.Comp.AttachedEntity is {} user)
RemoveAction(user, (ent, ent));
// TODO: make this an event bruh
if (ent.Comp.StartDelay && ent.Comp.UseDelay != null)
SetCooldown((ent, ent), ent.Comp.UseDelay.Value);
DebugTools.AssertOwner(performer, performer.Comp);
performer.Comp ??= EnsureComp<ActionsComponent>(performer);
ent.Comp.AttachedEntity = performer;
DirtyField(ent, ent.Comp, nameof(ActionComponent.AttachedEntity));
performer.Comp.Actions.Add(ent);
Dirty(performer, performer.Comp);
ActionAdded((performer, performer.Comp), (ent, ent.Comp));
return true;
}
/// <summary>
/// This method gets called after a new action got added.
/// </summary>
protected virtual void ActionAdded(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
{
// See client-side system for UI code.
}
/// <summary>
/// Grant pre-existing actions. If the entity has no action component, this will give them one.
/// </summary>
/// <param name="performer">Entity to receive the actions</param>
/// <param name="actions">The actions to add</param>
/// <param name="container">The entity that enables these actions (e.g., flashlight). May be null (innate actions).</param>
public void GrantActions(Entity<ActionsComponent?> performer,
IEnumerable<EntityUid> actions,
Entity<ActionsContainerComponent?> container)
{
if (!Resolve(container, ref container.Comp))
return;
DebugTools.AssertOwner(performer, performer.Comp);
performer.Comp ??= EnsureComp<ActionsComponent>(performer);
foreach (var actionId in actions)
{
AddAction(performer, actionId, container);
}
}
/// <summary>
/// Grants all actions currently contained in some action-container. If the target entity has no action
/// component, this will give them one.
/// </summary>
/// <param name="performer">Entity to receive the actions</param>
/// <param name="container">The entity that contains thee actions.</param>
public void GrantContainedActions(Entity<ActionsComponent?> performer, Entity<ActionsContainerComponent?> container)
{
if (!Resolve(container, ref container.Comp))
return;
performer.Comp ??= EnsureComp<ActionsComponent>(performer);
foreach (var actionId in container.Comp.Container.ContainedEntities)
{
if (GetAction(actionId) is {} action)
AddActionDirect(performer, (action, action));
}
}
/// <summary>
/// Grants the provided action from the container to the target entity. If the target entity has no action
/// component, this will give them one.
/// </summary>
/// <param name="performer"></param>
/// <param name="container"></param>
/// <param name="actionId"></param>
public void GrantContainedAction(Entity<ActionsComponent?> performer, Entity<ActionsContainerComponent?> container, EntityUid actionId)
{
if (!Resolve(container, ref container.Comp))
return;
performer.Comp ??= EnsureComp<ActionsComponent>(performer);
AddActionDirect(performer, actionId);
}
public IEnumerable<Entity<ActionComponent>> GetActions(EntityUid holderId, ActionsComponent? actions = null)
{
if (!Resolve(holderId, ref actions, false))
yield break;
foreach (var actionId in actions.Actions)
{
if (GetAction(actionId) is not {} ent)
continue;
yield return ent;
}
}
/// <summary>
/// Remove any actions that were enabled by some other entity. Useful when unequiping items that grant actions.
/// </summary>
public void RemoveProvidedActions(EntityUid performer, EntityUid container, ActionsComponent? comp = null)
{
if (!Resolve(performer, ref comp, false))
return;
foreach (var actionId in comp.Actions.ToArray())
{
if (GetAction(actionId) is not {} ent)
return;
if (ent.Comp.Container == container)
RemoveAction((performer, comp), (ent, ent));
}
}
/// <summary>
/// Removes a single provided action provided by another entity.
/// </summary>
public void RemoveProvidedAction(EntityUid performer, EntityUid container, EntityUid actionId, ActionsComponent? comp = null)
{
if (!_actionsQuery.Resolve(performer, ref comp, false) || GetAction(actionId) is not {} ent)
return;
if (ent.Comp.Container == container)
RemoveAction((performer, comp), (ent, ent));
}
/// <summary>
/// Removes an action from its container, if it still exists.
/// </summary>
public void RemoveAction(Entity<ActionComponent?>? action)
{
if (GetAction(action) is not {} ent || ent.Comp.AttachedEntity is not {} actions)
return;
if (!_actionsQuery.TryComp(actions, out var comp))
return;
RemoveAction((actions, comp), (ent, ent));
}
public void RemoveAction(Entity<ActionsComponent?> performer, Entity<ActionComponent?>? action)
{
if (GetAction(action) is not {} ent)
return;
if (ent.Comp.AttachedEntity != performer.Owner)
{
DebugTools.Assert(!Resolve(performer, ref performer.Comp, false)
|| performer.Comp.LifeStage >= ComponentLifeStage.Stopping
|| !performer.Comp.Actions.Contains(ent.Owner));
if (!GameTiming.ApplyingState)
Log.Error($"Attempted to remove an action {ToPrettyString(ent)} from an entity that it was never attached to: {ToPrettyString(performer)}. Trace: {Environment.StackTrace}");
return;
}
if (!_actionsQuery.Resolve(performer, ref performer.Comp, false))
{
DebugTools.Assert(performer == null || TerminatingOrDeleted(performer));
ent.Comp.AttachedEntity = null;
// TODO: should this delete the action since it's now orphaned?
return;
}
performer.Comp.Actions.Remove(ent.Owner);
Dirty(performer, performer.Comp);
ent.Comp.AttachedEntity = null;
DirtyField(ent, ent.Comp, nameof(ActionComponent.AttachedEntity));
ActionRemoved((performer, performer.Comp), ent);
if (ent.Comp.Temporary)
QueueDel(ent);
}
// Starlight-Abductor-start
public EntityUid[] HideActions(EntityUid performer, ActionsComponent? comp = null)
{
if (!Resolve(performer, ref comp, false))
return [];
var actions = comp.Actions.ToArray();
comp.Actions.Clear();
Dirty(performer, comp);
return actions;
}
public void UnHideActions(EntityUid performer, EntityUid[] actions, ActionsComponent? comp = null)
{
if (!Resolve(performer, ref comp, false))
return;
foreach (var action in actions)
comp.Actions.Add(action);
Dirty(performer, comp);
}
// Starlight-Abductor-end
/// <summary>
/// This method gets called after an action got removed.
/// </summary>
protected virtual void ActionRemoved(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
{
// See client-side system for UI code.
}
public bool ValidAction(Entity<ActionComponent> ent, bool canReach = true)
{
var (uid, comp) = ent;
if (!comp.Enabled)
return false;
var curTime = GameTiming.CurTime;
if (comp.Cooldown.HasValue && comp.Cooldown.Value.End > curTime)
return false;
// TODO: use event for this
return canReach || Comp<TargetActionComponent>(ent)?.CheckCanAccess == false;
}
#endregion
private void OnRelayActionCompChange(Entity<ActionsComponent> ent, ref RelayedActionComponentChangeEvent args)
{
if (args.Handled)
return;
var ev = new AttemptRelayActionComponentChangeEvent();
RaiseLocalEvent(ent.Owner, ref ev);
var target = ev.Target ?? ent.Owner;
args.Handled = true;
args.Toggle = true;
if (!args.Action.Comp.Toggled)
{
EntityManager.AddComponents(target, args.Components);
}
else
{
EntityManager.RemoveComponents(target, args.Components);
}
}
private void OnActionCompChange(Entity<ActionsComponent> ent, ref ActionComponentChangeEvent args)
{
if (args.Handled)
return;
args.Handled = true;
args.Toggle = true;
var target = ent.Owner;
if (!args.Action.Comp.Toggled)
{
EntityManager.AddComponents(target, args.Components);
}
else
{
EntityManager.RemoveComponents(target, args.Components);
}
}
#region EquipHandlers
private void OnDidEquip(Entity<ActionsComponent> ent, ref DidEquipEvent args)
{
if (GameTiming.ApplyingState)
return;
var ev = new GetItemActionsEvent(_actionContainer, args.Equipee, args.Equipment, args.SlotFlags);
RaiseLocalEvent(args.Equipment, ev);
if (ev.Actions.Count == 0)
return;
GrantActions((ent, ent), ev.Actions, args.Equipment);
}
private void OnHandEquipped(Entity<ActionsComponent> ent, ref DidEquipHandEvent args)
{
if (GameTiming.ApplyingState)
return;
var ev = new GetItemActionsEvent(_actionContainer, args.User, args.Equipped);
RaiseLocalEvent(args.Equipped, ev);
if (ev.Actions.Count == 0)
return;
GrantActions((ent, ent), ev.Actions, args.Equipped);
}
private void OnDidUnequip(EntityUid uid, ActionsComponent component, DidUnequipEvent args)
{
if (GameTiming.ApplyingState)
return;
RemoveProvidedActions(uid, args.Equipment, component);
}
private void OnHandUnequipped(EntityUid uid, ActionsComponent component, DidUnequipHandEvent args)
{
if (GameTiming.ApplyingState)
return;
RemoveProvidedActions(uid, args.Unequipped, component);
}
#endregion
public void SetEntityIcon(Entity<ActionComponent?> ent, EntityUid? icon)
{
if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.EntityIcon == icon)
return;
ent.Comp.EntityIcon = icon;
DirtyField(ent, ent.Comp, nameof(ActionComponent.EntIcon));
}
public void SetIcon(Entity<ActionComponent?> ent, SpriteSpecifier? icon)
{
if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.Icon == icon)
return;
ent.Comp.Icon = icon;
DirtyField(ent, ent.Comp, nameof(ActionComponent.Icon));
}
public void SetIconOn(Entity<ActionComponent?> ent, SpriteSpecifier? iconOn)
{
if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.IconOn == iconOn)
return;
ent.Comp.IconOn = iconOn;
DirtyField(ent, ent.Comp, nameof(ActionComponent.IconOn));
}
public void SetItemIconStyle(Entity<ActionComponent?> ent, ItemActionIconStyle itemIconStyle)
{
if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.ItemIconStyle == itemIconStyle)
return;
ent.Comp.ItemIconStyle = itemIconStyle;
DirtyField(ent, ent.Comp, nameof(ActionComponent.ItemIconStyle));
}
public void SetPriority(Entity<ActionComponent?> ent, int priority)
{
if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.Priority == priority)
return;
ent.Comp.Priority = priority;
DirtyField(ent, ent.Comp, nameof(ActionComponent.Priority));
}
public void SetIconColor(Entity<ActionComponent?> ent, Color color)
{
if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.IconColor == color)
return;
ent.Comp.IconColor = color;
DirtyField(ent, ent.Comp, nameof(ActionComponent.IconColor));
}
public void SetCheckCanInteract(Entity<ActionComponent?> ent, bool value)
{
if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.CheckCanInteract == value)
return;
ent.Comp.CheckCanInteract = value;
DirtyField(ent, ent.Comp, nameof(ActionComponent.CheckCanInteract));
}
public void SetCheckCanAccess(Entity<TargetActionComponent?> ent, bool value)
{
if (!_targetActionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.CheckCanAccess == value)
return;
ent.Comp.CheckCanAccess = value;
DirtyField(ent, ent.Comp, nameof(TargetActionComponent.CheckCanAccess));
}
public void SetIgnoreContainer(Entity<TargetActionComponent?> ent, bool value)
{
if (!_targetActionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.IgnoreContainer == value)
return;
ent.Comp.IgnoreContainer = value;
DirtyField(ent, ent.Comp, nameof(TargetActionComponent.IgnoreContainer));
}
/// <summary>
/// Set the event of an action.
/// Since the event isn't required to be serializable this is not networked.
/// Only use this if it's predicted or for a clientside action.
/// </summary>
public void SetEvent(EntityUid uid, BaseActionEvent ev)
{
// now this is meta
var setEv = new ActionSetEventEvent(ev);
RaiseLocalEvent(uid, ref setEv);
if (!setEv.Handled)
Log.Error($"Tried to set event of {ToPrettyString(uid):action} but nothing handled it!");
}
public BaseActionEvent? GetEvent(EntityUid uid)
{
DebugTools.Assert(_actionQuery.HasComp(uid), $"Entity {ToPrettyString(uid)} is missing ActionComponent");
var ev = new ActionGetEventEvent();
RaiseLocalEvent(uid, ref ev);
return ev.Event;
}
public bool SetEventTarget(EntityUid uid, EntityUid target)
{
DebugTools.Assert(_actionQuery.HasComp(uid), $"Entity {ToPrettyString(uid)} is missing ActionComponent");
var ev = new ActionSetTargetEvent(target);
RaiseLocalEvent(uid, ref ev);
return ev.Handled;
}
/// <summary>
/// Checks if the action has a cooldown and if it's still active
/// </summary>
public bool IsCooldownActive(ActionComponent action, TimeSpan? curTime = null)
{
// TODO: Check for charge recovery timer
curTime ??= GameTiming.CurTime;
return action.Cooldown.HasValue && action.Cooldown.Value.End > curTime;
}
/// <summary>
/// Marks the action as temporary.
/// Temporary actions get deleted upon being removed from an entity.
/// </summary>
public void SetTemporary(Entity<ActionComponent?> ent, bool temporary)
{
if (!Resolve(ent.Owner, ref ent.Comp, false))
return;
ent.Comp.Temporary = temporary;
Dirty(ent);
}
}