]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
action refactor proper ecs edition (#27422)
authordeltanedas <39013340+deltanedas@users.noreply.github.com>
Wed, 28 May 2025 19:52:11 +0000 (19:52 +0000)
committerGitHub <noreply@github.com>
Wed, 28 May 2025 19:52:11 +0000 (15:52 -0400)
111 files changed:
Content.Client/Actions/ActionEvents.cs
Content.Client/Actions/ActionsSystem.cs
Content.Client/Charges/ChargesSystem.cs
Content.Client/Decals/DecalPlacementSystem.cs
Content.Client/Mapping/MappingSystem.cs
Content.Client/Store/Ui/StoreMenu.xaml.cs
Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
Content.IntegrationTests/Tests/Actions/ActionsAddedTest.cs
Content.Server/Actions/ActionOnInteractSystem.cs
Content.Server/Commands/ActionCommands.cs
Content.Server/NPC/Components/NPCUseActionOnTargetComponent.cs
Content.Server/NPC/Systems/NPCUseActionOnTargetSystem.cs
Content.Server/Polymorph/Systems/PolymorphSystem.cs
Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
Content.Server/Store/Systems/StoreSystem.Ui.cs
Content.Shared/Abilities/Goliath/GoliathSummonTentacleAction.cs
Content.Shared/Abilities/Goliath/GoliathTentacleSystem.cs
Content.Shared/Actions/ActionContainerSystem.cs
Content.Shared/Actions/ActionEvents.cs
Content.Shared/Actions/ActionUpgradeSystem.cs
Content.Shared/Actions/Components/ActionComponent.cs [moved from Content.Shared/Actions/BaseActionComponent.cs with 61% similarity]
Content.Shared/Actions/Components/ActionContainerComponent.cs [moved from Content.Shared/Actions/ActionContainerComponent.cs with 66% similarity]
Content.Shared/Actions/Components/ActionUpgradeComponent.cs [moved from Content.Shared/Actions/ActionUpgradeComponent.cs with 68% similarity]
Content.Shared/Actions/Components/ActionsComponent.cs [moved from Content.Shared/Actions/ActionsComponent.cs with 78% similarity]
Content.Shared/Actions/Components/ConfirmableActionComponent.cs [moved from Content.Shared/Actions/ConfirmableActionComponent.cs with 90% similarity]
Content.Shared/Actions/Components/EntityTargetActionComponent.cs [new file with mode: 0644]
Content.Shared/Actions/Components/InstantActionComponent.cs [new file with mode: 0644]
Content.Shared/Actions/Components/TargetActionComponent.cs [moved from Content.Shared/Actions/BaseTargetActionComponent.cs with 65% similarity]
Content.Shared/Actions/Components/WorldTargetActionComponent.cs [new file with mode: 0644]
Content.Shared/Actions/ConfirmableActionSystem.cs
Content.Shared/Actions/EntityTargetActionComponent.cs [deleted file]
Content.Shared/Actions/EntityWorldTargetActionComponent.cs [deleted file]
Content.Shared/Actions/Events/ActionAttemptEvent.cs
Content.Shared/Actions/Events/ActionGetEventEvent.cs [new file with mode: 0644]
Content.Shared/Actions/Events/ActionSetEventEvent.cs [new file with mode: 0644]
Content.Shared/Actions/Events/ActionSetTargetEvent.cs [new file with mode: 0644]
Content.Shared/Actions/Events/ActionValidateEvent.cs [new file with mode: 0644]
Content.Shared/Actions/Events/GetActionDataEvent.cs [deleted file]
Content.Shared/Actions/Events/ValidateActionEntityTargetEvent.cs [deleted file]
Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs [deleted file]
Content.Shared/Actions/Events/ValidateActionWorldTargetEvent.cs [deleted file]
Content.Shared/Actions/InstantActionComponent.cs [deleted file]
Content.Shared/Actions/SharedActionsSystem.cs
Content.Shared/Actions/WorldTargetActionComponent.cs [deleted file]
Content.Shared/Bed/SharedBedSystem.cs
Content.Shared/Bed/Sleep/SleepingSystem.cs
Content.Shared/Clothing/Components/ToggleClothingComponent.cs
Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs
Content.Shared/Eye/Blinding/Systems/EyeClosingSystem.cs
Content.Shared/ItemRecall/SharedItemRecallSystem.cs
Content.Shared/Magic/SpellbookSystem.cs
Content.Shared/Mapping/StartPlacementActionEvent.cs
Content.Shared/Mindshield/FakeMindShield/SharedFakeMindShieldImplantSystem.cs
Content.Shared/Mobs/Systems/MobStateActionsSystem.cs
Content.Shared/Ninja/Components/DashAbilityComponent.cs
Content.Shared/Ninja/Components/ItemCreatorComponent.cs
Content.Shared/PAI/SharedPAISystem.cs
Content.Shared/RatKing/SharedRatKingSystem.cs
Content.Shared/Silicons/Borgs/SharedBorgSwitchableTypeSystem.cs
Content.Shared/Xenoarchaeology/Artifact/Components/XenoArtifactComponent.cs
Resources/Prototypes/Actions/anomaly.yml
Resources/Prototypes/Actions/borgs.yml
Resources/Prototypes/Actions/crit.yml
Resources/Prototypes/Actions/diona.yml
Resources/Prototypes/Actions/internals.yml
Resources/Prototypes/Actions/mapping.yml [new file with mode: 0644]
Resources/Prototypes/Actions/mech.yml
Resources/Prototypes/Actions/ninja.yml
Resources/Prototypes/Actions/polymorph.yml
Resources/Prototypes/Actions/revenant.yml
Resources/Prototypes/Actions/security.yml
Resources/Prototypes/Actions/speech.yml
Resources/Prototypes/Actions/spider.yml
Resources/Prototypes/Actions/station_ai.yml
Resources/Prototypes/Actions/types.yml
Resources/Prototypes/Entities/Clothing/Head/helmets.yml
Resources/Prototypes/Entities/Clothing/Head/misc.yml
Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml
Resources/Prototypes/Entities/Clothing/Neck/misc.yml
Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml
Resources/Prototypes/Entities/Clothing/Shoes/misc.yml
Resources/Prototypes/Entities/Mobs/NPCs/asteroid.yml
Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml
Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml
Resources/Prototypes/Entities/Mobs/Player/dragon.yml
Resources/Prototypes/Entities/Mobs/Player/guardian.yml
Resources/Prototypes/Entities/Mobs/Player/observer.yml
Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml
Resources/Prototypes/Entities/Objects/Fun/pai.yml
Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml
Resources/Prototypes/Entities/Objects/Specific/Xenoarchaeology/xenoartifacts.yml
Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml
Resources/Prototypes/Magic/animate_spell.yml
Resources/Prototypes/Magic/event_spells.yml
Resources/Prototypes/Magic/forcewall_spells.yml
Resources/Prototypes/Magic/knock_spell.yml
Resources/Prototypes/Magic/mindswap_spell.yml
Resources/Prototypes/Magic/projectile_spells.yml
Resources/Prototypes/Magic/recall_spell.yml
Resources/Prototypes/Magic/repulse_spell.yml
Resources/Prototypes/Magic/rune_spells.yml
Resources/Prototypes/Magic/smoke_spell.yml
Resources/Prototypes/Magic/spawn_spells.yml
Resources/Prototypes/Magic/staves.yml
Resources/Prototypes/Magic/teleport_spells.yml
Resources/Prototypes/Magic/touch_spells.yml
Resources/Prototypes/Magic/utility_spells.yml
Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
Resources/mapping_actions.yml

index 2fdf25c97672b1d7b930948a348b6835408809e3..73bee331be6082998d62fc2e4b2325a9055e0c5a 100644 (file)
@@ -1,3 +1,6 @@
+using Content.Shared.Actions.Components;
+using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
+
 namespace Content.Client.Actions;
 
 /// <summary>
@@ -7,3 +10,17 @@ public sealed class FillActionSlotEvent : EntityEventArgs
 {
     public EntityUid? Action;
 }
+
+/// <summary>
+/// Client-side event used to attempt to trigger a targeted action.
+/// This only gets raised if the has <see cref="TargetActionComponent">.
+/// Handlers must set <c>Handled</c> to true, then if the action has been performed,
+/// i.e. a target is found, then FoundTarget must be set to true.
+/// </summary>
+[ByRefEvent]
+public record struct ActionTargetAttemptEvent(
+    PointerInputCmdArgs Input,
+    Entity<ActionsComponent> User,
+    ActionComponent Action,
+    bool Handled = false,
+    bool FoundTarget = false);
index 31350a6a5da8ffa67f9c6318ee6f72797ff9ae4a..91baa3b1a9fdd8bdf952b4ca913fcd0c40f9ca98 100644 (file)
@@ -1,18 +1,23 @@
 using System.IO;
 using System.Linq;
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Charges.Systems;
+using Content.Shared.Mapping;
+using Content.Shared.Maps;
 using JetBrains.Annotations;
 using Robust.Client.Player;
 using Robust.Shared.ContentPack;
 using Robust.Shared.GameStates;
 using Robust.Shared.Input.Binding;
 using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.Manager;
 using Robust.Shared.Serialization.Markdown;
 using Robust.Shared.Serialization.Markdown.Mapping;
 using Robust.Shared.Serialization.Markdown.Sequence;
 using Robust.Shared.Serialization.Markdown.Value;
+using Robust.Shared.Timing;
 using Robust.Shared.Utility;
 using YamlDotNet.RepresentationModel;
 
@@ -25,6 +30,7 @@ namespace Content.Client.Actions
 
         [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
         [Dependency] private readonly IPlayerManager _playerManager = default!;
+        [Dependency] private readonly IPrototypeManager _proto = default!;
         [Dependency] private readonly IResourceManager _resources = default!;
         [Dependency] private readonly ISerializationManager _serialization = default!;
         [Dependency] private readonly MetaDataSystem _metaData = default!;
@@ -38,131 +44,67 @@ namespace Content.Client.Actions
         public event Action<List<SlotAssignment>>? AssignSlot;
 
         private readonly List<EntityUid> _removed = new();
-        private readonly List<(EntityUid, BaseActionComponent?)> _added = new();
+        private readonly List<Entity<ActionComponent>> _added = new();
+
+        public static readonly EntProtoId MappingEntityAction = "BaseMappingEntityAction";
 
         public override void Initialize()
         {
             base.Initialize();
+
             SubscribeLocalEvent<ActionsComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
             SubscribeLocalEvent<ActionsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
-            SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleComponentState);
-
-            SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState);
-            SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState);
-            SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState);
-            SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
-        }
-
-        private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
-        {
-            if (args.Current is not InstantActionComponentState state)
-                return;
-
-            BaseHandleState<InstantActionComponent>(uid, component, state);
-        }
-
-        private void OnEntityTargetHandleState(EntityUid uid, EntityTargetActionComponent component, ref ComponentHandleState args)
-        {
-            if (args.Current is not EntityTargetActionComponentState state)
-                return;
+            SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(OnHandleState);
 
-            component.Whitelist = state.Whitelist;
-            component.Blacklist = state.Blacklist;
-            component.CanTargetSelf = state.CanTargetSelf;
-            BaseHandleState<EntityTargetActionComponent>(uid, component, state);
-        }
-
-        private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent component, ref ComponentHandleState args)
-        {
-            if (args.Current is not WorldTargetActionComponentState state)
-                return;
+            SubscribeLocalEvent<ActionComponent, AfterAutoHandleStateEvent>(OnActionAutoHandleState);
 
-            BaseHandleState<WorldTargetActionComponent>(uid, component, state);
+            SubscribeLocalEvent<EntityTargetActionComponent, ActionTargetAttemptEvent>(OnEntityTargetAttempt);
+            SubscribeLocalEvent<WorldTargetActionComponent, ActionTargetAttemptEvent>(OnWorldTargetAttempt);
         }
 
-        private void OnEntityWorldTargetHandleState(EntityUid uid,
-            EntityWorldTargetActionComponent component,
-            ref ComponentHandleState args)
-        {
-            if (args.Current is not EntityWorldTargetActionComponentState state)
-                return;
-
-            component.Whitelist = state.Whitelist;
-            component.CanTargetSelf = state.CanTargetSelf;
-            BaseHandleState<EntityWorldTargetActionComponent>(uid, component, state);
-        }
 
-        private void BaseHandleState<T>(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent
+        private void OnActionAutoHandleState(Entity<ActionComponent> ent, ref AfterAutoHandleStateEvent args)
         {
-            // TODO ACTIONS use auto comp states
-            component.Icon = state.Icon;
-            component.IconOn = state.IconOn;
-            component.IconColor = state.IconColor;
-            component.OriginalIconColor = state.OriginalIconColor;
-            component.DisabledIconColor = state.DisabledIconColor;
-            component.Keywords.Clear();
-            component.Keywords.UnionWith(state.Keywords);
-            component.Enabled = state.Enabled;
-            component.Toggled = state.Toggled;
-            component.Cooldown = state.Cooldown;
-            component.UseDelay = state.UseDelay;
-            component.Container = EnsureEntity<T>(state.Container, uid);
-            component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid);
-            component.CheckCanInteract = state.CheckCanInteract;
-            component.CheckConsciousness = state.CheckConsciousness;
-            component.ClientExclusive = state.ClientExclusive;
-            component.Priority = state.Priority;
-            component.AttachedEntity = EnsureEntity<T>(state.AttachedEntity, uid);
-            component.RaiseOnUser = state.RaiseOnUser;
-            component.RaiseOnAction = state.RaiseOnAction;
-            component.AutoPopulate = state.AutoPopulate;
-            component.Temporary = state.Temporary;
-            component.ItemIconStyle = state.ItemIconStyle;
-            component.Sound = state.Sound;
-
-            UpdateAction(uid, component);
+            UpdateAction(ent);
         }
 
-        public override void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null)
+        public override void UpdateAction(Entity<ActionComponent> ent)
         {
-            if (!ResolveActionData(actionId, ref action))
-                return;
-
             // TODO: Decouple this.
-            action.IconColor = _sharedCharges.GetCurrentCharges(actionId.Value) == 0 ? action.DisabledIconColor : action.OriginalIconColor;
-
-            base.UpdateAction(actionId, action);
-            if (_playerManager.LocalEntity != action.AttachedEntity)
+            ent.Comp.IconColor = _sharedCharges.GetCurrentCharges(ent.Owner) == 0 ? ent.Comp.DisabledIconColor : ent.Comp.OriginalIconColor;
+            base.UpdateAction(ent);
+            if (_playerManager.LocalEntity != ent.Comp.AttachedEntity)
                 return;
 
             ActionsUpdated?.Invoke();
         }
 
-        private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
+        private void OnHandleState(Entity<ActionsComponent> ent, ref ComponentHandleState args)
         {
             if (args.Current is not ActionsComponentState state)
                 return;
 
+            var (uid, comp) = ent;
             _added.Clear();
             _removed.Clear();
             var stateEnts = EnsureEntitySet<ActionsComponent>(state.Actions, uid);
-            foreach (var act in component.Actions)
+            foreach (var act in comp.Actions)
             {
                 if (!stateEnts.Contains(act) && !IsClientSide(act))
                     _removed.Add(act);
             }
-            component.Actions.ExceptWith(_removed);
+            comp.Actions.ExceptWith(_removed);
 
             foreach (var actionId in stateEnts)
             {
                 if (!actionId.IsValid())
                     continue;
 
-                if (!component.Actions.Add(actionId))
+                if (!comp.Actions.Add(actionId))
                     continue;
 
-                TryGetActionData(actionId, out var action);
-                _added.Add((actionId, action));
+                if (GetAction(actionId) is {} action)
+                    _added.Add(action);
             }
 
             if (_playerManager.LocalEntity != uid)
@@ -177,47 +119,46 @@ namespace Content.Client.Actions
 
             foreach (var action in _added)
             {
-                OnActionAdded?.Invoke(action.Item1);
+                OnActionAdded?.Invoke(action);
             }
 
             ActionsUpdated?.Invoke();
         }
 
-        public static int ActionComparer((EntityUid, BaseActionComponent?) a, (EntityUid, BaseActionComponent?) b)
+        public static int ActionComparer(Entity<ActionComponent> a, Entity<ActionComponent> b)
         {
-            var priorityA = a.Item2?.Priority ?? 0;
-            var priorityB = b.Item2?.Priority ?? 0;
+            var priorityA = a.Comp?.Priority ?? 0;
+            var priorityB = b.Comp?.Priority ?? 0;
             if (priorityA != priorityB)
                 return priorityA - priorityB;
 
-            priorityA = a.Item2?.Container?.Id ?? 0;
-            priorityB = b.Item2?.Container?.Id ?? 0;
+            priorityA = a.Comp?.Container?.Id ?? 0;
+            priorityB = b.Comp?.Container?.Id ?? 0;
             return priorityA - priorityB;
         }
 
-        protected override void ActionAdded(EntityUid performer, EntityUid actionId, ActionsComponent comp,
-            BaseActionComponent action)
+        protected override void ActionAdded(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
         {
-            if (_playerManager.LocalEntity != performer)
+            if (_playerManager.LocalEntity != performer.Owner)
                 return;
 
-            OnActionAdded?.Invoke(actionId);
+            OnActionAdded?.Invoke(action);
             ActionsUpdated?.Invoke();
         }
 
-        protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
+        protected override void ActionRemoved(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
         {
-            if (_playerManager.LocalEntity != performer)
+            if (_playerManager.LocalEntity != performer.Owner)
                 return;
 
-            OnActionRemoved?.Invoke(actionId);
+            OnActionRemoved?.Invoke(action);
             ActionsUpdated?.Invoke();
         }
 
-        public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()
+        public IEnumerable<Entity<ActionComponent>> GetClientActions()
         {
             if (_playerManager.LocalEntity is not { } user)
-                return Enumerable.Empty<(EntityUid, BaseActionComponent)>();
+                return Enumerable.Empty<Entity<ActionComponent>>();
 
             return GetActions(user);
         }
@@ -254,24 +195,23 @@ namespace Content.Client.Actions
             CommandBinds.Unregister<ActionsSystem>();
         }
 
-        public void TriggerAction(EntityUid actionId, BaseActionComponent action)
+        public void TriggerAction(Entity<ActionComponent> action)
         {
-            if (_playerManager.LocalEntity is not { } user ||
-                !TryComp(user, out ActionsComponent? actions))
-            {
+            if (_playerManager.LocalEntity is not { } user)
                 return;
-            }
 
-            if (action is not InstantActionComponent instantAction)
+            // TODO: unhardcode this somehow
+
+            if (!HasComp<InstantActionComponent>(action))
                 return;
 
-            if (action.ClientExclusive)
+            if (action.Comp.ClientExclusive)
             {
-                PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime);
+                PerformAction(user, action);
             }
             else
             {
-                var request = new RequestPerformActionEvent(GetNetEntity(actionId));
+                var request = new RequestPerformActionEvent(GetNetEntity(action));
                 EntityManager.RaisePredictiveEvent(request);
             }
         }
@@ -295,39 +235,137 @@ namespace Content.Client.Actions
             if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
                 return;
 
+            var actions = EnsureComp<ActionsComponent>(user);
+
             ClearAssignments?.Invoke();
 
             var assignments = new List<SlotAssignment>();
-
             foreach (var entry in sequence.Sequence)
             {
                 if (entry is not MappingDataNode map)
                     continue;
 
-                if (!map.TryGet("action", out var actionNode))
+                if (!map.TryGet("assignments", out var assignmentNode))
+                    continue;
+
+                var actionId = EntityUid.Invalid;
+                if (map.TryGet<ValueDataNode>("action", out var actionNode))
+                {
+                    var id = new EntProtoId(actionNode.Value);
+                    actionId = Spawn(id);
+                }
+                else if (map.TryGet<ValueDataNode>("entity", out var entityNode))
+                {
+                    var id = new EntProtoId(entityNode.Value);
+                    var proto = _proto.Index(id);
+                    actionId = Spawn(MappingEntityAction);
+                    SetIcon(actionId, new SpriteSpecifier.EntityPrototype(id));
+                    SetEvent(actionId, new StartPlacementActionEvent()
+                    {
+                        PlacementOption = "SnapgridCenter",
+                        EntityType = id
+                    });
+                    _metaData.SetEntityName(actionId, proto.Name);
+                }
+                else if (map.TryGet<ValueDataNode>("tileId", out var tileNode))
+                {
+                    var id = new ProtoId<ContentTileDefinition>(tileNode.Value);
+                    var proto = _proto.Index(id);
+                    actionId = Spawn(MappingEntityAction);
+                    if (proto.Sprite is {} sprite)
+                        SetIcon(actionId, new SpriteSpecifier.Texture(sprite));
+                    SetEvent(actionId, new StartPlacementActionEvent()
+                    {
+                        PlacementOption = "AlignTileAny",
+                        TileId = id
+                    });
+                    _metaData.SetEntityName(actionId, Loc.GetString(proto.Name));
+                }
+                else
+                {
+                    Log.Error($"Mapping actions from {path} had unknown action data!");
                     continue;
+                }
+
+                AddActionDirect((user, actions), actionId);
+            }
+        }
 
-                var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
-                var actionId = Spawn();
-                AddComp(actionId, action);
-                AddActionDirect(user, actionId);
+        private void OnWorldTargetAttempt(Entity<WorldTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
+        {
+            if (args.Handled)
+                return;
 
-                if (map.TryGet<ValueDataNode>("name", out var nameNode))
-                    _metaData.SetEntityName(actionId, nameNode.Value);
+            args.Handled = true;
 
-                if (!map.TryGet("assignments", out var assignmentNode))
-                    continue;
+            var (uid, comp) = ent;
+            var action = args.Action;
+            var coords = args.Input.Coordinates;
+            var user = args.User;
 
-                var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode, notNullableOverride: true);
+            if (!ValidateWorldTarget(user, coords, ent))
+                return;
 
-                foreach (var index in nodeAssignments)
+            // optionally send the clicked entity too, if it matches its whitelist etc
+            // this is the actual entity-world targeting magic
+            EntityUid? targetEnt = null;
+            if (TryComp<EntityTargetActionComponent>(ent, out var entity) &&
+                args.Input.EntityUid != null &&
+                ValidateEntityTarget(user, args.Input.EntityUid, (uid, entity)))
+            {
+                targetEnt = args.Input.EntityUid;
+            }
+
+            if (action.ClientExclusive)
+            {
+                // TODO: abstract away from single event or maybe just RaiseLocalEvent?
+                if (comp.Event is {} ev)
                 {
-                    var assignment = new SlotAssignment(index.Hotbar, index.Slot, actionId);
-                    assignments.Add(assignment);
+                    ev.Target = coords;
+                    ev.Entity = targetEnt;
                 }
+
+                PerformAction((user, user.Comp), (uid, action));
+            }
+            else
+                RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(targetEnt), GetNetCoordinates(coords)));
+
+            args.FoundTarget = true;
+        }
+
+        private void OnEntityTargetAttempt(Entity<EntityTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
+        {
+            if (args.Handled || args.Input.EntityUid is not { Valid: true } entity)
+                return;
+
+            // let world target component handle it
+            var (uid, comp) = ent;
+            if (comp.Event is not {} ev)
+            {
+                DebugTools.Assert(HasComp<WorldTargetActionComponent>(ent), $"Action {ToPrettyString(ent)} requires WorldTargetActionComponent for entity-world targeting");
+                return;
+            }
+
+            args.Handled = true;
+
+            var action = args.Action;
+            var user = args.User;
+
+            if (!ValidateEntityTarget(user, entity, ent))
+                return;
+
+            if (action.ClientExclusive)
+            {
+                ev.Target = entity;
+
+                PerformAction((user, user.Comp), (uid, action));
+            }
+            else
+            {
+                RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(entity)));
             }
 
-            AssignSlot?.Invoke(assignments);
+            args.FoundTarget = true;
         }
 
         public record struct SlotAssignment(byte Hotbar, byte Slot, EntityUid ActionId);
index 2c7e0536cd089bb275970ca2e963aea30ab1d37b..890ff207ace506f45a5e59c6dce71e3c8f3d26a8 100644 (file)
@@ -25,16 +25,14 @@ public sealed class ChargesSystem : SharedChargesSystem
 
         while (query.MoveNext(out var uid, out var recharge, out var charges))
         {
-            BaseActionComponent? actionComp = null;
-
-            if (!_actions.ResolveActionData(uid, ref actionComp, logError: false))
+            if (_actions.GetAction(uid, false) is not {} action)
                 continue;
 
             var current = GetCurrentCharges((uid, charges, recharge));
 
             if (!_lastCharges.TryGetValue(uid, out var last) || current != last)
             {
-                _actions.UpdateAction(uid, actionComp);
+                _actions.UpdateAction(action);
             }
 
             _tempLastCharges[uid] = current;
index a4495042c6944a4f4c177f8364128dea7bae67f8..db00534a387472c74c291803e3732bebae2dad38 100644 (file)
@@ -2,6 +2,7 @@ using System.Numerics;
 using Content.Client.Actions;
 using Content.Client.Decals.Overlays;
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Decals;
 using Robust.Client.GameObjects;
 using Robust.Client.Graphics;
@@ -21,9 +22,12 @@ public sealed class DecalPlacementSystem : EntitySystem
     [Dependency] private readonly IPrototypeManager _protoMan = default!;
     [Dependency] private readonly InputSystem _inputSystem = default!;
     [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly SharedActionsSystem _actions = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly SpriteSystem _sprite = default!;
 
+    public static readonly EntProtoId DecalAction = "BaseMappingDecalAction";
+
     private string? _decalId;
     private Color _decalColor = Color.White;
     private Angle _decalAngle = Angle.Zero;
@@ -152,19 +156,12 @@ public sealed class DecalPlacementSystem : EntitySystem
             Cleanable = _cleanable,
         };
 
-        var actionId = Spawn(null);
-        AddComp(actionId, new WorldTargetActionComponent
-        {
-            // non-unique actions may be considered duplicates when saving/loading.
-            Icon = decalProto.Sprite,
-            Repeat = true,
-            ClientExclusive = true,
-            CheckCanAccess = false,
-            CheckCanInteract = false,
-            Range = -1,
-            Event = actionEvent,
-            IconColor = _decalColor,
-        });
+        var actionId = Spawn(DecalAction);
+        var action = Comp<ActionComponent>(actionId);
+        var ent = (actionId, action);
+        _actions.SetEvent(actionId, actionEvent);
+        _actions.SetIcon(ent, decalProto.Sprite);
+        _actions.SetIconColor(ent, _decalColor);
 
         _metaData.SetEntityName(actionId, $"{_decalId} ({_decalColor.ToHex()}, {(int) _decalAngle.Degrees})");
 
index 80189fbdfc1fbcb9a7ade502124c2e51edac27ea..627977a5268d179591b24e43470d787ba7ddb439 100644 (file)
@@ -4,8 +4,8 @@ using Content.Shared.Mapping;
 using Content.Shared.Maps;
 using Robust.Client.Placement;
 using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
-using static Robust.Shared.Utility.SpriteSpecifier;
 
 namespace Content.Client.Mapping;
 
@@ -14,16 +14,10 @@ public sealed partial class MappingSystem : EntitySystem
     [Dependency] private readonly IPlacementManager _placementMan = default!;
     [Dependency] private readonly ITileDefinitionManager _tileMan = default!;
     [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly SharedActionsSystem _actions = default!;
 
-    /// <summary>
-    ///     The icon to use for space tiles.
-    /// </summary>
-    private readonly SpriteSpecifier _spaceIcon = new Texture(new ("Tiles/cropped_parallax.png"));
-
-    /// <summary>
-    ///     The icon to use for entity-eraser.
-    /// </summary>
-    private readonly SpriteSpecifier _deleteIcon = new Texture(new ("Interface/VerbIcons/delete.svg.192dpi.png"));
+    public static readonly EntProtoId SpawnAction = "BaseMappingSpawnAction";
+    public static readonly EntProtoId EraserAction = "ActionMappingEraser";
 
     public override void Initialize()
     {
@@ -38,90 +32,46 @@ public sealed partial class MappingSystem : EntitySystem
     ///     some entity or tile into an action. This is somewhat janky, but it seem to work well enough. Though I'd
     ///     prefer if it were to function more like DecalPlacementSystem.
     /// </summary>
-    private void OnFillActionSlot(FillActionSlotEvent ev)
+    private void OnFillActionSlot(FillActionSlotEvent args)
     {
         if (!_placementMan.IsActive)
             return;
 
-        if (ev.Action != null)
+        if (args.Action != null)
             return;
 
-        var actionEvent = new StartPlacementActionEvent();
-        ITileDefinition? tileDef = null;
-
-        if (_placementMan.CurrentPermission != null)
+        if (_placementMan.CurrentPermission is {} permission)
         {
-            actionEvent.EntityType = _placementMan.CurrentPermission.EntityType;
-            actionEvent.PlacementOption = _placementMan.CurrentPermission.PlacementOption;
-
-            if (_placementMan.CurrentPermission.IsTile)
+            var ev = new StartPlacementActionEvent()
             {
-                tileDef = _tileMan[_placementMan.CurrentPermission.TileType];
-                actionEvent.TileId = tileDef.ID;
-            }
-        }
-        else if (_placementMan.Eraser)
-        {
-            actionEvent.Eraser = true;
-        }
-        else
-            return;
-
-        InstantActionComponent action;
-        string name;
-
-        if (tileDef != null)
-        {
-            if (tileDef is not ContentTileDefinition contentTileDef)
-                return;
-
-            var tileIcon = contentTileDef.MapAtmosphere
-                ? _spaceIcon
-                : new Texture(contentTileDef.Sprite!.Value);
+                EntityType = permission.EntityType,
+                PlacementOption = permission.PlacementOption,
+            };
 
-            action = new InstantActionComponent
+            var action = Spawn(SpawnAction);
+            if (_placementMan.CurrentPermission.IsTile)
             {
-                ClientExclusive = true,
-                CheckCanInteract = false,
-                Event = actionEvent,
-                Icon = tileIcon
-            };
+                if (_tileMan[_placementMan.CurrentPermission.TileType] is not ContentTileDefinition tileDef)
+                    return;
 
-            name = Loc.GetString(tileDef.Name);
-        }
-        else if (actionEvent.Eraser)
-        {
-            action = new InstantActionComponent
+                if (!tileDef.MapAtmosphere && tileDef.Sprite is {} sprite)
+                    _actions.SetIcon(action, new SpriteSpecifier.Texture(sprite));
+                ev.TileId = tileDef.ID;
+                _metaData.SetEntityName(action, Loc.GetString(tileDef.Name));
+            }
+            else if (permission.EntityType is {} id)
             {
-                ClientExclusive = true,
-                CheckCanInteract = false,
-                Event = actionEvent,
-                Icon = _deleteIcon,
-            };
+                _actions.SetIcon(action, new SpriteSpecifier.EntityPrototype(id));
+                _metaData.SetEntityName(action, id);
+            }
 
-            name = Loc.GetString("action-name-mapping-erase");
+            _actions.SetEvent(action, ev);
+            args.Action = action;
         }
-        else
+        else if (_placementMan.Eraser)
         {
-            if (string.IsNullOrWhiteSpace(actionEvent.EntityType))
-                return;
-
-            action = new InstantActionComponent
-            {
-                ClientExclusive = true,
-                CheckCanInteract = false,
-                Event = actionEvent,
-                Icon = new EntityPrototype(actionEvent.EntityType),
-            };
-
-            name = actionEvent.EntityType;
+            args.Action = Spawn(EraserAction);
         }
-
-        var actionId = Spawn(null);
-        AddComp<Component>(actionId, action);
-        _metaData.SetEntityName(actionId, name);
-
-        ev.Action = actionId;
     }
 
     private void OnStartPlacementAction(StartPlacementActionEvent args)
index 260bcfe4f260fc1be0dd80b55858d6c31ebae085..5b5d8739f75f3e92114d091c1fac63e529eaeb18 100644 (file)
@@ -141,11 +141,8 @@ public sealed partial class StoreMenu : DefaultWindow
         else if (listing.ProductAction != null)
         {
             var actionId = _entityManager.Spawn(listing.ProductAction);
-            if (_entityManager.System<ActionsSystem>().TryGetActionData(actionId, out var action) &&
-                action.Icon != null)
-            {
-                texture = spriteSys.Frame0(action.Icon);
-            }
+            if (_entityManager.System<ActionsSystem>().GetAction(actionId)?.Comp?.Icon is {} icon)
+                texture = spriteSys.Frame0(icon);
         }
 
         var listingInStock = GetListingPriceString(listing);
index f6fc2c997f71e51162ace48163c075781ac7efae..dc62dbe89717c2b1e010a15f51732bcdd5e5dc22 100644 (file)
@@ -12,6 +12,7 @@ using Content.Client.UserInterface.Systems.Actions.Widgets;
 using Content.Client.UserInterface.Systems.Actions.Windows;
 using Content.Client.UserInterface.Systems.Gameplay;
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Charges.Systems;
 using Content.Shared.Input;
 using Robust.Client.GameObjects;
@@ -162,142 +163,33 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         if (_playerManager.LocalEntity is not { } user)
             return false;
 
-        if (!EntityManager.TryGetComponent(user, out ActionsComponent? comp))
+        if (!EntityManager.TryGetComponent<ActionsComponent>(user, out var comp))
             return false;
 
-        if (!_actionsSystem.TryGetActionData(actionId, out var baseAction) ||
-            baseAction is not BaseTargetActionComponent action)
+        if (_actionsSystem.GetAction(actionId) is not {} action ||
+            !EntityManager.TryGetComponent<TargetActionComponent>(action, out var target))
         {
             return false;
         }
 
         // Is the action currently valid?
-        if (!action.Enabled
-            || action.Cooldown.HasValue && action.Cooldown.Value.End > _timing.CurTime)
+        if (!_actionsSystem.ValidAction(action))
         {
             // 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;
+            return !target.InteractOnMiss;
         }
 
-        switch (action)
+        var ev = new ActionTargetAttemptEvent(args, (user, comp), action);
+        EntityManager.EventBus.RaiseLocalEvent(action, ref ev);
+        if (!ev.Handled)
         {
-            case WorldTargetActionComponent mapTarget:
-                return TryTargetWorld(args, actionId, mapTarget, user, comp) || !mapTarget.InteractOnMiss;
-
-            case EntityTargetActionComponent entTarget:
-                return TryTargetEntity(args, actionId, entTarget, user, comp) || !entTarget.InteractOnMiss;
-
-            case EntityWorldTargetActionComponent entMapTarget:
-                return TryTargetEntityWorld(args, actionId, entMapTarget, user, comp) || !entMapTarget.InteractOnMiss;
-
-            default:
-                Log.Error($"Unknown targeting action: {actionId.GetType()}");
-                return false;
-        }
-    }
-
-    private bool TryTargetWorld(in PointerInputCmdArgs args, EntityUid actionId, WorldTargetActionComponent action, EntityUid user, ActionsComponent actionComp)
-    {
-        if (_actionsSystem == null)
-            return false;
-
-        var coords = args.Coordinates;
-
-        if (!_actionsSystem.ValidateWorldTarget(user, coords, (actionId, action)))
-        {
-            // Invalid target.
-            if (action.DeselectOnMiss)
-                StopTargeting();
-
-            return false;
-        }
-
-        if (action.ClientExclusive)
-        {
-            if (action.Event != null)
-            {
-                action.Event.Target = coords;
-            }
-
-            _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
-        }
-        else
-            EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetCoordinates(coords)));
-
-        if (!action.Repeat)
-            StopTargeting();
-
-        return true;
-    }
-
-    private bool TryTargetEntity(in PointerInputCmdArgs args, EntityUid actionId, EntityTargetActionComponent action, EntityUid user, ActionsComponent actionComp)
-    {
-        if (_actionsSystem == null)
-            return false;
-
-        var entity = args.EntityUid;
-
-        if (!_actionsSystem.ValidateEntityTarget(user, entity, (actionId, action)))
-        {
-            if (action.DeselectOnMiss)
-                StopTargeting();
-
-            return false;
-        }
-
-        if (action.ClientExclusive)
-        {
-            if (action.Event != null)
-            {
-                action.Event.Target = entity;
-            }
-
-            _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
-        }
-        else
-            EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetEntity(args.EntityUid)));
-
-        if (!action.Repeat)
-            StopTargeting();
-
-        return true;
-    }
-
-    private bool TryTargetEntityWorld(in PointerInputCmdArgs args,
-        EntityUid actionId,
-        EntityWorldTargetActionComponent action,
-        EntityUid user,
-        ActionsComponent actionComp)
-    {
-        if (_actionsSystem == null)
+            Log.Error($"Action {EntityManager.ToPrettyString(actionId)} did not handle ActionTargetAttemptEvent!");
             return false;
-
-        var entity = args.EntityUid;
-        var coords = args.Coordinates;
-
-        if (!_actionsSystem.ValidateEntityWorldTarget(user, entity, coords, (actionId, action)))
-        {
-            if (action.DeselectOnMiss)
-                StopTargeting();
-
-            return false;
-        }
-
-        if (action.ClientExclusive)
-        {
-            if (action.Event != null)
-            {
-                action.Event.Entity = entity;
-                action.Event.Coords = coords;
-            }
-
-            _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
         }
-        else
-            EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetEntity(args.EntityUid), EntityManager.GetNetCoordinates(coords)));
 
-        if (!action.Repeat)
+        // stop targeting when needed
+        if (ev.FoundTarget ? !target.Repeat : target.DeselectOnMiss)
             StopTargeting();
 
         return true;
@@ -305,36 +197,26 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
 
     public void UnloadButton()
     {
-        if (ActionButton == null)
-        {
-            return;
-        }
-
-        ActionButton.OnPressed -= ActionButtonPressed;
+        if (ActionButton != null)
+            ActionButton.OnPressed -= ActionButtonPressed;
     }
 
     public void LoadButton()
     {
-        if (ActionButton == null)
-        {
-            return;
-        }
-
-        ActionButton.OnPressed += ActionButtonPressed;
+        if (ActionButton != null)
+            ActionButton.OnPressed += ActionButtonPressed;
     }
 
     private void OnWindowOpened()
     {
-        if (ActionButton != null)
-            ActionButton.SetClickPressed(true);
+        ActionButton?.SetClickPressed(true);
 
         SearchAndDisplay();
     }
 
     private void OnWindowClosed()
     {
-        if (ActionButton != null)
-            ActionButton.SetClickPressed(false);
+        ActionButton?.SetClickPressed(false);
     }
 
     public void OnStateExited(GameplayState state)
@@ -351,35 +233,33 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
 
     private void TriggerAction(int index)
     {
-        if (_actionsSystem == null ||
-            !_actions.TryGetValue(index, out var actionId) ||
-            !_actionsSystem.TryGetActionData(actionId, out var baseAction))
+        if (!_actions.TryGetValue(index, out var actionId) ||
+            _actionsSystem?.GetAction(actionId) is not {} action)
         {
             return;
         }
 
-        if (baseAction is BaseTargetActionComponent action)
-            ToggleTargeting(actionId.Value, action);
+        // TODO: probably should have a clientside event raised for flexibility
+        if (EntityManager.TryGetComponent<TargetActionComponent>(action, out var target))
+            ToggleTargeting((action, action, target));
         else
-            _actionsSystem?.TriggerAction(actionId.Value, baseAction);
+            _actionsSystem?.TriggerAction(action);
     }
 
     private void OnActionAdded(EntityUid actionId)
     {
-        if (_actionsSystem == null ||
-            !_actionsSystem.TryGetActionData(actionId, out var action))
-        {
+        if (_actionsSystem?.GetAction(actionId) is not {} action)
             return;
-        }
 
+        // TODO: event
         // if the action is toggled when we add it, start targeting
-        if (action is BaseTargetActionComponent targetAction && action.Toggled)
-            StartTargeting(actionId, targetAction);
+        if (action.Comp.Toggled && EntityManager.TryGetComponent<TargetActionComponent>(actionId, out var target))
+            StartTargeting((action, action, target));
 
-        if (_actions.Contains(actionId))
+        if (_actions.Contains(action))
             return;
 
-        _actions.Add(actionId);
+        _actions.Add(action);
     }
 
     private void OnActionRemoved(EntityUid actionId)
@@ -437,15 +317,16 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         }
     }
 
-    private bool MatchesFilter(BaseActionComponent action, Filters filter)
+    private bool MatchesFilter(Entity<ActionComponent> ent, Filters filter)
     {
+        var (uid, comp) = ent;
         return filter switch
         {
-            Filters.Enabled => action.Enabled,
-            Filters.Item => action.Container != null && action.Container != _playerManager.LocalEntity,
-            Filters.Innate => action.Container == null || action.Container == _playerManager.LocalEntity,
-            Filters.Instant => action is InstantActionComponent,
-            Filters.Targeted => action is BaseTargetActionComponent,
+            Filters.Enabled => comp.Enabled,
+            Filters.Item => comp.Container != null && comp.Container != _playerManager.LocalEntity,
+            Filters.Innate => comp.Container == null || comp.Container == _playerManager.LocalEntity,
+            Filters.Instant => EntityManager.HasComponent<InstantActionComponent>(uid),
+            Filters.Targeted => EntityManager.HasComponent<TargetActionComponent>(uid),
             _ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null)
         };
     }
@@ -456,7 +337,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
             _window.ResultsGrid.RemoveAllChildren();
     }
 
-    private void PopulateActions(IEnumerable<(EntityUid Id, BaseActionComponent Comp)> actions)
+    private void PopulateActions(IEnumerable<Entity<ActionComponent>> actions)
     {
         if (_window is not { Disposed: false, IsOpen: true })
             return;
@@ -478,7 +359,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         {
             if (i < existing.Count)
             {
-                existing[i++].UpdateData(action.Id, _actionsSystem);
+                existing[i++].UpdateData(action, _actionsSystem);
                 continue;
             }
 
@@ -486,7 +367,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
             button.ActionPressed += OnWindowActionPressed;
             button.ActionUnpressed += OnWindowActionUnPressed;
             button.ActionFocusExited += OnWindowActionFocusExisted;
-            button.UpdateData(action.Id, _actionsSystem);
+            button.UpdateData(action, _actionsSystem);
             _window.ResultsGrid.AddChild(button);
         }
 
@@ -525,13 +406,13 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
 
         actions = actions.Where(action =>
         {
-            if (filters.Count > 0 && filters.Any(filter => !MatchesFilter(action.Comp, filter)))
+            if (filters.Count > 0 && filters.Any(filter => !MatchesFilter(action, filter)))
                 return false;
 
             if (action.Comp.Keywords.Any(keyword => search.Contains(keyword, StringComparison.OrdinalIgnoreCase)))
                 return true;
 
-            var name = EntityManager.GetComponent<MetaDataComponent>(action.Id).EntityName;
+            var name = EntityManager.GetComponent<MetaDataComponent>(action).EntityName;
             if (name.Contains(search, StringComparison.OrdinalIgnoreCase))
                 return true;
 
@@ -581,7 +462,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
 
     private void DragAction()
     {
-        if (_menuDragHelper.Dragged is not {ActionId: {} action} dragged)
+        if (_menuDragHelper.Dragged is not {Action: {} action} dragged)
         {
             _menuDragHelper.EndDrag();
             return;
@@ -591,7 +472,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         var currentlyHovered = UIManager.MouseGetControl(_input.MouseScreenPosition);
         if (currentlyHovered is ActionButton button)
         {
-            swapAction = button.ActionId;
+            swapAction = button.Action;
             SetAction(button, action, false);
         }
 
@@ -665,16 +546,13 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
     private void HandleActionPressed(GUIBoundKeyEventArgs args, ActionButton button)
     {
         args.Handle();
-        if (button.ActionId != null)
+        if (button.Action != null)
         {
             _menuDragHelper.MouseDown(button);
             return;
         }
 
-        var ev = new FillActionSlotEvent();
-        EntityManager.EventBus.RaiseEvent(EventSource.Local, ev);
-        if (ev.Action != null)
-            SetAction(button, ev.Action);
+        // good job
     }
 
     private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
@@ -700,12 +578,13 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
 
         _menuDragHelper.EndDrag();
 
-        if (!_actionsSystem.TryGetActionData(button.ActionId, out var baseAction))
+        if (button.Action is not {} action)
             return;
 
-        if (baseAction is not BaseTargetActionComponent action)
+        // TODO: make this an event
+        if (!EntityManager.TryGetComponent<TargetActionComponent>(action, out var target))
         {
-            _actionsSystem?.TriggerAction(button.ActionId.Value, baseAction);
+            _actionsSystem?.TriggerAction(action);
             return;
         }
 
@@ -714,7 +593,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
 
         // if we're clicking the same thing we're already targeting for, then we simply cancel
         // targeting
-        ToggleTargeting(button.ActionId.Value, action);
+        ToggleTargeting((action, action.Comp, target));
     }
 
     private bool OnMenuBeginDrag()
@@ -722,16 +601,16 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         // TODO ACTIONS
         // The dragging icon shuld be based on the entity's icon style. I.e. if the action has a large icon texture,
         // and a small item/provider sprite, then the dragged icon should be the big texture, not the provider.
-        if (_actionsSystem != null && _actionsSystem.TryGetActionData(_menuDragHelper.Dragged?.ActionId, out var action))
+        if (_menuDragHelper.Dragged?.Action is {} action)
         {
-            if (EntityManager.TryGetComponent(action.EntityIcon, out SpriteComponent? sprite)
+            if (EntityManager.TryGetComponent(action.Comp.EntityIcon, out SpriteComponent? sprite)
                 && sprite.Icon?.GetFrame(RsiDirection.South, 0) is {} frame)
             {
                 _dragShadow.Texture = frame;
             }
-            else if (action.Icon != null)
+            else if (action.Comp.Icon is {} icon)
             {
-                _dragShadow.Texture = _spriteSystem.Frame0(action.Icon);
+                _dragShadow.Texture = _spriteSystem.Frame0(icon);
             }
             else
             {
@@ -898,33 +777,35 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
     /// If currently targeting with no slot or a different slot, switches to
     /// targeting with the specified slot.
     /// </summary>
-    private void ToggleTargeting(EntityUid actionId, BaseTargetActionComponent action)
+    private void ToggleTargeting(Entity<ActionComponent, TargetActionComponent> ent)
     {
-        if (SelectingTargetFor == actionId)
+        if (SelectingTargetFor == ent)
         {
             StopTargeting();
             return;
         }
 
-        StartTargeting(actionId, action);
+        StartTargeting(ent);
     }
 
     /// <summary>
     /// Puts us in targeting mode, where we need to pick either a target point or entity
     /// </summary>
-    private void StartTargeting(EntityUid actionId, BaseTargetActionComponent action)
+    private void StartTargeting(Entity<ActionComponent, TargetActionComponent> ent)
     {
+        var (uid, action, target) = ent;
+
         // If we were targeting something else we should stop
         StopTargeting();
 
-        SelectingTargetFor = actionId;
+        SelectingTargetFor = uid;
         // TODO inform the server
-        action.Toggled = true;
+        _actionsSystem?.SetToggled(uid, true);
 
         // override "held-item" overlay
         var provider = action.Container;
 
-        if (action.TargetingIndicator && _overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
+        if (target.TargetingIndicator && _overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
         {
             if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Container != null)
             {
@@ -940,7 +821,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         {
             foreach (var button in _container.GetButtons())
             {
-                if (button.ActionId == actionId)
+                if (button.Action?.Owner == uid)
                     button.UpdateIcons();
             }
         }
@@ -950,19 +831,19 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         // - Add a yes/no checkmark where the HandItemOverlay usually is
 
         // Highlight valid entity targets
-        if (action is not EntityTargetActionComponent entityAction)
+        if (!EntityManager.TryGetComponent<EntityTargetActionComponent>(uid, out var entity))
             return;
 
         Func<EntityUid, bool>? predicate = null;
-        var attachedEnt = entityAction.AttachedEntity;
+        var attachedEnt = action.AttachedEntity;
 
-        if (!entityAction.CanTargetSelf)
+        if (!entity.CanTargetSelf)
             predicate = e => e != attachedEnt;
 
-        var range = entityAction.CheckCanAccess ? action.Range : -1;
+        var range = target.CheckCanAccess ? target.Range : -1;
 
         _interactionOutline?.SetEnabled(false);
-        _targetOutline?.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, entityAction.Blacklist, null);
+        _targetOutline?.Enable(range, target.CheckCanAccess, predicate, entity.Whitelist, entity.Blacklist, null);
     }
 
     /// <summary>
@@ -974,11 +855,8 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
             return;
 
         var oldAction = SelectingTargetFor;
-        if (_actionsSystem != null && _actionsSystem.TryGetActionData(oldAction, out var action))
-        {
-            // TODO inform the server
-            action.Toggled = false;
-        }
+        // TODO inform the server
+        _actionsSystem?.SetToggled(oldAction, false);
 
         SelectingTargetFor = null;
 
@@ -989,7 +867,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         {
             foreach (var button in _container.GetButtons())
             {
-                if (button.ActionId == oldAction)
+                if (button.Action?.Owner == oldAction)
                     button.UpdateIcons();
             }
         }
index cc12f1b8678cc6dbcf427b6bbe96cd25015c6f81..cad9045fa89807e1c00b397f1d9712adcd7dcc31 100644 (file)
@@ -4,6 +4,7 @@ using Content.Client.Actions.UI;
 using Content.Client.Cooldown;
 using Content.Client.Stylesheets;
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Charges.Components;
 using Content.Shared.Charges.Systems;
 using Robust.Client.GameObjects;
@@ -54,8 +55,7 @@ public sealed class ActionButton : Control, IEntityControl
 
     private Texture? _buttonBackgroundTexture;
 
-    public EntityUid? ActionId { get; private set; }
-    private BaseActionComponent? _action;
+    public Entity<ActionComponent>? Action { get; private set; }
     public bool Locked { get; set; }
 
     public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionPressed;
@@ -193,7 +193,7 @@ public sealed class ActionButton : Control, IEntityControl
 
     private Control? SupplyTooltip(Control sender)
     {
-        if (!_entities.TryGetComponent(ActionId, out MetaDataComponent? metadata))
+        if (!_entities.TryGetComponent(Action, out MetaDataComponent? metadata))
             return null;
 
         var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
@@ -201,14 +201,14 @@ public sealed class ActionButton : Control, IEntityControl
         FormattedMessage? chargesText = null;
 
         // TODO: Don't touch this use an event make callers able to add their own shit for actions or I kill you.
-        if (_entities.TryGetComponent(ActionId, out LimitedChargesComponent? actionCharges))
+        if (_entities.TryGetComponent(Action, out LimitedChargesComponent? actionCharges))
         {
-            var charges = _sharedChargesSys.GetCurrentCharges((ActionId.Value, actionCharges, null));
+            var charges = _sharedChargesSys.GetCurrentCharges((Action.Value, actionCharges, null));
             chargesText = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {charges.ToString()}/{actionCharges.MaxCharges}"));
 
-            if (_entities.TryGetComponent(ActionId, out AutoRechargeComponent? autoRecharge))
+            if (_entities.TryGetComponent(Action, out AutoRechargeComponent? autoRecharge))
             {
-                var chargeTimeRemaining = _sharedChargesSys.GetNextRechargeTime((ActionId.Value, actionCharges, autoRecharge));
+                var chargeTimeRemaining = _sharedChargesSys.GetNextRechargeTime((Action.Value, actionCharges, autoRecharge));
                 chargesText.AddText(Loc.GetString($"{Environment.NewLine}Time Til Recharge: {chargeTimeRemaining}"));
             }
         }
@@ -223,7 +223,7 @@ public sealed class ActionButton : Control, IEntityControl
 
     private void UpdateItemIcon()
     {
-        if (_action is not {EntityIcon: { } entity} ||
+        if (Action?.Comp is not {EntityIcon: { } entity} ||
             !_entities.HasComponent<SpriteComponent>(entity))
         {
             _bigItemSpriteView.Visible = false;
@@ -233,7 +233,7 @@ public sealed class ActionButton : Control, IEntityControl
         }
         else
         {
-            switch (_action.ItemIconStyle)
+            switch (Action?.Comp.ItemIconStyle)
             {
                 case ItemActionIconStyle.BigItem:
                     _bigItemSpriteView.Visible = true;
@@ -259,17 +259,17 @@ public sealed class ActionButton : Control, IEntityControl
 
     private void SetActionIcon(Texture? texture)
     {
-        if (_action == null || texture == null)
+        if (Action?.Comp is not {} action || texture == null)
         {
             _bigActionIcon.Texture = null;
             _bigActionIcon.Visible = false;
             _smallActionIcon.Texture = null;
             _smallActionIcon.Visible = false;
         }
-        else if (_action.EntityIcon != null && _action.ItemIconStyle == ItemActionIconStyle.BigItem)
+        else if (action.EntityIcon != null && action.ItemIconStyle == ItemActionIconStyle.BigItem)
         {
             _smallActionIcon.Texture = texture;
-            _smallActionIcon.Modulate = _action.IconColor;
+            _smallActionIcon.Modulate = action.IconColor;
             _smallActionIcon.Visible = true;
             _bigActionIcon.Texture = null;
             _bigActionIcon.Visible = false;
@@ -277,7 +277,7 @@ public sealed class ActionButton : Control, IEntityControl
         else
         {
             _bigActionIcon.Texture = texture;
-            _bigActionIcon.Modulate = _action.IconColor;
+            _bigActionIcon.Modulate = action.IconColor;
             _bigActionIcon.Visible = true;
             _smallActionIcon.Texture = null;
             _smallActionIcon.Visible = false;
@@ -289,7 +289,7 @@ public sealed class ActionButton : Control, IEntityControl
         UpdateItemIcon();
         UpdateBackground();
 
-        if (_action == null)
+        if (Action is not {} action)
         {
             SetActionIcon(null);
             return;
@@ -297,29 +297,27 @@ public sealed class ActionButton : Control, IEntityControl
 
         _controller ??= UserInterfaceManager.GetUIController<ActionUIController>();
         _spriteSys ??= _entities.System<SpriteSystem>();
-        if ((_controller.SelectingTargetFor == ActionId || _action.Toggled))
+        var icon = action.Comp.Icon;
+        if (_controller.SelectingTargetFor == action || action.Comp.Toggled)
         {
-            if (_action.IconOn != null)
-                SetActionIcon(_spriteSys.Frame0(_action.IconOn));
-            else if (_action.Icon != null)
-                SetActionIcon(_spriteSys.Frame0(_action.Icon));
-            else
-                SetActionIcon(null);
-
-            if (_action.BackgroundOn != null)
-                _buttonBackgroundTexture = _spriteSys.Frame0(_action.BackgroundOn);
+            if (action.Comp.IconOn is {} iconOn)
+                icon = iconOn;
+
+            if (action.Comp.BackgroundOn is {} background)
+                _buttonBackgroundTexture = _spriteSys.Frame0(background);
         }
         else
         {
-            SetActionIcon(_action.Icon != null ? _spriteSys.Frame0(_action.Icon) : null);
             _buttonBackgroundTexture = Theme.ResolveTexture("SlotBackground");
         }
+
+        SetActionIcon(icon != null ? _spriteSys.Frame0(icon) : null);
     }
 
     public void UpdateBackground()
     {
         _controller ??= UserInterfaceManager.GetUIController<ActionUIController>();
-        if (_action != null ||
+        if (Action != null ||
             _controller.IsDragging && GetPositionInParent() == Parent?.ChildCount - 1)
         {
             Button.Texture = _buttonBackgroundTexture;
@@ -333,9 +331,7 @@ public sealed class ActionButton : Control, IEntityControl
     public bool TryReplaceWith(EntityUid actionId, ActionsSystem system)
     {
         if (Locked)
-        {
             return false;
-        }
 
         UpdateData(actionId, system);
         return true;
@@ -343,16 +339,15 @@ public sealed class ActionButton : Control, IEntityControl
 
     public void UpdateData(EntityUid? actionId, ActionsSystem system)
     {
-        ActionId = actionId;
-        system.TryGetActionData(actionId, out _action);
-        Label.Visible = actionId != null;
+        Action = system.GetAction(actionId);
+
+        Label.Visible = Action != null;
         UpdateIcons();
     }
 
     public void ClearData()
     {
-        ActionId = null;
-        _action = null;
+        Action = null;
         Cooldown.Visible = false;
         Cooldown.Progress = 1;
         Label.Visible = false;
@@ -365,19 +360,15 @@ public sealed class ActionButton : Control, IEntityControl
 
         UpdateBackground();
 
-        Cooldown.Visible = _action != null && _action.Cooldown != null;
-        if (_action == null)
+        Cooldown.Visible = Action?.Comp.Cooldown != null;
+        if (Action?.Comp is not {} action)
             return;
 
-        if (_action.Cooldown != null)
-        {
-            Cooldown.FromTime(_action.Cooldown.Value.Start, _action.Cooldown.Value.End);
-        }
+        if (action.Cooldown is {} cooldown)
+            Cooldown.FromTime(cooldown.Start, cooldown.End);
 
-        if (ActionId != null && _toggled != _action.Toggled)
-        {
-            _toggled = _action.Toggled;
-        }
+        if (_toggled != action.Toggled)
+            _toggled = action.Toggled;
     }
 
     protected override void MouseEntered()
@@ -404,7 +395,7 @@ public sealed class ActionButton : Control, IEntityControl
     public void Depress(GUIBoundKeyEventArgs args, bool depress)
     {
         // action can still be toggled if it's allowed to stay selected
-        if (_action is not {Enabled: true})
+        if (Action?.Comp is not {Enabled: true})
             return;
 
         _depressed = depress;
@@ -414,17 +405,17 @@ public sealed class ActionButton : Control, IEntityControl
     public void DrawModeChanged()
     {
         _controller ??= UserInterfaceManager.GetUIController<ActionUIController>();
-        HighlightRect.Visible = _beingHovered && (_action != null || _controller.IsDragging);
+        HighlightRect.Visible = _beingHovered && (Action != null || _controller.IsDragging);
 
         // always show the normal empty button style if no action in this slot
-        if (_action == null)
+        if (Action?.Comp is not {} action)
         {
             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 && (_controller.IsDragging || _action!.Enabled))
+        if (_beingHovered && (_controller.IsDragging || action.Enabled))
         {
             SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
         }
@@ -439,16 +430,16 @@ public sealed class ActionButton : Control, IEntityControl
         }
 
         // if it's toggled on, always show the toggled on style (currently same as depressed style)
-        if (_action.Toggled || _controller.SelectingTargetFor == ActionId)
+        if (action.Toggled || _controller.SelectingTargetFor == Action?.Owner)
         {
             // when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
-            SetOnlyStylePseudoClass(_action.IconOn != null
+            SetOnlyStylePseudoClass(action.IconOn != null
                 ? ContainerButton.StylePseudoClassNormal
                 : ContainerButton.StylePseudoClassPressed);
             return;
         }
 
-        if (!_action.Enabled)
+        if (!action.Enabled)
         {
             SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
             return;
@@ -457,5 +448,5 @@ public sealed class ActionButton : Control, IEntityControl
         SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
     }
 
-    EntityUid? IEntityControl.UiEntity => ActionId;
+    EntityUid? IEntityControl.UiEntity => Action;
 }
index c232e823132a0f0343561394f05c9ace5c0495e4..0dbec0c83abb22b6931c7fb355a7da497e8dc431 100644 (file)
@@ -1,5 +1,6 @@
 using System.Linq;
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.CombatMode;
 using Robust.Server.Player;
 using Robust.Shared.GameObjects;
@@ -46,24 +47,26 @@ public sealed class ActionsAddedTest
         // This action should have a non-null event both on the server & client.
         var evType = typeof(ToggleCombatActionEvent);
 
+        var sQuery = sEntMan.GetEntityQuery<InstantActionComponent>();
+        var cQuery = cEntMan.GetEntityQuery<InstantActionComponent>();
         var sActions = sActionSystem.GetActions(serverEnt).Where(
-            x => x.Comp is InstantActionComponent act && act.Event?.GetType() == evType).ToArray();
+            ent => sQuery.CompOrNull(ent)?.Event?.GetType() == evType).ToArray();
         var cActions = cActionSystem.GetActions(clientEnt).Where(
-            x => x.Comp is InstantActionComponent act && act.Event?.GetType() == evType).ToArray();
+            ent => cQuery.CompOrNull(ent)?.Event?.GetType() == evType).ToArray();
 
         Assert.That(sActions.Length, Is.EqualTo(1));
         Assert.That(cActions.Length, Is.EqualTo(1));
 
-        var sAct = sActions[0].Comp;
-        var cAct = cActions[0].Comp;
+        var sAct = sActions[0];
+        var cAct = cActions[0];
 
-        Assert.That(sAct, Is.Not.Null);
-        Assert.That(cAct, Is.Not.Null);
+        Assert.That(sAct.Comp, Is.Not.Null);
+        Assert.That(cAct.Comp, Is.Not.Null);
 
         // Finally, these two actions are not the same object
         // required, because integration tests do not respect the [NonSerialized] attribute and will simply events by reference.
-        Assert.That(ReferenceEquals(sAct, cAct), Is.False);
-        Assert.That(ReferenceEquals(sAct.BaseEvent, cAct.BaseEvent), Is.False);
+        Assert.That(ReferenceEquals(sAct.Comp, cAct.Comp), Is.False);
+        Assert.That(ReferenceEquals(sQuery.GetComponent(sAct).Event, cQuery.GetComponent(cAct).Event), Is.False);
 
         await pair.CleanReturnAsync();
     }
index c658b3f90a4ee974bdf8229f44aa30eefadccb1a..973e1afbcd776567333fafba28e110a20271002b 100644 (file)
@@ -1,5 +1,6 @@
 using System.Linq;
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Charges.Components;
 using Content.Shared.Charges.Systems;
 using Content.Shared.Interaction;
@@ -60,8 +61,10 @@ public sealed class ActionOnInteractSystem : EntitySystem
         if (!TryUseCharge((uid, component)))
             return;
 
-        var (actId, act) = _random.Pick(options);
-        _actions.PerformAction(args.User, null, actId, act, act.Event, _timing.CurTime, false);
+        // not predicted as this is in server due to random
+        // TODO: use predicted random and move to shared?
+        var (actId, action, comp) = _random.Pick(options);
+        _actions.PerformAction(args.User, (actId, action), predicted: false);
         args.Handled = true;
     }
 
@@ -79,13 +82,13 @@ public sealed class ActionOnInteractSystem : EntitySystem
         }
 
         // First, try entity target actions
-        if (args.Target != null)
+        if (args.Target is {} target)
         {
             var entOptions = GetValidActions<EntityTargetActionComponent>(actionEnts, args.CanReach);
             for (var i = entOptions.Count - 1; i >= 0; i--)
             {
                 var action = entOptions[i];
-                if (!_actions.ValidateEntityTarget(args.User, args.Target.Value, action))
+                if (!_actions.ValidateEntityTarget(args.User, target, (action, action.Comp2)))
                     entOptions.RemoveAt(i);
             }
 
@@ -94,50 +97,19 @@ public sealed class ActionOnInteractSystem : EntitySystem
                 if (!TryUseCharge((uid, component)))
                     return;
 
-                var (entActId, entAct) = _random.Pick(entOptions);
-                if (entAct.Event != null)
-                {
-                    entAct.Event.Target = args.Target.Value;
-                }
-
-                _actions.PerformAction(args.User, null, entActId, entAct, entAct.Event, _timing.CurTime, false);
+                var (actionId, action, _) = _random.Pick(entOptions);
+                _actions.SetEventTarget(actionId, target);
+                _actions.PerformAction(args.User, (actionId, action), predicted: false);
                 args.Handled = true;
                 return;
             }
         }
-
-        // Then EntityWorld target actions
-        var entWorldOptions = GetValidActions<EntityWorldTargetActionComponent>(actionEnts, args.CanReach);
-        for (var i = entWorldOptions.Count - 1; i >= 0; i--)
-        {
-            var action = entWorldOptions[i];
-            if (!_actions.ValidateEntityWorldTarget(args.User, args.Target, args.ClickLocation, action))
-                entWorldOptions.RemoveAt(i);
-        }
-
-        if (entWorldOptions.Count > 0)
-        {
-            if (!TryUseCharge((uid, component)))
-                return;
-
-            var (entActId, entAct) = _random.Pick(entWorldOptions);
-            if (entAct.Event != null)
-            {
-                entAct.Event.Entity = args.Target;
-                entAct.Event.Coords = args.ClickLocation;
-            }
-
-            _actions.PerformAction(args.User, null, entActId, entAct, entAct.Event, _timing.CurTime, false);
-            args.Handled = true;
-            return;
-        }
-
         // else: try world target actions
         var options = GetValidActions<WorldTargetActionComponent>(component.ActionEntities, args.CanReach);
         for (var i = options.Count - 1; i >= 0; i--)
         {
             var action = options[i];
-            if (!_actions.ValidateWorldTarget(args.User, args.ClickLocation, action))
+            if (!_actions.ValidateWorldTarget(args.User, args.ClickLocation, (action, action.Comp2)))
                 options.RemoveAt(i);
         }
 
@@ -147,33 +119,34 @@ public sealed class ActionOnInteractSystem : EntitySystem
         if (!TryUseCharge((uid, component)))
             return;
 
-        var (actId, act) = _random.Pick(options);
-        if (act.Event != null)
+        var (actId, comp, world) = _random.Pick(options);
+        if (world.Event is {} worldEv)
         {
-            act.Event.Target = args.ClickLocation;
+            worldEv.Target = args.ClickLocation;
+            worldEv.Entity = HasComp<EntityTargetActionComponent>(actId) ? args.Target : null;
         }
 
-        _actions.PerformAction(args.User, null, actId, act, act.Event, _timing.CurTime, false);
+        _actions.PerformAction(args.User, (actId, comp), world.Event, predicted: false);
         args.Handled = true;
     }
 
-    private List<(EntityUid Id, T Comp)> GetValidActions<T>(List<EntityUid>? actions, bool canReach = true) where T : BaseActionComponent
+    private List<Entity<ActionComponent, T>> GetValidActions<T>(List<EntityUid>? actions, bool canReach = true) where T: Component
     {
-        var valid = new List<(EntityUid Id, T Comp)>();
+        var valid = new List<Entity<ActionComponent, T>>();
 
         if (actions == null)
             return valid;
 
         foreach (var id in actions)
         {
-            if (!_actions.TryGetActionData(id, out var baseAction) ||
-                baseAction as T is not { } action ||
+            if (_actions.GetAction(id) is not {} action ||
+                !TryComp<T>(id, out var comp) ||
                 !_actions.ValidAction(action, canReach))
             {
                 continue;
             }
 
-            valid.Add((id, action));
+            valid.Add((id, action, comp));
         }
 
         return valid;
index fb50cfb3a35ff276d54cb6b512e65763f48a1123..93de66c856c7ea78de460baf94d3dff5bb15417c 100644 (file)
@@ -1,5 +1,6 @@
-using Content.Server.Administration;
+using Content.Server.Administration;
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Administration;
 using Robust.Shared.Console;
 
index f022a45eccb8fe2f124f2dcc737b50c989152887..649607f724243ead3163b7c26cb65be4e18a7960 100644 (file)
@@ -1,5 +1,5 @@
 using Content.Server.NPC.Systems;
-using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Robust.Shared.Prototypes;
 
 namespace Content.Server.NPC.Components;
@@ -20,7 +20,7 @@ public sealed partial class NPCUseActionOnTargetComponent : Component
     /// Action that's going to attempt to be used.
     /// </summary>
     [DataField(required: true)]
-    public EntProtoId<EntityWorldTargetActionComponent> ActionId;
+    public EntProtoId<TargetActionComponent> ActionId;
 
     [DataField]
     public EntityUid? ActionEnt;
index 33bc8f9074c3686be93383b36e2268166f18df74..9822050f95a5d139fcad58ab35593e7f13a768b3 100644 (file)
@@ -28,24 +28,16 @@ public sealed class NPCUseActionOnTargetSystem : EntitySystem
         if (!Resolve(user, ref user.Comp, false))
             return false;
 
-        if (!TryComp<EntityWorldTargetActionComponent>(user.Comp.ActionEnt, out var action))
+        if (_actions.GetAction(user.Comp.ActionEnt) is not {} action)
             return false;
 
         if (!_actions.ValidAction(action))
             return false;
 
-        if (action.Event != null)
-        {
-            action.Event.Coords = Transform(target).Coordinates;
-        }
+        _actions.SetEventTarget(action, target);
 
-        _actions.PerformAction(user,
-            null,
-            user.Comp.ActionEnt.Value,
-            action,
-            action.BaseEvent,
-            _timing.CurTime,
-            false);
+        // NPC is serverside, no prediction :(
+        _actions.PerformAction(user.Owner, action, predicted: false);
         return true;
     }
 
index b87e3febee6283995564c931e7c1827ed627a119..5976594a577d5b429a6e98afa6857a89aefdf880 100644 (file)
@@ -4,6 +4,7 @@ using Content.Server.Inventory;
 using Content.Server.Mind.Commands;
 using Content.Server.Polymorph.Components;
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Buckle;
 using Content.Shared.Coordinates;
 using Content.Shared.Damage;
@@ -111,8 +112,8 @@ public sealed partial class PolymorphSystem : EntitySystem
 
         if (_actions.AddAction(uid, ref component.Action, out var action, RevertPolymorphId))
         {
-            action.EntityIcon = component.Parent;
-            action.UseDelay = TimeSpan.FromSeconds(component.Configuration.Delay);
+            _actions.SetEntityIcon((component.Action.Value, action), component.Parent);
+            _actions.SetUseDelay(component.Action.Value, TimeSpan.FromSeconds(component.Configuration.Delay));
         }
     }
 
@@ -397,20 +398,19 @@ public sealed partial class PolymorphSystem : EntitySystem
         _metaData.SetEntityName(actionId.Value, Loc.GetString("polymorph-self-action-name", ("target", entProto.Name)), metaDataCache);
         _metaData.SetEntityDescription(actionId.Value, Loc.GetString("polymorph-self-action-description", ("target", entProto.Name)), metaDataCache);
 
-        if (!_actions.TryGetActionData(actionId, out var baseAction))
+        if (_actions.GetAction(actionId) is not {} action)
             return;
 
-        baseAction.Icon = new SpriteSpecifier.EntityPrototype(polyProto.Configuration.Entity);
-        if (baseAction is InstantActionComponent action)
-            action.Event = new PolymorphActionEvent(id);
+        _actions.SetIcon((action, action.Comp), new SpriteSpecifier.EntityPrototype(polyProto.Configuration.Entity));
+        _actions.SetEvent(action, new PolymorphActionEvent(id));
     }
 
     public void RemovePolymorphAction(ProtoId<PolymorphPrototype> id, Entity<PolymorphableComponent> target)
     {
-        if (target.Comp.PolymorphActions == null)
+        if (target.Comp.PolymorphActions is not {} actions)
             return;
 
-        if (target.Comp.PolymorphActions.TryGetValue(id, out var val))
-            _actions.RemoveAction(target, val);
+        if (actions.TryGetValue(id, out var action))
+            _actions.RemoveAction(target.Owner, action);
     }
 }
index f95a5807360ca656b1a059acc5fe510360a5d16a..784c2f8fbb9c7ae7df443c8b08645c9dea38418f 100644 (file)
@@ -61,12 +61,10 @@ public sealed partial class BorgSystem
 
         if (_actions.AddAction(chassis, ref component.ModuleSwapActionEntity, out var action, component.ModuleSwapActionId, uid))
         {
-            if(TryComp<BorgModuleIconComponent>(uid, out var moduleIconComp))
-            {
-                action.Icon = moduleIconComp.Icon;
-            };
-            action.EntityIcon = uid;
-            Dirty(component.ModuleSwapActionEntity.Value, action);
+            var actEnt = (component.ModuleSwapActionEntity.Value, action);
+            _actions.SetEntityIcon(actEnt, uid);
+            if (TryComp<BorgModuleIconComponent>(uid, out var moduleIconComp))
+                _actions.SetIcon(actEnt, moduleIconComp.Icon);
         }
 
         if (!TryComp(chassis, out BorgChassisComponent? chassisComp))
index 587270bd641160b8eb099af9003da7c9437d02f1..f9772e48dafa8db63958c215151289110376bca1 100644 (file)
@@ -350,10 +350,7 @@ public sealed partial class StoreSystem
 
             component.BoughtEntities.RemoveAt(i);
 
-            if (_actions.TryGetActionData(purchase, out var actionComponent, logError: false))
-            {
-                _actionContainer.RemoveAction(purchase, actionComponent);
-            }
+            _actionContainer.RemoveAction(purchase, logMissing: false);
 
             EntityManager.DeleteEntity(purchase);
         }
index abdb0d5e1085a2770ebb6e438d66883e4cc0a8ca..fa151b4f09f55e60264593ad20b38ab668c07394 100644 (file)
@@ -3,7 +3,7 @@ using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Abilities.Goliath;
 
-public sealed partial class GoliathSummonTentacleAction : EntityWorldTargetActionEvent
+public sealed partial class GoliathSummonTentacleAction : WorldTargetActionEvent
 {
     /// <summary>
     /// The ID of the entity that is spawned.
index 98dbc5e18a4f138af9e8cf929666dfdae4b5ced3..e0a0453285853c72a260d45edec754b8b5bf4394 100644 (file)
@@ -28,7 +28,7 @@ public sealed class GoliathTentacleSystem : EntitySystem
 
     private void OnSummonAction(GoliathSummonTentacleAction args)
     {
-        if (args.Handled || args.Coords is not { } coords)
+        if (args.Handled)
             return;
 
         // TODO: animation
@@ -36,6 +36,7 @@ public sealed class GoliathTentacleSystem : EntitySystem
         _popup.PopupPredicted(Loc.GetString("tentacle-ability-use-popup", ("entity", args.Performer)), args.Performer, args.Performer, type: PopupType.SmallCaution);
         _stun.TryStun(args.Performer, TimeSpan.FromSeconds(0.8f), false);
 
+        var coords = args.Target;
         List<EntityCoordinates> spawnPos = new();
         spawnPos.Add(coords);
 
index 1a83cf38e51343e4a4a752573b3e8a4a0d326e39..534f0d3ee74ed60a1f8674712634bad6664df282 100644 (file)
@@ -1,5 +1,6 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
+using Content.Shared.Actions.Components;
 using Content.Shared.Ghost;
 using Content.Shared.Mind;
 using Content.Shared.Mind.Components;
@@ -22,10 +23,14 @@ public sealed class ActionContainerSystem : EntitySystem
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly SharedMindSystem _mind = default!;
 
+    private EntityQuery<ActionComponent> _query;
+
     public override void Initialize()
     {
         base.Initialize();
 
+        _query = GetEntityQuery<ActionComponent>();
+
         SubscribeLocalEvent<ActionsContainerComponent, ComponentInit>(OnInit);
         SubscribeLocalEvent<ActionsContainerComponent, ComponentShutdown>(OnShutdown);
         SubscribeLocalEvent<ActionsContainerComponent, EntRemovedFromContainerMessage>(OnEntityRemoved);
@@ -77,7 +82,7 @@ public sealed class ActionContainerSystem : EntitySystem
     /// <inheritdoc cref="EnsureAction(Robust.Shared.GameObjects.EntityUid,ref System.Nullable{Robust.Shared.GameObjects.EntityUid},string?,Content.Shared.Actions.ActionsContainerComponent?)"/>
     public bool EnsureAction(EntityUid uid,
         [NotNullWhen(true)] ref EntityUid? actionId,
-        [NotNullWhen(true)] out BaseActionComponent? action,
+        [NotNullWhen(true)] out ActionComponent? action,
         string? actionPrototypeId,
         ActionsContainerComponent? comp = null)
     {
@@ -94,12 +99,14 @@ public sealed class ActionContainerSystem : EntitySystem
                 return false;
             }
 
-            if (!_actions.TryGetActionData(actionId, out action))
+            if (_actions.GetAction(actionId) is not {} ent)
                 return false;
 
-            DebugTools.Assert(Transform(actionId.Value).ParentUid == uid);
-            DebugTools.Assert(_container.IsEntityInContainer(actionId.Value));
-            DebugTools.Assert(action.Container == uid);
+            actionId = ent;
+            action = ent.Comp;
+            DebugTools.Assert(Transform(ent).ParentUid == uid);
+            DebugTools.Assert(_container.IsEntityInContainer(ent));
+            DebugTools.Assert(ent.Comp.Container == uid);
             return true;
         }
 
@@ -112,7 +119,14 @@ public sealed class ActionContainerSystem : EntitySystem
             return false;
 
         actionId = Spawn(actionPrototypeId);
-        if (AddAction(uid, actionId.Value, action, comp) && _actions.TryGetActionData(actionId, out action))
+        if (!_query.TryComp(actionId, out action))
+        {
+            Log.Error($"Tried to add invalid action {ToPrettyString(actionId)} to {ToPrettyString(uid)}!");
+            Del(actionId);
+            return false;
+        }
+
+        if (AddAction(uid, actionId.Value, action, comp))
             return true;
 
         Del(actionId.Value);
@@ -129,21 +143,21 @@ public sealed class ActionContainerSystem : EntitySystem
     public void TransferAction(
         EntityUid actionId,
         EntityUid newContainer,
-        BaseActionComponent? action = null,
+        ActionComponent? action = null,
         ActionsContainerComponent? container = null)
     {
-        if (!_actions.ResolveActionData(actionId, ref action))
+        if (_actions.GetAction((actionId, action)) is not {} ent)
             return;
 
-        if (action.Container == newContainer)
+        if (ent.Comp.Container == newContainer)
             return;
 
-        var attached = action.AttachedEntity;
-        if (!AddAction(newContainer, actionId, action, container))
+        var attached = ent.Comp.AttachedEntity;
+        if (!AddAction(newContainer, ent, ent.Comp, container))
             return;
 
-        DebugTools.AssertEqual(action.Container, newContainer);
-        DebugTools.AssertEqual(action.AttachedEntity, attached);
+        DebugTools.AssertEqual(ent.Comp.Container, newContainer);
+        DebugTools.AssertEqual(ent.Comp.AttachedEntity, attached);
     }
 
     /// <summary>
@@ -180,23 +194,23 @@ public sealed class ActionContainerSystem : EntitySystem
         EntityUid actionId,
         EntityUid newContainer,
         EntityUid newAttached,
-        BaseActionComponent? action = null,
+        ActionComponent? action = null,
         ActionsContainerComponent? container = null)
     {
-        if (!_actions.ResolveActionData(actionId, ref action))
+        if (_actions.GetAction((actionId, action)) is not {} ent)
             return;
 
-        if (action.Container == newContainer)
+        if (ent.Comp.Container == newContainer)
             return;
 
         var attached = newAttached;
-        if (!AddAction(newContainer, actionId, action, container))
+        if (!AddAction(newContainer, ent, ent.Comp, container))
             return;
 
-        DebugTools.AssertEqual(action.Container, newContainer);
-        _actions.AddActionDirect(newAttached, actionId, action: action);
+        DebugTools.AssertEqual(ent.Comp.Container, newContainer);
+        _actions.AddActionDirect(newAttached, (ent, ent.Comp));
 
-        DebugTools.AssertEqual(action.AttachedEntity, attached);
+        DebugTools.AssertEqual(ent.Comp.AttachedEntity, attached);
     }
 
     /// <summary>
@@ -227,25 +241,25 @@ public sealed class ActionContainerSystem : EntitySystem
     /// <summary>
     /// Adds a pre-existing action to an action container. If the action is already in some container it will first remove it.
     /// </summary>
-    public bool AddAction(EntityUid uid, EntityUid actionId, BaseActionComponent? action = null, ActionsContainerComponent? comp = null)
+    public bool AddAction(EntityUid uid, EntityUid actionId, ActionComponent? action = null, ActionsContainerComponent? comp = null)
     {
-        if (!_actions.ResolveActionData(actionId, ref action))
+        if (_actions.GetAction((actionId, action)) is not {} ent)
             return false;
 
-        if (action.Container != null)
-            RemoveAction(actionId, action);
+        if (ent.Comp.Container != null)
+            RemoveAction((ent, ent));
 
         DebugTools.AssertOwner(uid, comp);
         comp ??= EnsureComp<ActionsContainerComponent>(uid);
-        if (!_container.Insert(actionId, comp.Container))
+        if (!_container.Insert(ent.Owner, comp.Container))
         {
-            Log.Error($"Failed to insert action {ToPrettyString(actionId)} into {ToPrettyString(uid)}");
+            Log.Error($"Failed to insert action {ToPrettyString(ent)} into {ToPrettyString(uid)}");
             return false;
         }
 
         // Container insert events should have updated the component's fields:
-        DebugTools.Assert(comp.Container.Contains(actionId));
-        DebugTools.Assert(action.Container == uid);
+        DebugTools.Assert(comp.Container.Contains(ent));
+        DebugTools.Assert(ent.Comp.Container == uid);
 
         return true;
     }
@@ -253,30 +267,31 @@ public sealed class ActionContainerSystem : EntitySystem
     /// <summary>
     /// Removes an action from its container and any action-performer and moves the action to null-space
     /// </summary>
-    public void RemoveAction(EntityUid actionId, BaseActionComponent? action = null)
+    public void RemoveAction(Entity<ActionComponent?>? action, bool logMissing = true)
     {
-        if (!_actions.ResolveActionData(actionId, ref action))
+        if (_actions.GetAction(action, logMissing) is not {} ent)
             return;
 
-        if (action.Container == null)
+        if (ent.Comp.Container == null)
             return;
 
-        _transform.DetachEntity(actionId, Transform(actionId));
+        _transform.DetachEntity(ent, Transform(ent));
 
         // Container removal events should have removed the action from the action container.
         // However, just in case the container was already deleted we will still manually clear the container field
-        if (action.Container != null)
+        if (ent.Comp.Container is {} container)
         {
-            if (Exists(action.Container))
-                Log.Error($"Failed to remove action {ToPrettyString(actionId)} from its container {ToPrettyString(action.Container)}?");
-            action.Container = null;
+            if (Exists(container))
+                Log.Error($"Failed to remove action {ToPrettyString(ent)} from its container {ToPrettyString(container)}?");
+            ent.Comp.Container = null;
+            DirtyField(ent, ent.Comp, nameof(ActionComponent.Container));
         }
 
         // If the action was granted to some entity, then the removal from the container should have automatically removed it.
         // However, if the action was granted without ever being placed in an action container, it will not have been removed.
         // Therefore, to ensure that the behaviour of the method is consistent we will also explicitly remove the action.
-        if (action.AttachedEntity != null)
-            _actions.RemoveAction(action.AttachedEntity.Value, actionId, action: action);
+        if (ent.Comp.AttachedEntity is {} actions)
+            _actions.RemoveAction(actions, (ent, ent));
     }
 
     private void OnInit(EntityUid uid, ActionsContainerComponent component, ComponentInit args)
@@ -297,16 +312,16 @@ public sealed class ActionContainerSystem : EntitySystem
         if (args.Container.ID != ActionsContainerComponent.ContainerId)
             return;
 
-        if (!_actions.TryGetActionData(args.Entity, out var data))
+        if (_actions.GetAction(args.Entity) is not {} action)
             return;
 
-        if (data.Container != uid)
+        if (action.Comp.Container != uid)
         {
-            data.Container = uid;
-            Dirty(args.Entity, data);
+            action.Comp.Container = uid;
+            DirtyField(action, action.Comp, nameof(ActionComponent.Container));
         }
 
-        var ev = new ActionAddedEvent(args.Entity, data);
+        var ev = new ActionAddedEvent(args.Entity, action);
         RaiseLocalEvent(uid, ref ev);
     }
 
@@ -315,17 +330,17 @@ public sealed class ActionContainerSystem : EntitySystem
         if (args.Container.ID != ActionsContainerComponent.ContainerId)
             return;
 
-        if (!_actions.TryGetActionData(args.Entity, out var data, false))
+        if (_actions.GetAction(args.Entity, false) is not {} action)
             return;
 
-        var ev = new ActionRemovedEvent(args.Entity, data);
+        var ev = new ActionRemovedEvent(args.Entity, action);
         RaiseLocalEvent(uid, ref ev);
 
-        if (data.Container == null)
+        if (action.Comp.Container == null)
             return;
 
-        data.Container = null;
-        Dirty(args.Entity, data);
+        action.Comp.Container = null;
+        DirtyField(action, action.Comp, nameof(ActionComponent.Container));
     }
 
     private void OnActionAdded(EntityUid uid, ActionsContainerComponent component, ActionAddedEvent args)
@@ -342,9 +357,9 @@ public sealed class ActionContainerSystem : EntitySystem
 public readonly struct ActionAddedEvent
 {
     public readonly EntityUid Action;
-    public readonly BaseActionComponent Component;
+    public readonly ActionComponent Component;
 
-    public ActionAddedEvent(EntityUid action, BaseActionComponent component)
+    public ActionAddedEvent(EntityUid action, ActionComponent component)
     {
         Action = action;
         Component = component;
@@ -358,9 +373,9 @@ public readonly struct ActionAddedEvent
 public readonly struct ActionRemovedEvent
 {
     public readonly EntityUid Action;
-    public readonly BaseActionComponent Component;
+    public readonly ActionComponent Component;
 
-    public ActionRemovedEvent(EntityUid action, BaseActionComponent component)
+    public ActionRemovedEvent(EntityUid action, ActionComponent component)
     {
         Action = action;
         Component = component;
index 6ff86604589cbe69a1f55ecb87367daaff105e63..3e03911b0ac4c81d547570bb9e98ffcc0c2814c9 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.Actions.Components;
 using Content.Shared.Hands;
 using Content.Shared.Inventory;
 using Content.Shared.Inventory.Events;
@@ -102,7 +103,7 @@ public sealed class RequestPerformActionEvent : EntityEventArgs
         EntityCoordinatesTarget = entityCoordinatesTarget;
     }
 
-    public RequestPerformActionEvent(NetEntity action, NetEntity entityTarget, NetCoordinates entityCoordinatesTarget)
+    public RequestPerformActionEvent(NetEntity action, NetEntity? entityTarget, NetCoordinates entityCoordinatesTarget)
     {
         Action = action;
         EntityTarget = entityTarget;
@@ -149,27 +150,12 @@ public abstract partial class WorldTargetActionEvent : BaseActionEvent
     ///     The coordinates of the location that the user targeted.
     /// </summary>
     public EntityCoordinates Target;
-}
 
-/// <summary>
-///     This is the type of event that gets raised when an <see cref="EntityWorldTargetActionComponent"/> is performed.
-///     The <see cref="BaseActionEvent.Performer"/>, <see cref="Entity"/>, and <see cref="Coords"/>
-///     fields will automatically be filled out by the <see cref="SharedActionsSystem"/>.
-/// </summary>
-/// <remarks>
-///     To define a new action for some system, you need to create an event that inherits from this class.
-/// </remarks>
-public abstract partial class EntityWorldTargetActionEvent : BaseActionEvent
-{
     /// <summary>
-    ///     The entity that the user targeted.
+    /// When combined with <see cref="EntityTargetAction"/> (and <c>Event</c> is null), the entity the client was hovering when clicked.
+    /// This can be null as the primary purpose of this event is for getting coordinates.
     /// </summary>
     public EntityUid? Entity;
-
-    /// <summary>
-    ///     The coordinates of the location that the user targeted.
-    /// </summary>
-    public EntityCoordinates? Coords;
 }
 
 /// <summary>
@@ -187,7 +173,7 @@ public abstract partial class BaseActionEvent : HandledEntityEventArgs
     /// <summary>
     ///     The action the event belongs to.
     /// </summary>
-    public Entity<BaseActionComponent> Action;
+    public Entity<ActionComponent> Action;
 
     /// <summary>
     /// Should we toggle the action entity?
index b89b462814cea555876f3509c94152ce91f2b77a..72e412b50f688daf76a97f077ad4fa32a94e6411 100644 (file)
@@ -1,5 +1,6 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
+using Content.Shared.Actions.Components;
 using Content.Shared.Actions.Events;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
@@ -22,13 +23,13 @@ public sealed class ActionUpgradeSystem : EntitySystem
     private void OnActionUpgradeEvent(EntityUid uid, ActionUpgradeComponent component, ActionUpgradeEvent args)
     {
         if (!CanUpgrade(args.NewLevel, component.EffectedLevels, out var newActionProto)
-            || !_actions.TryGetActionData(uid, out var actionComp))
+            || _actions.GetAction(uid) is not {} action)
             return;
 
-        var originalContainer = actionComp.Container;
-        var originalAttachedEntity = actionComp.AttachedEntity;
+        var originalContainer = action.Comp.Container;
+        var originalAttachedEntity = action.Comp.AttachedEntity;
 
-        _actionContainer.RemoveAction(uid, actionComp);
+        _actionContainer.RemoveAction((action, action));
 
         EntityUid? upgradedActionId = null;
         if (originalContainer != null
@@ -150,16 +151,16 @@ public sealed class ActionUpgradeSystem : EntitySystem
         // RaiseActionUpgradeEvent(newLevel, actionId.Value);
 
         if (!CanUpgrade(newLevel, actionUpgradeComponent.EffectedLevels, out var newActionPrototype)
-            || !_actions.TryGetActionData(actionId, out var actionComp))
+            || _actions.GetAction(actionId) is not {} action)
             return null;
 
         newActionProto ??= newActionPrototype;
         DebugTools.AssertNotNull(newActionProto);
 
-        var originalContainer = actionComp.Container;
-        var originalAttachedEntity = actionComp.AttachedEntity;
+        var originalContainer = action.Comp.Container;
+        var originalAttachedEntity = action.Comp.AttachedEntity;
 
-        _actionContainer.RemoveAction(actionId.Value, actionComp);
+        _actionContainer.RemoveAction((action, action.Comp));
 
         EntityUid? upgradedActionId = null;
         if (originalContainer != null
similarity index 61%
rename from Content.Shared/Actions/BaseActionComponent.cs
rename to Content.Shared/Actions/Components/ActionComponent.cs
index 05abe30f24edffb00757eb55a91d28ddb5dcd222..2833aa1798d19a4f84e378326f9c7a2a931a5825 100644 (file)
@@ -1,35 +1,39 @@
-using Robust.Shared.Audio;
+using Content.Shared.Actions;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 using Robust.Shared.Utility;
 
-namespace Content.Shared.Actions;
+namespace Content.Shared.Actions.Components;
 
-// TODO ACTIONS make this a separate component and remove the inheritance stuff.
-// TODO ACTIONS convert to auto comp state?
-
-// TODO add access attribute. Need to figure out what to do with decal & mapping actions.
-// [Access(typeof(SharedActionsSystem))]
+/// <summary>
+/// Component all actions are required to have.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedActionsSystem))]
+[AutoGenerateComponentState(true, true)]
 [EntityCategory("Actions")]
-public abstract partial class BaseActionComponent : Component
+public sealed partial class ActionComponent : Component
 {
-    public abstract BaseActionEvent? BaseEvent { get; }
-
     /// <summary>
     ///     Icon representing this action in the UI.
     /// </summary>
-    [DataField("icon")] public SpriteSpecifier? Icon;
+    [DataField, AutoNetworkedField]
+    public SpriteSpecifier? Icon;
 
     /// <summary>
     ///     For toggle actions only, icon to show when toggled on. If omitted, the action will simply be highlighted
     ///     when turned on.
     /// </summary>
-    [DataField("iconOn")] public SpriteSpecifier? IconOn;
+    [DataField, AutoNetworkedField]
+    public SpriteSpecifier? IconOn;
 
     /// <summary>
     ///     For toggle actions only, background to show when toggled on.
     /// </summary>
-    [DataField] public SpriteSpecifier? BackgroundOn;
+    [DataField]
+    public SpriteSpecifier? BackgroundOn;
 
     /// <summary>
     ///     If not null, this color will modulate the action icon color.
@@ -38,12 +42,14 @@ public abstract partial class BaseActionComponent : Component
     ///     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.
     /// </remarks>
-    [DataField("iconColor")] public Color IconColor = Color.White;
+    [DataField, AutoNetworkedField]
+    public Color IconColor = Color.White;
 
     /// <summary>
     ///     The original <see cref="IconColor"/> this action was.
     /// </summary>
-    [DataField] public Color OriginalIconColor;
+    [DataField, AutoNetworkedField]
+    public Color OriginalIconColor;
 
     /// <summary>
     ///     The color the action should turn to when disabled
@@ -53,12 +59,14 @@ public abstract partial class BaseActionComponent : Component
     /// <summary>
     ///     Keywords that can be used to search for this action in the action menu.
     /// </summary>
-    [DataField("keywords")] public HashSet<string> Keywords = new();
+    [DataField, AutoNetworkedField]
+    public HashSet<string> Keywords = new();
 
     /// <summary>
     ///     Whether this action is currently enabled. If not enabled, this action cannot be performed.
     /// </summary>
-    [DataField("enabled")] public bool Enabled = true;
+    [DataField, AutoNetworkedField]
+    public bool Enabled = true;
 
     /// <summary>
     ///     The toggle state of this action. Toggling switches the currently displayed icon, see <see cref="Icon"/> and <see cref="IconOn"/>.
@@ -67,14 +75,14 @@ public abstract partial class BaseActionComponent : Component
     ///     The toggle can set directly via <see cref="SharedActionsSystem.SetToggled"/>, but it will also be
     ///     automatically toggled for targeted-actions while selecting a target.
     /// </remarks>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public bool Toggled;
 
     /// <summary>
     ///     The current cooldown on the action.
     /// </summary>
-    // TODO serialization
-    public (TimeSpan Start, TimeSpan End)? Cooldown;
+    [DataField, AutoNetworkedField]
+    public ActionCooldown? Cooldown;
 
     /// <summary>
     ///     If true, the action will have an initial cooldown applied upon addition.
@@ -84,14 +92,15 @@ public abstract partial class BaseActionComponent : Component
     /// <summary>
     ///     Time interval between action uses.
     /// </summary>
-    [DataField("useDelay")] public TimeSpan? UseDelay;
+    [DataField, AutoNetworkedField]
+    public TimeSpan? UseDelay;
 
     /// <summary>
     /// The entity that contains this action. If the action is innate, this may be the user themselves.
     /// This should almost always be non-null.
     /// </summary>
     [Access(typeof(ActionContainerSystem), typeof(SharedActionsSystem))]
-    [DataField]
+    [DataField, AutoNetworkedField]
     public EntityUid? Container;
 
     /// <summary>
@@ -113,40 +122,45 @@ public abstract partial class BaseActionComponent : Component
         set => EntIcon = value;
     }
 
-    [DataField]
+    [DataField, AutoNetworkedField]
     public EntityUid? EntIcon;
 
     /// <summary>
     ///     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.
     /// </summary>
-    [DataField("checkCanInteract")] public bool CheckCanInteract = true;
+    [DataField, AutoNetworkedField]
+    public bool CheckCanInteract = true;
 
     /// <summary>
     /// Whether to check if the user is conscious or not. Can be used instead of <see cref="CheckCanInteract"/>
     /// for a more permissive check.
     /// </summary>
-    [DataField] public bool CheckConsciousness = true;
+    [DataField, AutoNetworkedField]
+    public bool CheckConsciousness = true;
 
     /// <summary>
     ///     If true, this will cause the action to only execute locally without ever notifying the server.
     /// </summary>
-    [DataField("clientExclusive")] public bool ClientExclusive = false;
+    [DataField, AutoNetworkedField]
+    public bool ClientExclusive;
 
     /// <summary>
     ///     Determines the order in which actions are automatically added the action bar.
     /// </summary>
-    [DataField("priority")] public int Priority = 0;
+    [DataField, AutoNetworkedField]
+    public int Priority = 0;
 
     /// <summary>
     ///     What entity, if any, currently has this action in the actions component?
     /// </summary>
-    [DataField] public EntityUid? AttachedEntity;
+    [DataField, AutoNetworkedField]
+    public EntityUid? AttachedEntity;
 
     /// <summary>
     ///     If true, this will cause the the action event to always be raised directed at the action performer/user instead of the action's container/provider.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public bool RaiseOnUser;
 
     /// <summary>
@@ -160,75 +174,34 @@ public abstract partial class BaseActionComponent : Component
     /// <summary>
     ///     Whether or not to automatically add this action to the action bar when it becomes available.
     /// </summary>
-    [DataField("autoPopulate")] public bool AutoPopulate = true;
+    [DataField, AutoNetworkedField]
+    public bool AutoPopulate = true;
 
     /// <summary>
     ///     Temporary actions are deleted when they get removed a <see cref="ActionsComponent"/>.
     /// </summary>
-    [DataField("temporary")] public bool Temporary;
+    [DataField, AutoNetworkedField]
+    public bool Temporary;
 
     /// <summary>
     ///     Determines the appearance of the entity-icon for actions that are enabled via some entity.
     /// </summary>
-    [DataField("itemIconStyle")] public ItemActionIconStyle ItemIconStyle;
+    [DataField, AutoNetworkedField]
+    public ItemActionIconStyle ItemIconStyle;
 
     /// <summary>
     ///     If not null, this sound will be played when performing this action.
     /// </summary>
-    [DataField("sound")] public SoundSpecifier? Sound;
+    [DataField, AutoNetworkedField]
+    public SoundSpecifier? Sound;
 }
 
-[Serializable, NetSerializable]
-public abstract class BaseActionComponentState : ComponentState
+[DataRecord, Serializable, NetSerializable]
+public record struct ActionCooldown
 {
-    public SpriteSpecifier? Icon;
-    public SpriteSpecifier? IconOn;
-    public Color IconColor;
-    public Color OriginalIconColor;
-    public Color DisabledIconColor;
-    public HashSet<string> Keywords;
-    public bool Enabled;
-    public bool Toggled;
-    public (TimeSpan Start, TimeSpan End)? Cooldown;
-    public TimeSpan? UseDelay;
-    public NetEntity? Container;
-    public NetEntity? EntityIcon;
-    public bool CheckCanInteract;
-    public bool CheckConsciousness;
-    public bool ClientExclusive;
-    public int Priority;
-    public NetEntity? AttachedEntity;
-    public bool RaiseOnUser;
-    public bool RaiseOnAction;
-    public bool AutoPopulate;
-    public bool Temporary;
-    public ItemActionIconStyle ItemIconStyle;
-    public SoundSpecifier? Sound;
+    [DataField(required: true, customTypeSerializer: typeof(TimeOffsetSerializer))]
+    public TimeSpan Start;
 
-    protected BaseActionComponentState(BaseActionComponent component, IEntityManager entManager)
-    {
-        Container = entManager.GetNetEntity(component.Container);
-        EntityIcon = entManager.GetNetEntity(component.EntIcon);
-        AttachedEntity = entManager.GetNetEntity(component.AttachedEntity);
-        RaiseOnUser = component.RaiseOnUser;
-        RaiseOnAction = component.RaiseOnAction;
-        Icon = component.Icon;
-        IconOn = component.IconOn;
-        IconColor = component.IconColor;
-        OriginalIconColor = component.OriginalIconColor;
-        DisabledIconColor = component.DisabledIconColor;
-        Keywords = component.Keywords;
-        Enabled = component.Enabled;
-        Toggled = component.Toggled;
-        Cooldown = component.Cooldown;
-        UseDelay = component.UseDelay;
-        CheckCanInteract = component.CheckCanInteract;
-        CheckConsciousness = component.CheckConsciousness;
-        ClientExclusive = component.ClientExclusive;
-        Priority = component.Priority;
-        AutoPopulate = component.AutoPopulate;
-        Temporary = component.Temporary;
-        ItemIconStyle = component.ItemIconStyle;
-        Sound = component.Sound;
-    }
+    [DataField(required: true, customTypeSerializer: typeof(TimeOffsetSerializer))]
+    public TimeSpan End;
 }
similarity index 66%
rename from Content.Shared/Actions/ActionContainerComponent.cs
rename to Content.Shared/Actions/Components/ActionContainerComponent.cs
index c18d1ead621305cfec0ecd523fcf7addb810b156..f7f812031693b1e14907363535bf1629a22a79be 100644 (file)
@@ -1,13 +1,13 @@
+using Content.Shared.Actions;
 using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
 
-namespace Content.Shared.Actions;
+namespace Content.Shared.Actions.Components;
 
 /// <summary>
 /// This component indicates that this entity contains actions inside of some container.
 /// </summary>
-[NetworkedComponent, RegisterComponent]
-[Access(typeof(ActionContainerSystem), typeof(SharedActionsSystem))]
+[NetworkedComponent, RegisterComponent, Access(typeof(ActionContainerSystem), typeof(SharedActionsSystem))]
 public sealed partial class ActionsContainerComponent : Component
 {
     public const string ContainerId = "actions";
similarity index 68%
rename from Content.Shared/Actions/ActionUpgradeComponent.cs
rename to Content.Shared/Actions/Components/ActionUpgradeComponent.cs
index d12ce339c863007a8066c37e44ee065269dd8c40..72a3447a831c30031fb751855a67417bb3f84808 100644 (file)
@@ -1,24 +1,29 @@
+using Content.Shared.Actions;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 
-namespace Content.Shared.Actions;
+namespace Content.Shared.Actions.Components;
 
-// For actions that can use basic upgrades
-// Not all actions should be upgradable
+/// <summary>
+/// For actions that can use basic upgrades
+/// Not all actions should be upgradable
+/// Requires <see cref="ActionComponent"/>.
+/// </summary>
 [RegisterComponent, NetworkedComponent, Access(typeof(ActionUpgradeSystem))]
+[EntityCategory("Actions")]
 public sealed partial class ActionUpgradeComponent : Component
 {
     /// <summary>
     ///     Current Level of the action.
     /// </summary>
-    [ViewVariables]
+    [DataField]
     public int Level = 1;
 
     /// <summary>
     ///     What level(s) effect this action?
     ///     You can skip levels, so you can have this entity change at level 2 but then won't change again until level 5.
     /// </summary>
-    [DataField("effectedLevels"), ViewVariables]
+    [DataField]
     public Dictionary<int, EntProtoId> EffectedLevels = new();
 
     // TODO: Branching level upgrades
similarity index 78%
rename from Content.Shared/Actions/ActionsComponent.cs
rename to Content.Shared/Actions/Components/ActionsComponent.cs
index a081a238671bac44f74c05c480063c8b06f68b19..f4c41261cd6b002351f810573d29c7d65eb8283b 100644 (file)
@@ -1,18 +1,21 @@
+using Content.Shared.Actions;
 using Robust.Shared.GameStates;
 using Robust.Shared.Serialization;
 
-namespace Content.Shared.Actions;
+namespace Content.Shared.Actions.Components;
 
-[NetworkedComponent]
-[RegisterComponent]
-[Access(typeof(SharedActionsSystem))]
+/// <summary>
+/// Lets the player controlling this entity use actions.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedActionsSystem))]
 public sealed partial class ActionsComponent : Component
 {
     /// <summary>
     /// List of actions currently granted to this entity.
     /// On the client, this may contain a mixture of client-side and networked entities.
     /// </summary>
-    [DataField] public HashSet<EntityUid> Actions = new();
+    [DataField]
+    public HashSet<EntityUid> Actions = new();
 }
 
 [Serializable, NetSerializable]
similarity index 90%
rename from Content.Shared/Actions/ConfirmableActionComponent.cs
rename to Content.Shared/Actions/Components/ConfirmableActionComponent.cs
index 6c208f47c6e10eb3847876f201839763b39184a1..a681062368a7e3fc21935a83130c3a0ab324d2c3 100644 (file)
@@ -1,15 +1,19 @@
+using Content.Shared.Actions;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
-namespace Content.Shared.Actions;
+namespace Content.Shared.Actions.Components;
 
 /// <summary>
 /// An action that must be confirmed before using it.
 /// Using it for the first time primes it, after a delay you can then confirm it.
 /// Used for dangerous actions that cannot be undone (unlike screaming).
+/// Requires <see cref="ActionComponent"/>.
 /// </summary>
 [RegisterComponent, NetworkedComponent, Access(typeof(ConfirmableActionSystem))]
 [AutoGenerateComponentState, AutoGenerateComponentPause]
+[EntityCategory("Actions")]
 public sealed partial class ConfirmableActionComponent : Component
 {
     /// <summary>
diff --git a/Content.Shared/Actions/Components/EntityTargetActionComponent.cs b/Content.Shared/Actions/Components/EntityTargetActionComponent.cs
new file mode 100644 (file)
index 0000000..d32205d
--- /dev/null
@@ -0,0 +1,48 @@
+using Content.Shared.Actions;
+using Content.Shared.Whitelist;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Actions.Components;
+
+/// <summary>
+/// Used on action entities to define an action that triggers when targeting an entity.
+/// If used with <see cref="WorldTargetActionComponent"/>, the event here can be set to null and <c>Optional</c> should be set.
+/// Then <see cref="WorldActionEvent"> can have <c>TargetEntity</c> optionally set to the client's hovered entity, if it is valid.
+/// Using entity-world targeting like this will always give coords, but doesn't need to have an entity.
+/// </summary>
+/// <remarks>
+/// Requires <see cref="TargetActionComponent"/>.
+/// </remarks>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedActionsSystem))]
+[EntityCategory("Actions")]
+[AutoGenerateComponentState]
+public sealed partial class EntityTargetActionComponent : Component
+{
+    /// <summary>
+    /// The local-event to raise when this action is performed.
+    /// If this is null entity-world targeting is done as specified on the component doc.
+    /// </summary>
+    [DataField, NonSerialized]
+    public EntityTargetActionEvent? Event;
+
+    /// <summary>
+    /// Determines which entities are valid targets for this action.
+    /// </summary>
+    /// <remarks>No whitelist check when null.</remarks>
+    [DataField, AutoNetworkedField]
+    public EntityWhitelist? Whitelist;
+
+    /// <summary>
+    /// Determines which entities cannot be valid targets for this action, even if matching the whitelist.
+    /// </summary>
+    /// <remarks>No blacklist check when null.</remarks>
+    [DataField, AutoNetworkedField]
+    public EntityWhitelist? Blacklist;
+
+    /// <summary>
+    /// Whether this action considers the user as a valid target entity when using this action.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool CanTargetSelf = true;
+}
diff --git a/Content.Shared/Actions/Components/InstantActionComponent.cs b/Content.Shared/Actions/Components/InstantActionComponent.cs
new file mode 100644 (file)
index 0000000..0634b31
--- /dev/null
@@ -0,0 +1,20 @@
+using Content.Shared.Actions;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Actions.Components;
+
+/// <summary>
+/// An action that raises an event as soon as it gets used.
+/// Requires <see cref="ActionComponent"/>.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedActionsSystem))]
+[EntityCategory("Actions")]
+public sealed partial class InstantActionComponent : Component
+{
+    /// <summary>
+    ///     The local-event to raise when this action is performed.
+    /// </summary>
+    [DataField(required: true), NonSerialized]
+    public InstantActionEvent? Event;
+}
similarity index 65%
rename from Content.Shared/Actions/BaseTargetActionComponent.cs
rename to Content.Shared/Actions/Components/TargetActionComponent.cs
index 7e40b10c327111a448f65754418069370d80e1f2..0cb9de49468a8d243fd35fb8ea91787bdc2dd744 100644 (file)
@@ -1,19 +1,30 @@
+using Content.Shared.Actions;
 using Content.Shared.Interaction;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 
-namespace Content.Shared.Actions;
+namespace Content.Shared.Actions.Components;
 
-public abstract partial class BaseTargetActionComponent : BaseActionComponent
+/// <summary>
+/// An action that targets an entity or map.
+/// Requires <see cref="ActionComponent"/>.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedActionsSystem))]
+[EntityCategory("Actions")]
+public sealed partial class TargetActionComponent : Component
 {
     /// <summary>
     ///     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.
     /// </summary>
-    [DataField("repeat")] public bool Repeat;
+    [DataField]
+    public bool Repeat;
 
     /// <summary>
     ///     For  entity- or map-targeting action, determines whether the action is deselected if the user doesn't click a valid target.
     /// </summary>
-    [DataField("deselectOnMiss")] public bool DeselectOnMiss;
+    [DataField]
+    public bool DeselectOnMiss;
 
     /// <summary>
     ///     Whether the action system should block this action if the user cannot actually access the target
@@ -23,9 +34,11 @@ public abstract partial class BaseTargetActionComponent : BaseActionComponent
     /// <remarks>
     ///     Even if this is false, the <see cref="Range"/> will still be checked.
     /// </remarks>
-    [DataField("checkCanAccess")] public bool CheckCanAccess = true;
+    [DataField]
+    public bool CheckCanAccess = true;
 
-    [DataField("range")] public float Range = SharedInteractionSystem.InteractionRange;
+    [DataField]
+    public float Range = SharedInteractionSystem.InteractionRange;
 
     /// <summary>
     ///     If the target is invalid, this bool determines whether the left-click will default to performing a standard-interaction
@@ -33,11 +46,13 @@ public abstract partial class BaseTargetActionComponent : BaseActionComponent
     /// <remarks>
     ///     Interactions will still be blocked if the target-validation generates a pop-up
     /// </remarks>
-    [DataField("interactOnMiss")] public bool InteractOnMiss = false;
+    [DataField]
+    public bool InteractOnMiss;
 
     /// <summary>
     ///     If true, and if <see cref="ShowHandItemOverlay"/> is enabled, then this action's icon will be drawn by that
     ///     over lay in place of the currently held item "held item".
     /// </summary>
-    [DataField("targetingIndicator")] public bool TargetingIndicator = true;
+    [DataField]
+    public bool TargetingIndicator = true;
 }
diff --git a/Content.Shared/Actions/Components/WorldTargetActionComponent.cs b/Content.Shared/Actions/Components/WorldTargetActionComponent.cs
new file mode 100644 (file)
index 0000000..6ec201d
--- /dev/null
@@ -0,0 +1,23 @@
+using Content.Shared.Actions;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Actions.Components;
+
+/// <summary>
+/// Used on action entities to define an action that triggers when targeting an entity coordinate.
+/// Can be combined with <see cref="EntityTargetActionComponent"/>, see its docs for more information.
+/// </summary>
+/// <remarks>
+/// Requires <see cref="TargetActionComponent"/>.
+/// </remarks>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedActionsSystem))]
+[EntityCategory("Actions")]
+public sealed partial class WorldTargetActionComponent : Component
+{
+    /// <summary>
+    ///     The local-event to raise when this action is performed.
+    /// </summary>
+    [DataField(required: true), NonSerialized]
+    public WorldTargetActionEvent? Event;
+}
index 26cc7111d2cb102586d9510557c8f2cf4299df11..eaffc47549adfa7465f137b6fa975ae248994fcd 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.Actions.Components;
 using Content.Shared.Actions.Events;
 using Content.Shared.Popups;
 using Robust.Shared.Timing;
diff --git a/Content.Shared/Actions/EntityTargetActionComponent.cs b/Content.Shared/Actions/EntityTargetActionComponent.cs
deleted file mode 100644 (file)
index aa8c0f4..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-using Content.Shared.Whitelist;
-using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Actions;
-
-/// <summary>
-/// Used on action entities to define an action that triggers when targeting an entity.
-/// </summary>
-[RegisterComponent, NetworkedComponent]
-public sealed partial class EntityTargetActionComponent : BaseTargetActionComponent
-{
-    public override BaseActionEvent? BaseEvent => Event;
-
-    /// <summary>
-    ///     The local-event to raise when this action is performed.
-    /// </summary>
-    [DataField("event")]
-    [NonSerialized]
-    public EntityTargetActionEvent? Event;
-
-    /// <summary>
-    /// Determines which entities are valid targets for this action.
-    /// </summary>
-    /// <remarks>No whitelist check when null.</remarks>
-    [DataField("whitelist")] public EntityWhitelist? Whitelist;
-
-    /// <summary>
-    /// Determines which entities are NOT valid targets for this action.
-    /// </summary>
-    /// <remarks>No blacklist check when null.</remarks>
-    [DataField] public EntityWhitelist? Blacklist;
-
-    /// <summary>
-    /// Whether this action considers the user as a valid target entity when using this action.
-    /// </summary>
-    [DataField("canTargetSelf")] public bool CanTargetSelf = true;
-}
-
-[Serializable, NetSerializable]
-public sealed class EntityTargetActionComponentState : BaseActionComponentState
-{
-    public EntityWhitelist? Whitelist;
-    public EntityWhitelist? Blacklist;
-    public bool CanTargetSelf;
-
-    public EntityTargetActionComponentState(EntityTargetActionComponent component, IEntityManager entManager) : base(component, entManager)
-    {
-        Whitelist = component.Whitelist;
-        Blacklist = component.Blacklist;
-        CanTargetSelf = component.CanTargetSelf;
-    }
-}
diff --git a/Content.Shared/Actions/EntityWorldTargetActionComponent.cs b/Content.Shared/Actions/EntityWorldTargetActionComponent.cs
deleted file mode 100644 (file)
index 3cfa60d..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-using Content.Shared.Whitelist;
-using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Actions;
-
-/// <summary>
-/// Used on action entities to define an action that triggers when targeting an entity or entity coordinates.
-/// </summary>
-[RegisterComponent, NetworkedComponent]
-public sealed partial class EntityWorldTargetActionComponent : BaseTargetActionComponent
-{
-    public override BaseActionEvent? BaseEvent => Event;
-
-    /// <summary>
-    ///     The local-event to raise when this action is performed.
-    /// </summary>
-    [DataField]
-    [NonSerialized]
-    public EntityWorldTargetActionEvent? Event;
-
-    /// <summary>
-    /// Determines which entities are valid targets for this action.
-    /// </summary>
-    /// <remarks>No whitelist check when null.</remarks>
-    [DataField] public EntityWhitelist? Whitelist;
-
-    /// <summary>
-    /// Whether this action considers the user as a valid target entity when using this action.
-    /// </summary>
-    [DataField] public bool CanTargetSelf = true;
-}
-
-[Serializable, NetSerializable]
-public sealed class EntityWorldTargetActionComponentState(
-    EntityWorldTargetActionComponent component,
-    IEntityManager entManager)
-    : BaseActionComponentState(component, entManager)
-{
-    public EntityWhitelist? Whitelist = component.Whitelist;
-    public bool CanTargetSelf = component.CanTargetSelf;
-}
index 26f23f9ec39068b526b61e615ba954784a2ed27d..98db50fd392348af81ae6449bac46d717c408f48 100644 (file)
@@ -2,7 +2,7 @@ namespace Content.Shared.Actions.Events;
 
 /// <summary>
 /// Raised before an action is used and can be cancelled to prevent it.
-/// Allowed to have side effects like modifying the action component.
+/// Allowed to have side effects like modifying the action components.
 /// </summary>
 [ByRefEvent]
 public record struct ActionAttemptEvent(EntityUid User, bool Cancelled = false);
diff --git a/Content.Shared/Actions/Events/ActionGetEventEvent.cs b/Content.Shared/Actions/Events/ActionGetEventEvent.cs
new file mode 100644 (file)
index 0000000..df231d3
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Shared.Actions.Events;
+
+/// <summary>
+/// Raised on an action entity to get its event.
+/// </summary>
+[ByRefEvent]
+public record struct ActionGetEventEvent(BaseActionEvent? Event = null);
diff --git a/Content.Shared/Actions/Events/ActionSetEventEvent.cs b/Content.Shared/Actions/Events/ActionSetEventEvent.cs
new file mode 100644 (file)
index 0000000..b33a548
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Content.Shared.Actions.Events;
+
+/// <summary>
+/// Raised on an action entity to have the event-holding component cast and set its event.
+/// If it was set successfully then <c>Handled</c> must be set to true.
+/// </summary>
+[ByRefEvent]
+public record struct ActionSetEventEvent(BaseActionEvent Event, bool Handled = false);
diff --git a/Content.Shared/Actions/Events/ActionSetTargetEvent.cs b/Content.Shared/Actions/Events/ActionSetTargetEvent.cs
new file mode 100644 (file)
index 0000000..9868c80
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Content.Shared.Actions.Events;
+
+/// <summary>
+/// Raised on an action entity to set its event's target to an entity, if it makes sense.
+/// Does nothing for an instant action as it has no target.
+/// </summary>
+[ByRefEvent]
+public record struct ActionSetTargetEvent(EntityUid Target, bool Handled = false);
diff --git a/Content.Shared/Actions/Events/ActionValidateEvent.cs b/Content.Shared/Actions/Events/ActionValidateEvent.cs
new file mode 100644 (file)
index 0000000..fa60a83
--- /dev/null
@@ -0,0 +1,32 @@
+namespace Content.Shared.Actions.Events;
+
+/// <summary>
+/// Raised on an action entity before being used to:
+/// 1. Make sure client is sending the correct kind of target (if any)
+/// 2. Do any validation on the target, if needed
+/// 3. Give the action system an event to raise on the performer, to actually do the action.
+/// </summary>
+[ByRefEvent]
+public struct ActionValidateEvent
+{
+    /// <summary>
+    /// Request event the client sent.
+    /// </summary>
+    public RequestPerformActionEvent Input;
+
+    /// <summary>
+    /// User trying to use the action.
+    /// </summary>
+    public EntityUid User;
+
+    /// <summary>
+    /// Entity providing this action to the user, used for logging.
+    /// </summary>
+    public EntityUid Provider;
+
+    /// <summary>
+    /// If set to true, the client sent invalid event data and this should be logged as an error.
+    /// For functioning input that happens to not be allowed this should not be set, for example a range check.
+    /// </summary>
+    public bool Invalid;
+}
diff --git a/Content.Shared/Actions/Events/GetActionDataEvent.cs b/Content.Shared/Actions/Events/GetActionDataEvent.cs
deleted file mode 100644 (file)
index be77cfd..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace Content.Shared.Actions.Events;
-
-[ByRefEvent]
-public record struct GetActionDataEvent(BaseActionComponent? Action);
diff --git a/Content.Shared/Actions/Events/ValidateActionEntityTargetEvent.cs b/Content.Shared/Actions/Events/ValidateActionEntityTargetEvent.cs
deleted file mode 100644 (file)
index 9f22e79..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace Content.Shared.Actions.Events;
-
-[ByRefEvent]
-public record struct ValidateActionEntityTargetEvent(EntityUid User, EntityUid Target, bool Cancelled = false);
diff --git a/Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs b/Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs
deleted file mode 100644 (file)
index 57c4702..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-using Robust.Shared.Map;
-
-namespace Content.Shared.Actions.Events;
-
-[ByRefEvent]
-public record struct ValidateActionEntityWorldTargetEvent(
-    EntityUid User,
-    EntityUid? Target,
-    EntityCoordinates? Coords,
-    bool Cancelled = false);
diff --git a/Content.Shared/Actions/Events/ValidateActionWorldTargetEvent.cs b/Content.Shared/Actions/Events/ValidateActionWorldTargetEvent.cs
deleted file mode 100644 (file)
index 43e398a..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-using Robust.Shared.Map;
-
-namespace Content.Shared.Actions.Events;
-
-[ByRefEvent]
-public record struct ValidateActionWorldTargetEvent(EntityUid User, EntityCoordinates Target, bool Cancelled = false);
diff --git a/Content.Shared/Actions/InstantActionComponent.cs b/Content.Shared/Actions/InstantActionComponent.cs
deleted file mode 100644 (file)
index 04c9b94..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Actions;
-
-[RegisterComponent, NetworkedComponent]
-public sealed partial class InstantActionComponent : BaseActionComponent
-{
-    public override BaseActionEvent? BaseEvent => Event;
-
-    /// <summary>
-    ///     The local-event to raise when this action is performed.
-    /// </summary>
-    [DataField("event")]
-    [NonSerialized]
-    public InstantActionEvent? Event;
-}
-
-[Serializable, NetSerializable]
-public sealed class InstantActionComponentState : BaseActionComponentState
-{
-    public InstantActionComponentState(InstantActionComponent component, IEntityManager entManager) : base(component, entManager)
-    {
-    }
-}
index 174f2c7881adf023aced6d5374e97fd30081252a..2a7bc39b907373e08d374ba146c5c0d0f16f7536 100644 (file)
@@ -1,6 +1,7 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Content.Shared.ActionBlocker;
+using Content.Shared.Actions.Components;
 using Content.Shared.Actions.Events;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Database;
@@ -22,27 +23,29 @@ public abstract class SharedActionsSystem : EntitySystem
 {
     [Dependency] protected readonly IGameTiming GameTiming = default!;
     [Dependency] private   readonly ISharedAdminLogManager _adminLogger = default!;
-    [Dependency] private   readonly ActionBlockerSystem _actionBlockerSystem = default!;
+    [Dependency] private   readonly ActionBlockerSystem _actionBlocker = default!;
     [Dependency] private   readonly ActionContainerSystem _actionContainer = default!;
-    [Dependency] private   readonly EntityWhitelistSystem _whitelistSystem = default!;
-    [Dependency] private   readonly RotateToFaceSystem _rotateToFaceSystem = default!;
+    [Dependency] private   readonly EntityWhitelistSystem _whitelist = default!;
+    [Dependency] private   readonly RotateToFaceSystem _rotateToFace = default!;
     [Dependency] private   readonly SharedAudioSystem _audio = default!;
-    [Dependency] private   readonly SharedInteractionSystem _interactionSystem = default!;
-    [Dependency] private   readonly SharedTransformSystem _transformSystem = default!;
+    [Dependency] private   readonly SharedInteractionSystem _interaction = default!;
+    [Dependency] private   readonly SharedTransformSystem _transform = default!;
+
+    private EntityQuery<ActionComponent> _actionQuery;
+    private EntityQuery<ActionsComponent> _actionsQuery;
+    private EntityQuery<MindComponent> _mindQuery;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<InstantActionComponent, MapInitEvent>(OnActionMapInit);
-        SubscribeLocalEvent<EntityTargetActionComponent, MapInitEvent>(OnActionMapInit);
-        SubscribeLocalEvent<WorldTargetActionComponent, MapInitEvent>(OnActionMapInit);
-        SubscribeLocalEvent<EntityWorldTargetActionComponent, MapInitEvent>(OnActionMapInit);
+        _actionQuery = GetEntityQuery<ActionComponent>();
+        _actionsQuery = GetEntityQuery<ActionsComponent>();
+        _mindQuery = GetEntityQuery<MindComponent>();
+
+        SubscribeLocalEvent<ActionComponent, MapInitEvent>(OnActionMapInit);
 
-        SubscribeLocalEvent<InstantActionComponent, ComponentShutdown>(OnActionShutdown);
-        SubscribeLocalEvent<EntityTargetActionComponent, ComponentShutdown>(OnActionShutdown);
-        SubscribeLocalEvent<WorldTargetActionComponent, ComponentShutdown>(OnActionShutdown);
-        SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentShutdown>(OnActionShutdown);
+        SubscribeLocalEvent<ActionComponent, ComponentShutdown>(OnActionShutdown);
 
         SubscribeLocalEvent<ActionsComponent, ActionComponentChangeEvent>(OnActionCompChange);
         SubscribeLocalEvent<ActionsComponent, RelayedActionComponentChangeEvent>(OnRelayActionCompChange);
@@ -53,230 +56,198 @@ public abstract class SharedActionsSystem : EntitySystem
         SubscribeLocalEvent<ActionsComponent, RejuvenateEvent>(OnRejuventate);
 
         SubscribeLocalEvent<ActionsComponent, ComponentShutdown>(OnShutdown);
+        SubscribeLocalEvent<ActionsComponent, ComponentGetState>(OnGetState);
+
+        SubscribeLocalEvent<InstantActionComponent, ActionValidateEvent>(OnInstantValidate);
+        SubscribeLocalEvent<EntityTargetActionComponent, ActionValidateEvent>(OnEntityValidate);
+        SubscribeLocalEvent<WorldTargetActionComponent, ActionValidateEvent>(OnWorldValidate);
 
-        SubscribeLocalEvent<ActionsComponent, ComponentGetState>(OnActionsGetState);
+        SubscribeLocalEvent<InstantActionComponent, ActionGetEventEvent>(OnInstantGetEvent);
+        SubscribeLocalEvent<EntityTargetActionComponent, ActionGetEventEvent>(OnEntityGetEvent);
+        SubscribeLocalEvent<WorldTargetActionComponent, ActionGetEventEvent>(OnWorldGetEvent);
 
-        SubscribeLocalEvent<InstantActionComponent, ComponentGetState>(OnInstantGetState);
-        SubscribeLocalEvent<EntityTargetActionComponent, ComponentGetState>(OnEntityTargetGetState);
-        SubscribeLocalEvent<WorldTargetActionComponent, ComponentGetState>(OnWorldTargetGetState);
-        SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentGetState>(OnEntityWorldTargetGetState);
+        SubscribeLocalEvent<InstantActionComponent, ActionSetEventEvent>(OnInstantSetEvent);
+        SubscribeLocalEvent<EntityTargetActionComponent, ActionSetEventEvent>(OnEntitySetEvent);
+        SubscribeLocalEvent<WorldTargetActionComponent, ActionSetEventEvent>(OnWorldSetEvent);
 
-        SubscribeLocalEvent<InstantActionComponent, GetActionDataEvent>(OnGetActionData);
-        SubscribeLocalEvent<EntityTargetActionComponent, GetActionDataEvent>(OnGetActionData);
-        SubscribeLocalEvent<WorldTargetActionComponent, GetActionDataEvent>(OnGetActionData);
-        SubscribeLocalEvent<EntityWorldTargetActionComponent, GetActionDataEvent>(OnGetActionData);
+        SubscribeLocalEvent<EntityTargetActionComponent, ActionSetTargetEvent>(OnEntitySetTarget);
+        SubscribeLocalEvent<WorldTargetActionComponent, ActionSetTargetEvent>(OnWorldSetTarget);
 
         SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest);
     }
 
-    private void OnActionMapInit(EntityUid uid, BaseActionComponent component, MapInitEvent args)
+    private void OnActionMapInit(Entity<ActionComponent> ent, ref MapInitEvent args)
     {
-        component.OriginalIconColor = component.IconColor;
+        var comp = ent.Comp;
+        comp.OriginalIconColor = comp.IconColor;
+        DirtyField(ent, ent.Comp, nameof(ActionComponent.OriginalIconColor));
     }
 
-    private void OnActionShutdown(EntityUid uid, BaseActionComponent component, ComponentShutdown args)
+    private void OnActionShutdown(Entity<ActionComponent> ent, ref ComponentShutdown args)
     {
-        if (component.AttachedEntity != null && !TerminatingOrDeleted(component.AttachedEntity.Value))
-            RemoveAction(component.AttachedEntity.Value, uid, action: component);
+        if (ent.Comp.AttachedEntity is {} user && !TerminatingOrDeleted(user))
+            RemoveAction(user, (ent, ent));
     }
 
-    private void OnShutdown(EntityUid uid, ActionsComponent component, ComponentShutdown args)
+    private void OnShutdown(Entity<ActionsComponent> ent, ref ComponentShutdown args)
     {
-        foreach (var act in component.Actions)
+        foreach (var actionId in ent.Comp.Actions)
         {
-            RemoveAction(uid, act, component);
+            RemoveAction((ent, ent), actionId);
         }
     }
 
-    private void OnInstantGetState(EntityUid uid, InstantActionComponent component, ref ComponentGetState args)
-    {
-        args.State = new InstantActionComponentState(component, EntityManager);
-    }
-
-    private void OnEntityTargetGetState(EntityUid uid, EntityTargetActionComponent component, ref ComponentGetState args)
+    private void OnGetState(Entity<ActionsComponent> ent, ref ComponentGetState args)
     {
-        args.State = new EntityTargetActionComponentState(component, EntityManager);
+        args.State = new ActionsComponentState(GetNetEntitySet(ent.Comp.Actions));
     }
 
-    private void OnWorldTargetGetState(EntityUid uid, WorldTargetActionComponent component, ref ComponentGetState args)
+    /// <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)
     {
-        args.State = new WorldTargetActionComponentState(component, EntityManager);
-    }
+        if (action is not {} ent || TerminatingOrDeleted(ent))
+            return null;
 
-    private void OnEntityWorldTargetGetState(EntityUid uid, EntityWorldTargetActionComponent component, ref ComponentGetState args)
-    {
-        args.State = new EntityWorldTargetActionComponentState(component, EntityManager);
-    }
+        if (!_actionQuery.Resolve(ent, ref ent.Comp, logError))
+            return null;
 
-    private void OnGetActionData<T>(EntityUid uid, T component, ref GetActionDataEvent args) where T : BaseActionComponent
-    {
-        args.Action = component;
+        return (ent, ent.Comp);
     }
 
-    public bool TryGetActionData(
-        [NotNullWhen(true)] EntityUid? uid,
-        [NotNullWhen(true)] out BaseActionComponent? result,
-        bool logError = true)
+    public void SetCooldown(Entity<ActionComponent?>? action, TimeSpan start, TimeSpan end)
     {
-        result = null;
-        if (uid == null || TerminatingOrDeleted(uid.Value))
-            return false;
-
-        var ev = new GetActionDataEvent();
-        RaiseLocalEvent(uid.Value, ref ev);
-        result = ev.Action;
-
-        if (result != null)
-            return true;
-
-        if (logError)
-            Log.Error($"Failed to get action from action entity: {ToPrettyString(uid.Value)}. Trace: {Environment.StackTrace}");
-
-        return false;
-    }
+        if (GetAction(action) is not {} ent)
+            return;
 
-    public bool ResolveActionData(
-        [NotNullWhen(true)] EntityUid? uid,
-        [NotNullWhen(true)] ref BaseActionComponent? result,
-        bool logError = true)
-    {
-        if (result != null)
+        ent.Comp.Cooldown = new ActionCooldown
         {
-            DebugTools.AssertOwner(uid, result);
-            return true;
-        }
-
-        return TryGetActionData(uid, out result, logError);
+            Start = start,
+            End = end
+        };
+        DirtyField(ent, ent.Comp, nameof(ActionComponent.Cooldown));
     }
 
-    public void SetCooldown(EntityUid? actionId, TimeSpan start, TimeSpan end)
+    public void RemoveCooldown(Entity<ActionComponent?>? action)
     {
-        if (!TryGetActionData(actionId, out var action))
+        if (GetAction(action) is not {} ent)
             return;
 
-        action.Cooldown = (start, end);
-        Dirty(actionId.Value, action);
+        ent.Comp.Cooldown = null;
+        DirtyField(ent, ent.Comp, nameof(ActionComponent.Cooldown));
     }
 
-    public void SetCooldown(EntityUid? actionId, TimeSpan 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(actionId, start, start + cooldown);
+        SetCooldown(action, start, start + cooldown);
     }
 
-    public void ClearCooldown(EntityUid? actionId)
+    public void ClearCooldown(Entity<ActionComponent?>? action)
     {
-        if (!TryGetActionData(actionId, out var action))
+        if (GetAction(action) is not {} ent)
             return;
 
-        if (action.Cooldown is not { } cooldown)
+        if (ent.Comp.Cooldown is not {} cooldown)
             return;
 
-        action.Cooldown = (cooldown.Start, GameTiming.CurTime);
-        Dirty(actionId.Value, action);
+        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(EntityUid? actionId, TimeSpan? cooldown)
+    public void SetIfBiggerCooldown(Entity<ActionComponent?>? action, TimeSpan cooldown)
     {
-        if (cooldown == null ||
-            cooldown.Value <= TimeSpan.Zero ||
-            !TryGetActionData(actionId, out var action))
-        {
+        if (GetAction(action) is not {} ent || cooldown < TimeSpan.Zero)
             return;
-        }
 
         var start = GameTiming.CurTime;
         var end = start + cooldown;
-        if (action.Cooldown?.End > end)
+        if (ent.Comp.Cooldown?.End > end)
             return;
 
-        action.Cooldown = (start, end.Value);
-        Dirty(actionId.Value, action);
+        SetCooldown((ent, ent), start, end);
     }
 
-    public void StartUseDelay(EntityUid? actionId)
+    /// <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 (actionId == null)
+        if (GetAction(action) is not {} ent || ent.Comp.UseDelay is not {} delay)
             return;
 
-        if (!TryGetActionData(actionId, out var action) || action.UseDelay == null)
-            return;
-
-        action.Cooldown = (GameTiming.CurTime, GameTiming.CurTime + action.UseDelay.Value);
-        Dirty(actionId.Value, action);
+        SetCooldown((ent, ent), delay);
     }
 
-    public void SetUseDelay(EntityUid? actionId, TimeSpan? delay)
+    public void SetUseDelay(Entity<ActionComponent?>? action, TimeSpan? delay)
     {
-        if (!TryGetActionData(actionId, out var action) || action.UseDelay == delay)
+        if (GetAction(action) is not {} ent || ent.Comp.UseDelay == delay)
             return;
 
-        action.UseDelay = delay;
-        UpdateAction(actionId, action);
-        Dirty(actionId.Value, action);
+        ent.Comp.UseDelay = delay;
+        UpdateAction(ent);
+        DirtyField(ent, ent.Comp, nameof(ActionComponent.UseDelay));
     }
 
-    public void ReduceUseDelay(EntityUid? actionId, TimeSpan? lowerDelay)
+    public void ReduceUseDelay(Entity<ActionComponent?>? action, TimeSpan? lowerDelay)
     {
-        if (!TryGetActionData(actionId, out var action))
+        if (GetAction(action) is not {} ent)
             return;
 
-        if (action.UseDelay != null && lowerDelay != null)
-            action.UseDelay = action.UseDelay - lowerDelay;
+        if (ent.Comp.UseDelay != null && lowerDelay != null)
+            ent.Comp.UseDelay -= lowerDelay;
 
-        if (action.UseDelay < TimeSpan.Zero)
-            action.UseDelay = null;
+        if (ent.Comp.UseDelay < TimeSpan.Zero)
+            ent.Comp.UseDelay = null;
 
-        UpdateAction(actionId, action);
-        Dirty(actionId.Value, action);
+        UpdateAction(ent);
+        DirtyField(ent, ent.Comp, nameof(ActionComponent.UseDelay));
     }
 
-    private void OnRejuventate(EntityUid uid, ActionsComponent component, RejuvenateEvent args)
+    private void OnRejuventate(Entity<ActionsComponent> ent, ref RejuvenateEvent args)
     {
-        foreach (var act in component.Actions)
+        foreach (var act in ent.Comp.Actions)
         {
             ClearCooldown(act);
         }
     }
 
     #region ComponentStateManagement
-    public virtual void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null)
+    public virtual void UpdateAction(Entity<ActionComponent> ent)
     {
         // See client-side code.
     }
 
-    public void SetToggled(EntityUid? actionId, bool toggled)
+    public void SetToggled(Entity<ActionComponent?>? action, bool toggled)
     {
-        if (!TryGetActionData(actionId, out var action) ||
-            action.Toggled == toggled)
-        {
+        if (GetAction(action) is not {} ent || ent.Comp.Toggled == toggled)
             return;
-        }
 
-        action.Toggled = toggled;
-        UpdateAction(actionId, action);
-        Dirty(actionId.Value, action);
+        ent.Comp.Toggled = toggled;
+        UpdateAction(ent);
+        DirtyField(ent, ent.Comp, nameof(ActionComponent.Toggled));
     }
 
-    public void SetEnabled(EntityUid? actionId, bool enabled)
+    public void SetEnabled(Entity<ActionComponent?>? action, bool enabled)
     {
-        if (!TryGetActionData(actionId, out var action) ||
-            action.Enabled == enabled)
-        {
+        if (GetAction(action) is not {} ent || ent.Comp.Enabled == enabled)
             return;
-        }
 
-        action.Enabled = enabled;
-        UpdateAction(actionId, action);
-        Dirty(actionId.Value, action);
-    }
-
-    private void OnActionsGetState(EntityUid uid, ActionsComponent component, ref ComponentGetState args)
-    {
-        args.State = new ActionsComponentState(GetNetEntitySet(component.Actions));
+        ent.Comp.Enabled = enabled;
+        UpdateAction(ent);
+        DirtyField(ent, ent.Comp, nameof(ActionComponent.Enabled));
     }
 
     #endregion
@@ -291,7 +262,7 @@ public abstract class SharedActionsSystem : EntitySystem
         if (args.SenderSession.AttachedEntity is not { } user)
             return;
 
-        if (!TryComp(user, out ActionsComponent? component))
+        if (!_actionsQuery.TryComp(user, out var component))
             return;
 
         var actionEnt = GetEntity(ev.Action);
@@ -309,11 +280,11 @@ public abstract class SharedActionsSystem : EntitySystem
             return;
         }
 
-        if (!TryGetActionData(actionEnt, out var action))
+        if (GetAction(actionEnt) is not {} action)
             return;
 
-        DebugTools.Assert(action.AttachedEntity == user);
-        if (!action.Enabled)
+        DebugTools.Assert(action.Comp.AttachedEntity == user);
+        if (!action.Comp.Enabled)
             return;
 
         var curTime = GameTiming.CurTime;
@@ -323,298 +294,292 @@ public abstract class SharedActionsSystem : EntitySystem
         // check for action use prevention
         // TODO: make code below use this event with a dedicated component
         var attemptEv = new ActionAttemptEvent(user);
-        RaiseLocalEvent(actionEnt, ref attemptEv);
+        RaiseLocalEvent(action, ref attemptEv);
         if (attemptEv.Cancelled)
             return;
 
-        BaseActionEvent? performEvent = null;
+        // 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);
+    }
 
-        if (action.CheckConsciousness && !_actionBlockerSystem.CanConsciouslyPerformAction(user))
+    private void OnValidate(Entity<ActionComponent> ent, ref ActionValidateEvent args)
+    {
+        if (ent.Comp.CheckConsciousness && !_actionBlocker.CanConsciouslyPerformAction(args.User))
+        {
+            args.Invalid = true;
             return;
+        }
 
-        // Validate request by checking action blockers and the like:
-        switch (action)
+        if (ent.Comp.CheckCanInteract && !_actionBlocker.CanInteract(args.User, null))
         {
-            case EntityTargetActionComponent entityAction:
-                if (ev.EntityTarget is not { Valid: true } netTarget)
-                {
-                    Log.Error($"Attempted to perform an entity-targeted action without a target! Action: {name}");
-                    return;
-                }
-
-                var entityTarget = GetEntity(netTarget);
-
-                var targetWorldPos = _transformSystem.GetWorldPosition(entityTarget);
-                _rotateToFaceSystem.TryFaceCoordinates(user, targetWorldPos);
-
-                if (!ValidateEntityTarget(user, entityTarget, (actionEnt, entityAction)))
-                    return;
-
-                _adminLogger.Add(LogType.Action,
-                    $"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(action.Container ?? user):provider}) targeted at {ToPrettyString(entityTarget):target}.");
-
-                if (entityAction.Event != null)
-                {
-                    entityAction.Event.Target = entityTarget;
-                    Dirty(actionEnt, entityAction);
-                    performEvent = entityAction.Event;
-                }
-
-                break;
-            case WorldTargetActionComponent worldAction:
-                if (ev.EntityCoordinatesTarget is not { } netCoordinatesTarget)
-                {
-                    Log.Error($"Attempted to perform a world-targeted action without a target! Action: {name}");
-                    return;
-                }
-
-                var entityCoordinatesTarget = GetCoordinates(netCoordinatesTarget);
-                _rotateToFaceSystem.TryFaceCoordinates(user, _transformSystem.ToMapCoordinates(entityCoordinatesTarget).Position);
-
-                if (!ValidateWorldTarget(user, entityCoordinatesTarget, (actionEnt, worldAction)))
-                    return;
-
-                _adminLogger.Add(LogType.Action,
-                    $"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(action.Container ?? user):provider}) targeted at {entityCoordinatesTarget:target}.");
-
-                if (worldAction.Event != null)
-                {
-                    worldAction.Event.Target = entityCoordinatesTarget;
-                    Dirty(actionEnt, worldAction);
-                    performEvent = worldAction.Event;
-                }
-
-                break;
-            case EntityWorldTargetActionComponent entityWorldAction:
-            {
-                var actionEntity = GetEntity(ev.EntityTarget);
-                var actionCoords = GetCoordinates(ev.EntityCoordinatesTarget);
-
-                if (actionEntity is null && actionCoords is null)
-                {
-                    Log.Error($"Attempted to perform an entity-world-targeted action without an entity or world coordinates! Action: {name}");
-                    return;
-                }
-
-                var entWorldAction = new Entity<EntityWorldTargetActionComponent>(actionEnt, entityWorldAction);
-
-                if (!ValidateEntityWorldTarget(user, actionEntity, actionCoords, entWorldAction))
-                    return;
-
-                _adminLogger.Add(LogType.Action,
-                    $"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(action.Container ?? user):provider}) targeted at {ToPrettyString(actionEntity):target} {actionCoords:target}.");
-
-                if (entityWorldAction.Event != null)
-                {
-                    entityWorldAction.Event.Entity = actionEntity;
-                    entityWorldAction.Event.Coords = actionCoords;
-                    Dirty(actionEnt, entityWorldAction);
-                    performEvent = entityWorldAction.Event;
-                }
-                break;
-            }
-            case InstantActionComponent instantAction:
-                if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
-                    return;
-
-                _adminLogger.Add(LogType.Action,
-                    $"{ToPrettyString(user):user} is performing the {name:action} action provided by {ToPrettyString(action.Container ?? user):provider}.");
-
-                performEvent = instantAction.Event;
-                break;
+            args.Invalid = true;
+            return;
         }
 
-        // All checks passed. Perform the action!
-        PerformAction(user, component, actionEnt, action, performEvent, curTime);
+        // Event is not set here, only below
     }
 
-    public bool ValidateEntityTarget(EntityUid user, EntityUid target, Entity<EntityTargetActionComponent> actionEnt)
+    private void OnInstantValidate(Entity<InstantActionComponent> ent, ref ActionValidateEvent args)
     {
-        var comp = actionEnt.Comp;
-        if (!ValidateEntityTargetBase(user,
-                target,
-                comp.Whitelist,
-                comp.Blacklist,
-                comp.CheckCanInteract,
-                comp.CanTargetSelf,
-                comp.CheckCanAccess,
-                comp.Range))
-            return false;
+        _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);
+        _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}.");
 
-        var ev = new ValidateActionEntityTargetEvent(user, target);
-        RaiseLocalEvent(actionEnt, ref ev);
-        return !ev.Cancelled;
+        ev.Target = target;
     }
 
-    private bool ValidateEntityTargetBase(EntityUid user,
-        EntityUid? targetEntity,
-        EntityWhitelist? whitelist,
-        EntityWhitelist? blacklist,
-        bool checkCanInteract,
-        bool canTargetSelf,
-        bool checkCanAccess,
-        float range)
+    private void OnWorldValidate(Entity<WorldTargetActionComponent> ent, ref ActionValidateEvent args)
     {
-        if (targetEntity is not { } target || !target.IsValid() || Deleted(target))
+        if (args.Input.EntityCoordinatesTarget is not { } netTarget)
+        {
+            args.Invalid = true;
+            return;
+        }
+
+        var user = args.User;
+        var target = GetCoordinates(netTarget);
+        _rotateToFace.TryFaceCoordinates(user, target.ToMapPos(EntityManager, _transform));
+
+        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 (_whitelistSystem.IsWhitelistFail(whitelist, target))
+        if (_whitelist.IsWhitelistFail(comp.Whitelist, target))
             return false;
 
-        if (_whitelistSystem.IsBlacklistPass(blacklist, target))
+        if (_whitelist.IsBlacklistPass(comp.Blacklist, target))
             return false;
 
-        if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target))
+        if (_actionQuery.Comp(uid).CheckCanInteract && !_actionBlocker.CanInteract(user, target))
             return false;
 
         if (user == target)
-            return canTargetSelf;
+            return comp.CanTargetSelf;
 
-        if (!checkCanAccess)
+        var targetAction = Comp<TargetActionComponent>(uid);
+        var coords = Transform(target).Coordinates;
+        if (!ValidateBaseTarget(user, coords, (uid, targetAction)))
         {
-            // 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 (range <= 0)
-                return true;
-
-            var distance = (_transformSystem.GetWorldPosition(xform) - _transformSystem.GetWorldPosition(targetXform)).Length();
-            return distance <= range;
+            // if not just checking pure range, let stored entities be targeted by actions
+            // if it's out of range it probably isn't stored anyway...
+            return targetAction.CheckCanAccess && _interaction.CanAccessViaStorage(user, target);
         }
 
-        return _interactionSystem.InRangeAndAccessible(user, target, range: range);
+        return _interaction.InRangeAndAccessible(user, target, range: targetAction.Range);
     }
 
-    public bool ValidateWorldTarget(EntityUid user, EntityCoordinates coords, Entity<WorldTargetActionComponent> action)
+    public bool ValidateWorldTarget(EntityUid user, EntityCoordinates target, Entity<WorldTargetActionComponent> ent)
     {
-        var comp = action.Comp;
-        if (!ValidateWorldTargetBase(user, coords, comp.CheckCanInteract, comp.CheckCanAccess, comp.Range))
-            return false;
-
-        var ev = new ValidateActionWorldTargetEvent(user, coords);
-        RaiseLocalEvent(action, ref ev);
-        return !ev.Cancelled;
+        var targetAction = Comp<TargetActionComponent>(ent);
+        return ValidateBaseTarget(user, target, (ent, targetAction));
     }
 
-    private bool ValidateWorldTargetBase(EntityUid user,
-        EntityCoordinates? entityCoordinates,
-        bool checkCanInteract,
-        bool checkCanAccess,
-        float range)
+    private bool ValidateBaseTarget(EntityUid user, EntityCoordinates coords, Entity<TargetActionComponent> ent)
     {
-        if (entityCoordinates is not { } coords)
-            return false;
+        var (uid, comp) = ent;
+        if (comp.CheckCanAccess)
+            return _interaction.InRangeUnobstructed(user, coords, range: comp.Range);
 
-        if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, null))
+        // 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 (!checkCanAccess)
-        {
-            // even if we don't check for obstructions, we may still need to check the range.
-            var xform = Transform(user);
+        if (comp.Range <= 0)
+            return true;
 
-            if (xform.MapID != _transformSystem.GetMapId(coords))
-                return false;
+        return _transform.InRange(coords, xform.Coordinates, comp.Range);
+    }
 
-            if (range <= 0)
-                return true;
-            return _transformSystem.InRange(coords, xform.Coordinates, range);
-        }
+    private void OnInstantGetEvent(Entity<InstantActionComponent> ent, ref ActionGetEventEvent args)
+    {
+        if (ent.Comp.Event is {} ev)
+            args.Event = ev;
+    }
 
-        return _interactionSystem.InRangeUnobstructed(user, coords, range: range);
+    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;
     }
 
-    public bool ValidateEntityWorldTarget(EntityUid user,
-        EntityUid? entity,
-        EntityCoordinates? coords,
-        Entity<EntityWorldTargetActionComponent> action)
+    private void OnInstantSetEvent(Entity<InstantActionComponent> ent, ref ActionSetEventEvent args)
     {
-        var comp = action.Comp;
-        var entityValidated = ValidateEntityTargetBase(user,
-            entity,
-            comp.Whitelist,
-            null,
-            comp.CheckCanInteract,
-            comp.CanTargetSelf,
-            comp.CheckCanAccess,
-            comp.Range);
+        if (args.Event is InstantActionEvent ev)
+        {
+            ent.Comp.Event = ev;
+            args.Handled = true;
+        }
+    }
 
-        var worldValidated
-            = ValidateWorldTargetBase(user, coords, comp.CheckCanInteract, comp.CheckCanAccess, comp.Range);
+    private void OnEntitySetEvent(Entity<EntityTargetActionComponent> ent, ref ActionSetEventEvent args)
+    {
+        if (args.Event is EntityTargetActionEvent ev)
+        {
+            ent.Comp.Event = ev;
+            args.Handled = true;
+        }
+    }
 
-        if (!entityValidated && !worldValidated)
-            return false;
+    private void OnWorldSetEvent(Entity<WorldTargetActionComponent> ent, ref ActionSetEventEvent args)
+    {
+        if (args.Event is WorldTargetActionEvent ev)
+        {
+            ent.Comp.Event = ev;
+            args.Handled = true;
+        }
+    }
 
-        var ev = new ValidateActionEntityWorldTargetEvent(user,
-            entityValidated ? entity : null,
-            worldValidated ? coords : null);
-        RaiseLocalEvent(action, ref ev);
-        return !ev.Cancelled;
+    private void OnEntitySetTarget(Entity<EntityTargetActionComponent> ent, ref ActionSetTargetEvent args)
+    {
+        if (ent.Comp.Event is {} ev)
+        {
+            ev.Target = args.Target;
+            args.Handled = true;
+        }
     }
 
-    public void PerformAction(EntityUid performer, ActionsComponent? component, EntityUid actionId, BaseActionComponent action, BaseActionEvent? actionEvent, TimeSpan curTime, bool predicted = 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.Toggled;
+        var toggledBefore = action.Comp.Toggled;
 
         // Note that attached entity and attached container are allowed to be null here.
-        if (action.AttachedEntity != null && action.AttachedEntity != performer)
+        if (action.Comp.AttachedEntity != null && action.Comp.AttachedEntity != performer)
         {
-            Log.Error($"{ToPrettyString(performer)} is attempting to perform an action {ToPrettyString(actionId)} that is attached to another entity {ToPrettyString(action.AttachedEntity.Value)}");
+            Log.Error($"{ToPrettyString(performer)} is attempting to perform an action {ToPrettyString(action)} that is attached to another entity {ToPrettyString(action.Comp.AttachedEntity)}");
             return;
         }
 
-        if (actionEvent != null)
-        {
-            // This here is required because of client-side prediction (RaisePredictiveEvent results in event re-use).
-            actionEvent.Handled = false;
-            var target = performer;
-            actionEvent.Performer = performer;
-            actionEvent.Action = (actionId, action);
+        actionEvent ??= GetEvent(action);
 
-            if (!action.RaiseOnUser && action.Container != null && !HasComp<MindComponent>(action.Container))
-                target = action.Container.Value;
+        if (actionEvent is not {} ev)
+            return;
 
-            if (action.RaiseOnAction)
-                target = actionId;
+        ev.Performer = performer;
 
-            RaiseLocalEvent(target, (object) actionEvent, broadcast: true);
-            handled = actionEvent.Handled;
-        }
+        // 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, and mark as dirty (if required).
-        if (actionEvent?.Toggle == true)
-        {
-            action.Toggled = !action.Toggled;
-        }
-
-        _audio.PlayPredicted(action.Sound, performer, predicted ? performer : null);
+        // play sound, reduce charges, start cooldown
+        if (ev?.Toggle == true)
+            SetToggled((action, action), !action.Comp.Toggled);
 
-        var dirty = toggledBefore != action.Toggled;
+        _audio.PlayPredicted(action.Comp.Sound, performer, predicted ? performer : null);
 
-        action.Cooldown = null;
-        if (action is { UseDelay: not null})
-        {
-            dirty = true;
-            action.Cooldown = (curTime, curTime + action.UseDelay.Value);
-        }
+        // TODO: move to ActionCooldown ActionPerformedEvent?
+        RemoveCooldown((action, action));
+        StartUseDelay((action, action));
 
-        if (dirty)
-        {
-            Dirty(actionId, action);
-            UpdateAction(actionId, action);
-        }
+        UpdateAction(action);
 
-        var ev = new ActionPerformedEvent(performer);
-        RaiseLocalEvent(actionId, ref ev);
+        var performed = new ActionPerformedEvent(performer);
+        RaiseLocalEvent(action, ref performed);
     }
     #endregion
 
@@ -651,7 +616,7 @@ public abstract class SharedActionsSystem : EntitySystem
     /// <inheritdoc cref="AddAction(Robust.Shared.GameObjects.EntityUid,ref System.Nullable{Robust.Shared.GameObjects.EntityUid},string?,Robust.Shared.GameObjects.EntityUid,Content.Shared.Actions.ActionsComponent?)"/>
     public bool AddAction(EntityUid performer,
         [NotNullWhen(true)] ref EntityUid? actionId,
-        [NotNullWhen(true)] out BaseActionComponent? action,
+        [NotNullWhen(true)] out ActionComponent? action,
         string? actionPrototypeId,
         EntityUid container = default,
         ActionsComponent? component = null)
@@ -662,70 +627,65 @@ public abstract class SharedActionsSystem : EntitySystem
         if (!_actionContainer.EnsureAction(container, ref actionId, out action, actionPrototypeId))
             return false;
 
-        return AddActionDirect(performer, actionId.Value, component, action);
+        return AddActionDirect((performer, component), (actionId.Value, action));
     }
 
     /// <summary>
     ///     Adds a pre-existing action.
     /// </summary>
-    public bool AddAction(EntityUid performer,
-        EntityUid actionId,
-        EntityUid container,
-        ActionsComponent? comp = null,
-        BaseActionComponent? action = null,
-        ActionsContainerComponent? containerComp = null
-        )
-    {
-        if (!ResolveActionData(actionId, ref action))
+    public bool AddAction(Entity<ActionsComponent?> performer,
+        Entity<ActionComponent?> action,
+        Entity<ActionsContainerComponent?> container)
+    {
+        if (GetAction(action) is not {} ent)
             return false;
 
-        if (action.Container != container
-            || !Resolve(container, ref containerComp)
-            || !containerComp.Container.Contains(actionId))
+        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(actionId)}");
+            Log.Error($"Attempted to add an action with an invalid container: {ToPrettyString(ent)}");
             return false;
         }
 
-        return AddActionDirect(performer, actionId, comp, action);
+        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(EntityUid performer,
-        EntityUid actionId,
-        ActionsComponent? comp = null,
-        BaseActionComponent? action = null)
+    public bool AddActionDirect(Entity<ActionsComponent?> performer,
+        Entity<ActionComponent?>? action)
     {
-        if (!ResolveActionData(actionId, ref action))
+        if (GetAction(action) is not {} ent)
             return false;
 
-        DebugTools.Assert(action.Container == null ||
-                          (TryComp(action.Container, out ActionsContainerComponent? containerComp)
-                           && containerComp.Container.Contains(actionId)));
+        DebugTools.Assert(ent.Comp.Container == null ||
+                          (TryComp(ent.Comp.Container, out ActionsContainerComponent? containerComp)
+                           && containerComp.Container.Contains(ent)));
 
-        if (action.AttachedEntity != null)
-            RemoveAction(action.AttachedEntity.Value, actionId, action: action);
+        if (ent.Comp.AttachedEntity is {} user)
+            RemoveAction(user, (ent, ent));
 
-        if (action.StartDelay && action.UseDelay != null)
-            SetCooldown(actionId, action.UseDelay.Value);
+        // TODO: make this an event bruh
+        if (ent.Comp.StartDelay && ent.Comp.UseDelay != null)
+            SetCooldown((ent, ent), ent.Comp.UseDelay.Value);
 
-        DebugTools.AssertOwner(performer, comp);
-        comp ??= EnsureComp<ActionsComponent>(performer);
-        action.AttachedEntity = performer;
-        comp.Actions.Add(actionId);
-        Dirty(actionId, action);
-        Dirty(performer, comp);
-        ActionAdded(performer, actionId, comp, action);
+        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(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
+    protected virtual void ActionAdded(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
     {
         // See client-side system for UI code.
     }
@@ -736,17 +696,19 @@ public abstract class SharedActionsSystem : EntitySystem
     /// <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(EntityUid performer, IEnumerable<EntityUid> actions, EntityUid container, ActionsComponent? comp = null, ActionsContainerComponent? containerComp = null)
+    public void GrantActions(Entity<ActionsComponent?> performer,
+        IEnumerable<EntityUid> actions,
+        Entity<ActionsContainerComponent?> container)
     {
-        if (!Resolve(container, ref containerComp))
+        if (!Resolve(container, ref container.Comp))
             return;
 
-        DebugTools.AssertOwner(performer, comp);
-        comp ??= EnsureComp<ActionsComponent>(performer);
+        DebugTools.AssertOwner(performer, performer.Comp);
+        performer.Comp ??= EnsureComp<ActionsComponent>(performer);
 
         foreach (var actionId in actions)
         {
-            AddAction(performer, actionId, container, comp, containerComp: containerComp);
+            AddAction(performer, actionId, container);
         }
     }
 
@@ -765,8 +727,8 @@ public abstract class SharedActionsSystem : EntitySystem
 
         foreach (var actionId in container.Comp.Container.ContainedEntities)
         {
-            if (TryGetActionData(actionId, out var action))
-                AddActionDirect(performer, actionId, performer.Comp, action);
+            if (GetAction(actionId) is {} action)
+                AddActionDirect(performer, (action, action));
         }
     }
 
@@ -784,21 +746,20 @@ public abstract class SharedActionsSystem : EntitySystem
 
         performer.Comp ??= EnsureComp<ActionsComponent>(performer);
 
-        if (TryGetActionData(actionId, out var action))
-            AddActionDirect(performer, actionId, performer.Comp, action);
+        AddActionDirect(performer, actionId);
     }
 
-    public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetActions(EntityUid holderId, ActionsComponent? actions = null)
+    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 (!TryGetActionData(actionId, out var action))
+            if (GetAction(actionId) is not {} ent)
                 continue;
 
-            yield return (actionId, action);
+            yield return ent;
         }
     }
 
@@ -812,11 +773,11 @@ public abstract class SharedActionsSystem : EntitySystem
 
         foreach (var actionId in comp.Actions.ToArray())
         {
-            if (!TryGetActionData(actionId, out var action))
+            if (GetAction(actionId) is not {} ent)
                 return;
 
-            if (action.Container == container)
-                RemoveAction(performer, actionId, comp);
+            if (ent.Comp.Container == container)
+                RemoveAction((performer, comp), (ent, ent));
         }
     }
 
@@ -825,88 +786,81 @@ public abstract class SharedActionsSystem : EntitySystem
     /// </summary>
     public void RemoveProvidedAction(EntityUid performer, EntityUid container, EntityUid actionId, ActionsComponent? comp = null)
     {
-        if (!Resolve(performer, ref comp, false) || !TryGetActionData(actionId, out var action))
+        if (!_actionsQuery.Resolve(performer, ref comp, false) || GetAction(actionId) is not {} ent)
             return;
 
-        if (action.Container == container)
-            RemoveAction(performer, actionId, comp);
+        if (ent.Comp.Container == container)
+            RemoveAction((performer, comp), (ent, ent));
     }
 
-    public void RemoveAction(EntityUid? actionId)
+    /// <summary>
+    /// Removes an action from its container, if it still exists.
+    /// </summary>
+    public void RemoveAction(Entity<ActionComponent?>? action)
     {
-        if (actionId == null)
-            return;
-
-        if (!TryGetActionData(actionId, out var action))
+        if (GetAction(action) is not {} ent || ent.Comp.AttachedEntity is not {} actions)
             return;
 
-        if (!TryComp(action.AttachedEntity, out ActionsComponent? comp))
+        if (!_actionsQuery.TryComp(actions, out var comp))
             return;
 
-        RemoveAction(action.AttachedEntity.Value, actionId, comp, action);
+        RemoveAction((actions, comp), (ent, ent));
     }
 
-    public void RemoveAction(EntityUid performer, EntityUid? actionId, ActionsComponent? comp = null, BaseActionComponent? action = null)
+    public void RemoveAction(Entity<ActionsComponent?> performer, Entity<ActionComponent?>? action)
     {
-        if (actionId == null)
+        if (GetAction(action) is not {} ent)
             return;
 
-        if (!ResolveActionData(actionId, ref action))
-            return;
-
-        if (action.AttachedEntity != performer)
+        if (ent.Comp.AttachedEntity != performer.Owner)
         {
-            DebugTools.Assert(!Resolve(performer, ref comp, false)
-                              || comp.LifeStage >= ComponentLifeStage.Stopping
-                              || !comp.Actions.Contains(actionId.Value));
+            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(actionId)} from an entity that it was never attached to: {ToPrettyString(performer)}. Trace: {Environment.StackTrace}");
+                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 (!Resolve(performer, ref comp, false))
+        if (!_actionsQuery.Resolve(performer, ref performer.Comp, false))
         {
-            DebugTools.Assert(action.AttachedEntity == null || TerminatingOrDeleted(action.AttachedEntity.Value));
-            action.AttachedEntity = null;
+            DebugTools.Assert(performer == null || TerminatingOrDeleted(performer));
+            ent.Comp.AttachedEntity = null;
+            // TODO: should this delete the action since it's now orphaned?
             return;
         }
 
-        if (action.AttachedEntity == null)
-        {
-            // action was already removed?
-            DebugTools.Assert(!comp.Actions.Contains(actionId.Value) || GameTiming.ApplyingState);
-            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);
 
-        comp.Actions.Remove(actionId.Value);
-        action.AttachedEntity = null;
-        Dirty(actionId.Value, action);
-        Dirty(performer, comp);
-        ActionRemoved(performer, actionId.Value, comp, action);
-        if (action.Temporary)
-            QueueDel(actionId.Value);
+        if (ent.Comp.Temporary)
+            QueueDel(ent);
     }
 
     /// <summary>
     /// This method gets called after an action got removed.
     /// </summary>
-    protected virtual void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
+    protected virtual void ActionRemoved(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
     {
         // See client-side system for UI code.
     }
 
-    public bool ValidAction(BaseActionComponent action, bool canReach = true)
+    public bool ValidAction(Entity<ActionComponent> ent, bool canReach = true)
     {
-        if (!action.Enabled)
+        var (uid, comp) = ent;
+        if (!comp.Enabled)
             return false;
 
-
         var curTime = GameTiming.CurTime;
-        if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime)
+        if (comp.Cooldown.HasValue && comp.Cooldown.Value.End > curTime)
             return false;
 
-        return canReach || action is BaseTargetActionComponent { CheckCanAccess: false };
+        // TODO: use event for this
+        return canReach || Comp<TargetActionComponent>(ent)?.CheckCanAccess == false;
     }
 
     #endregion
@@ -953,7 +907,7 @@ public abstract class SharedActionsSystem : EntitySystem
     }
 
     #region EquipHandlers
-    private void OnDidEquip(EntityUid uid, ActionsComponent component, DidEquipEvent args)
+    private void OnDidEquip(Entity<ActionsComponent> ent, ref DidEquipEvent args)
     {
         if (GameTiming.ApplyingState)
             return;
@@ -964,10 +918,10 @@ public abstract class SharedActionsSystem : EntitySystem
         if (ev.Actions.Count == 0)
             return;
 
-        GrantActions(args.Equipee, ev.Actions, args.Equipment, component);
+        GrantActions((ent, ent), ev.Actions, args.Equipment);
     }
 
-    private void OnHandEquipped(EntityUid uid, ActionsComponent component, DidEquipHandEvent args)
+    private void OnHandEquipped(Entity<ActionsComponent> ent, ref DidEquipHandEvent args)
     {
         if (GameTiming.ApplyingState)
             return;
@@ -978,7 +932,7 @@ public abstract class SharedActionsSystem : EntitySystem
         if (ev.Actions.Count == 0)
             return;
 
-        GrantActions(args.User, ev.Actions, args.Equipped, component);
+        GrantActions((ent, ent), ev.Actions, args.Equipped);
     }
 
     private void OnDidUnequip(EntityUid uid, ActionsComponent component, DidUnequipEvent args)
@@ -998,19 +952,76 @@ public abstract class SharedActionsSystem : EntitySystem
     }
     #endregion
 
-    public void SetEntityIcon(EntityUid uid, EntityUid? icon, BaseActionComponent? action = null)
+    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 SetIconColor(Entity<ActionComponent?> ent, Color color)
     {
-        if (!Resolve(uid, ref action))
+        if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.IconColor == color)
             return;
 
-        action.EntityIcon = icon;
-        Dirty(uid, action);
+        ent.Comp.IconColor = color;
+        DirtyField(ent, ent.Comp, nameof(ActionComponent.IconColor));
+    }
+
+    /// <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(BaseActionComponent action, TimeSpan? curTime = null)
+    public bool IsCooldownActive(ActionComponent action, TimeSpan? curTime = null)
     {
         // TODO: Check for charge recovery timer
         return action.Cooldown.HasValue && action.Cooldown.Value.End > curTime;
diff --git a/Content.Shared/Actions/WorldTargetActionComponent.cs b/Content.Shared/Actions/WorldTargetActionComponent.cs
deleted file mode 100644 (file)
index 8066a64..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Actions;
-
-/// <summary>
-/// Used on action entities to define an action that triggers when targeting an entity coordinate.
-/// </summary>
-[RegisterComponent, NetworkedComponent]
-public sealed partial class WorldTargetActionComponent : BaseTargetActionComponent
-{
-    public override BaseActionEvent? BaseEvent => Event;
-
-    /// <summary>
-    ///     The local-event to raise when this action is performed.
-    /// </summary>
-    [DataField("event")]
-    [NonSerialized]
-    public WorldTargetActionEvent? Event;
-}
-
-[Serializable, NetSerializable]
-public sealed class WorldTargetActionComponentState : BaseActionComponentState
-{
-    public WorldTargetActionComponentState(WorldTargetActionComponent component, IEntityManager entManager) : base(component, entManager)
-    {
-    }
-}
index 903bfefc20c72823572a1737f8507fd639a0cb0f..cdf3ffcbd40bd438894a341011a6c4955bdb22b0 100644 (file)
@@ -42,7 +42,7 @@ public abstract class SharedBedSystem : EntitySystem
 
     private void OnUnstrapped(Entity<HealOnBuckleComponent> bed, ref UnstrappedEvent args)
     {
-        _actionsSystem.RemoveAction(args.Buckle, bed.Comp.SleepAction);
+        _actionsSystem.RemoveAction(args.Buckle.Owner, bed.Comp.SleepAction);
         _sleepingSystem.TryWaking(args.Buckle.Owner);
         RemComp<HealOnBuckleHealingComponent>(bed);
     }
index 916bab09e21450a4ac76ede78969718d932f48df..d1ca13843154abb8455f93b4874467a5736a7d95 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Buckle.Components;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Events;
@@ -255,7 +256,7 @@ public sealed partial class SleepingSystem : EntitySystem
     private void Wake(Entity<SleepingComponent> ent)
     {
         RemComp<SleepingComponent>(ent);
-        _actionsSystem.RemoveAction(ent, ent.Comp.WakeAction);
+        _actionsSystem.RemoveAction(ent.Owner, ent.Comp.WakeAction);
 
         var ev = new SleepStateChangedEvent(false);
         RaiseLocalEvent(ent, ref ev);
index f827cbfea8d791ec342e120e153cfcb10360dd2e..c92ab10b47cee4da7bdd4f65e0bf7d7c1a43bc15 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Clothing.EntitySystems;
 using Content.Shared.Item.ItemToggle.Components;
 using Content.Shared.Toggleable;
@@ -23,7 +23,7 @@ public sealed partial class ToggleClothingComponent : Component
     /// This must raise <see cref="ToggleActionEvent"/> to then get handled.
     /// </summary>
     [DataField(required: true)]
-    public EntProtoId<InstantActionComponent> Action = string.Empty;
+    public EntProtoId<InstantActionComponent> Action;
 
     [DataField, AutoNetworkedField]
     public EntityUid? ActionEntity;
index 4c8de4def1deb94a0c25ca76e994499e1618d419..334f88af6d55573ad7828d975d70d5931a8edf3e 100644 (file)
@@ -296,7 +296,7 @@ public sealed class ToggleableClothingSystem : EntitySystem
         }
 
         if (_actionContainer.EnsureAction(uid, ref component.ActionEntity, out var action, component.Action))
-            _actionsSystem.SetEntityIcon(component.ActionEntity.Value, component.ClothingUid, action);
+            _actionsSystem.SetEntityIcon((component.ActionEntity.Value, action), component.ClothingUid);
     }
 }
 
index 9a4afaec3a9614aae81c307dd326d9dde420a8af..1b62c45af4bf4763a72ab1ec6a66e26a404d310d 100644 (file)
@@ -36,7 +36,7 @@ public sealed class EyeClosingSystem : EntitySystem
 
     private void OnShutdown(Entity<EyeClosingComponent> eyelids, ref ComponentShutdown args)
     {
-        _actionsSystem.RemoveAction(eyelids, eyelids.Comp.EyeToggleActionEntity);
+        _actionsSystem.RemoveAction(eyelids.Owner, eyelids.Comp.EyeToggleActionEntity);
 
         SetEyelids((eyelids.Owner, eyelids.Comp), false);
     }
index aaedfb6c61850da5ffb6240de1b79534c3384fd4..8d0e371b8933705eed950bf5d28a9c28b9ebcfe5 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Hands.Components;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.IdentityManagement;
@@ -73,23 +74,21 @@ public abstract partial class SharedItemRecallSystem : EntitySystem
         if (!Resolve(ent.Owner, ref ent.Comp, false))
             return;
 
-        if (!TryComp<InstantActionComponent>(ent.Comp.MarkedByAction, out var instantAction))
+        if (_actions.GetAction(ent.Comp.MarkedByAction) is not {} action)
             return;
 
-        var actionOwner = instantAction.AttachedEntity;
-
-        if (actionOwner == null)
+        if (action.Comp.AttachedEntity is not {} user)
             return;
 
         if (TryComp<EmbeddableProjectileComponent>(ent, out var projectile))
-            _proj.EmbedDetach(ent, projectile, actionOwner.Value);
+            _proj.EmbedDetach(ent, projectile, user);
 
         _popups.PopupPredicted(Loc.GetString("item-recall-item-summon-self", ("item", ent)),
-                               Loc.GetString("item-recall-item-summon-others", ("item", ent), ("name", Identity.Entity(actionOwner.Value, EntityManager))),
-                               actionOwner.Value, actionOwner.Value);
-        _popups.PopupPredictedCoordinates(Loc.GetString("item-recall-item-disappear", ("item", ent)), Transform(ent).Coordinates, actionOwner.Value);
+                               Loc.GetString("item-recall-item-summon-others", ("item", ent), ("name", Identity.Entity(user, EntityManager))),
+                               user, user);
+        _popups.PopupPredictedCoordinates(Loc.GetString("item-recall-item-disappear", ("item", ent)), Transform(ent).Coordinates, user);
 
-        _hands.TryForcePickupAnyHand(actionOwner.Value, ent);
+        _hands.TryForcePickupAnyHand(user, ent);
     }
 
     private void OnRecallMarkerShutdown(Entity<RecallMarkerComponent> ent, ref ComponentShutdown args)
@@ -99,24 +98,22 @@ public abstract partial class SharedItemRecallSystem : EntitySystem
 
     private void TryMarkItem(Entity<ItemRecallComponent> ent, EntityUid item)
     {
-        if (!TryComp<InstantActionComponent>(ent, out var instantAction))
+        if (_actions.GetAction(ent.Owner) is not {} action)
             return;
 
-        var actionOwner = instantAction.AttachedEntity;
-
-        if (actionOwner == null)
+        if (action.Comp.AttachedEntity is not {} user)
             return;
 
-        AddToPvsOverride(item, actionOwner.Value);
+        AddToPvsOverride(item, user);
 
-        var marker = AddComp<RecallMarkerComponent>(item);
         ent.Comp.MarkedEntity = item;
         Dirty(ent);
 
-        marker.MarkedByAction = ent.Owner;
-
-        UpdateActionAppearance(ent);
+        var marker = AddComp<RecallMarkerComponent>(item);
+        marker.MarkedByAction = ent;
         Dirty(item, marker);
+
+        UpdateActionAppearance((action, action, ent));
     }
 
     private void TryUnmarkItem(EntityUid item)
@@ -124,52 +121,47 @@ public abstract partial class SharedItemRecallSystem : EntitySystem
         if (!TryComp<RecallMarkerComponent>(item, out var marker))
             return;
 
-        if (!TryComp<InstantActionComponent>(marker.MarkedByAction, out var instantAction))
+        if (_actions.GetAction(marker.MarkedByAction) is not {} action)
             return;
 
-        if (TryComp<ItemRecallComponent>(marker.MarkedByAction, out var action))
+        if (TryComp<ItemRecallComponent>(action, out var itemRecall))
         {
             // For some reason client thinks the station grid owns the action on client and this doesn't work. It doesn't work in PopupEntity(mispredicts) and PopupPredicted either(doesnt show).
             // I don't have the heart to move this code to server because of this small thing.
             // This line will only do something once that is fixed.
-            if (instantAction.AttachedEntity != null)
+            if (action.Comp.AttachedEntity is {} user)
             {
-                _popups.PopupClient(Loc.GetString("item-recall-item-unmark", ("item", item)), instantAction.AttachedEntity.Value, instantAction.AttachedEntity.Value, PopupType.MediumCaution);
-                RemoveFromPvsOverride(item, instantAction.AttachedEntity.Value);
+                _popups.PopupClient(Loc.GetString("item-recall-item-unmark", ("item", item)), user, user, PopupType.MediumCaution);
+                RemoveFromPvsOverride(item, user);
             }
 
-            action.MarkedEntity = null;
-            UpdateActionAppearance((marker.MarkedByAction.Value, action));
-            Dirty(marker.MarkedByAction.Value, action);
+            itemRecall.MarkedEntity = null;
+            UpdateActionAppearance((action, action, itemRecall));
+            Dirty(action, itemRecall);
         }
 
         RemCompDeferred<RecallMarkerComponent>(item);
     }
 
-    private void UpdateActionAppearance(Entity<ItemRecallComponent> action)
+    private void UpdateActionAppearance(Entity<ActionComponent, ItemRecallComponent> action)
     {
-        if (!TryComp<InstantActionComponent>(action, out var instantAction))
-            return;
-
-        if (action.Comp.MarkedEntity == null)
+        if (action.Comp2.MarkedEntity is {} marked)
         {
-            if (action.Comp.InitialName != null)
-                _metaData.SetEntityName(action, action.Comp.InitialName);
-            if (action.Comp.InitialDescription != null)
-                _metaData.SetEntityDescription(action, action.Comp.InitialDescription);
-            _actions.SetEntityIcon(action, null, instantAction);
+            if (action.Comp2.WhileMarkedName is {} name)
+                _metaData.SetEntityName(action, Loc.GetString(name, ("item", marked)));
+
+            if (action.Comp2.WhileMarkedDescription is {} desc)
+                _metaData.SetEntityDescription(action, Loc.GetString(desc, ("item", marked)));
+
+            _actions.SetEntityIcon((action, action), marked);
         }
         else
         {
-            if (action.Comp.WhileMarkedName != null)
-                _metaData.SetEntityName(action, Loc.GetString(action.Comp.WhileMarkedName,
-                    ("item", action.Comp.MarkedEntity.Value)));
-
-            if (action.Comp.WhileMarkedDescription != null)
-                _metaData.SetEntityDescription(action, Loc.GetString(action.Comp.WhileMarkedDescription,
-                    ("item", action.Comp.MarkedEntity.Value)));
-
-            _actions.SetEntityIcon(action, action.Comp.MarkedEntity, instantAction);
+            if (action.Comp2.InitialName is {} name)
+                _metaData.SetEntityName(action, name);
+            if (action.Comp2.InitialDescription is {} desc)
+                _metaData.SetEntityDescription(action, desc);
+            _actions.SetEntityIcon((action, action), null);
         }
     }
 
index 39fa16f62233031b4f8fbd1b1bc8051e24a6f065..6ef0a10b0cd2154516dcaadb03aeccff5c3c3d60 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Shared.Actions;
+using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Charges.Systems;
 using Content.Shared.DoAfter;
 using Content.Shared.Interaction.Events;
@@ -56,7 +57,7 @@ public sealed class SpellbookSystem : EntitySystem
 
         if (!ent.Comp.LearnPermanently)
         {
-            _actions.GrantActions(args.Args.User, ent.Comp.Spells, ent);
+            _actions.GrantActions(args.Args.User, ent.Comp.Spells, ent.Owner);
             return;
         }
 
index 9e5e26789829b0d842579179006d9dfd94a87ffe..2091068b6335896c2cfebf1337c6643177aac366 100644 (file)
@@ -1,18 +1,20 @@
-using Content.Shared.Actions;
+using Content.Shared.Actions;
+using Content.Shared.Maps;
+using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Mapping;
 
 public sealed partial class StartPlacementActionEvent : InstantActionEvent
 {
-    [DataField("entityType")]
-    public string? EntityType;
+    [DataField]
+    public EntProtoId? EntityType;
 
-    [DataField("tileId")]
-    public string? TileId;
+    [DataField]
+    public ProtoId<ContentTileDefinition>? TileId;
 
-    [DataField("placementOption")]
+    [DataField]
     public string? PlacementOption;
 
-    [DataField("eraser")]
+    [DataField]
     public bool Eraser;
 }
index cb48ef6997f374606e84acb08f45ed9be15f1193..a597e03406ddd02ade38906b5ddae90e4ac89dc0 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Shared.Actions;
+using Content.Shared.Actions;
 using Content.Shared.Implants;
 using Content.Shared.Implants.Components;
 using Content.Shared.Mindshield.Components;
@@ -28,7 +28,8 @@ public sealed class SharedFakeMindShieldImplantSystem : EntitySystem
 
         if (!TryComp<FakeMindShieldComponent>(ent, out var comp))
             return;
-        _actionsSystem.SetToggled(ev.Action, !comp.IsEnabled); // Set it to what the Mindshield component WILL be after this
+        // TODO: is there a reason this cant set ev.Toggle = true;
+        _actionsSystem.SetToggled((ev.Action, ev.Action), !comp.IsEnabled); // Set it to what the Mindshield component WILL be after this
         RaiseLocalEvent(ent, ev); //this reraises the action event to support an eventual future Changeling Antag which will also be using this component for it's "mindshield" ability
     }
     private void ImplantCheck(EntityUid uid, FakeMindShieldImplantComponent component ,ref ImplantImplantedEvent ev)
index 9419daf348151643f9e30a7a797aef717bfe8f56..8a442cd4b97afe22e6672eee868e389e1ce08ccd 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Mobs.Components;
 
 namespace Content.Shared.Mobs.Systems;
index 464f48f187ed3aabc2cf053e0e0bd24da5a5a033..de63506f372e819843237eb1b60edd7b27bb534f 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Ninja.Systems;
 using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
index d9f66d21a31b20864d2eb8a5ff50a3268dab31ec..c000b3e0c1135e0f890d68929d07bf7b99eba095 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Ninja.Systems;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
@@ -22,7 +23,7 @@ public sealed partial class ItemCreatorComponent : Component
     /// The action id for creating an item.
     /// </summary>
     [DataField(required: true)]
-    public EntProtoId<InstantActionComponent> Action = string.Empty;
+    public EntProtoId<InstantActionComponent> Action;
 
     [DataField, AutoNetworkedField]
     public EntityUid? ActionEntity;
index c100e38a764112cb6bda3eee639d674d7b6ad03f..00d3e23cef0947a1ea6ee832b2c732d530fef2cf 100644 (file)
@@ -30,7 +30,7 @@ public abstract class SharedPAISystem : EntitySystem
 
     private void OnShutdown(Entity<PAIComponent> ent, ref ComponentShutdown args)
     {
-        _actions.RemoveAction(ent, ent.Comp.ShopAction);
+        _actions.RemoveAction(ent.Owner, ent.Comp.ShopAction);
     }
 }
 public sealed partial class PAIShopActionEvent : InstantActionEvent
index ea489e332dba1f3b5cd9019a4922b4cc095f3329..edb2ab90db69afba7d8cc38dba29128e91faf890 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Shared.Actions;
+using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.DoAfter;
 using Content.Shared.Random;
 using Content.Shared.Random.Helpers;
@@ -60,12 +61,13 @@ public abstract class SharedRatKingSystem : EntitySystem
         if (!TryComp(uid, out ActionsComponent? comp))
             return;
 
-        _action.RemoveAction(uid, component.ActionRaiseArmyEntity, comp);
-        _action.RemoveAction(uid, component.ActionDomainEntity, comp);
-        _action.RemoveAction(uid, component.ActionOrderStayEntity, comp);
-        _action.RemoveAction(uid, component.ActionOrderFollowEntity, comp);
-        _action.RemoveAction(uid, component.ActionOrderCheeseEmEntity, comp);
-        _action.RemoveAction(uid, component.ActionOrderLooseEntity, comp);
+        var actions = new Entity<ActionsComponent?>(uid, comp);
+        _action.RemoveAction(actions, component.ActionRaiseArmyEntity);
+        _action.RemoveAction(actions, component.ActionDomainEntity);
+        _action.RemoveAction(actions, component.ActionOrderStayEntity);
+        _action.RemoveAction(actions, component.ActionOrderFollowEntity);
+        _action.RemoveAction(actions, component.ActionOrderCheeseEmEntity);
+        _action.RemoveAction(actions, component.ActionOrderLooseEntity);
     }
 
     private void OnOrderAction(EntityUid uid, RatKingComponent component, RatKingOrderActionEvent args)
index d9abeb2d32662ddef463beb9d73aa1751ec32594..55287e4d2386c764fef8bbff8b08c56e3658e674 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Shared.Actions;
+using Content.Shared.Actions;
 using Content.Shared.Interaction;
 using Content.Shared.Interaction.Components;
 using Content.Shared.Movement.Components;
@@ -56,7 +56,7 @@ public abstract class SharedBorgSwitchableTypeSystem : EntitySystem
 
     private void OnShutdown(Entity<BorgSwitchableTypeComponent> ent, ref ComponentShutdown args)
     {
-        _actionsSystem.RemoveAction(ent, ent.Comp.SelectTypeAction);
+        _actionsSystem.RemoveAction(ent.Owner, ent.Comp.SelectTypeAction);
     }
 
     private void OnSelectBorgTypeAction(Entity<BorgSwitchableTypeComponent> ent, ref BorgToggleSelectTypeEvent args)
@@ -90,7 +90,7 @@ public abstract class SharedBorgSwitchableTypeSystem : EntitySystem
     {
         ent.Comp.SelectedBorgType = borgType;
 
-        _actionsSystem.RemoveAction(ent, ent.Comp.SelectTypeAction);
+        _actionsSystem.RemoveAction(ent.Owner, ent.Comp.SelectTypeAction);
         ent.Comp.SelectTypeAction = null;
         Dirty(ent);
 
index b01d3d34f6e5d538180804bd7d048cabf468f8d5..ce037c7c2e97dfb1f9fe04b352f341289aece3c2 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
 using Content.Shared.Destructible.Thresholds;
 using Content.Shared.EntityTable.EntitySelectors;
 using Content.Shared.Xenoarchaeology.Artifact.Prototypes;
index 65c6ae164ea6131f05eca921a0bb9e7a56b954ff..f89ccce92c7fd00a24f199b6ce1d37912d3738b0 100644 (file)
@@ -1,9 +1,11 @@
 - type: entity
+  parent: BaseAction
   id: ActionAnomalyPulse
   name: Anomaly pulse
   description: Release a pulse of energy of your abnormal nature
   components:
-  - type: InstantAction
+  - type: Action
+    useDelay: 30
     icon: Structures/Specific/anomaly.rsi/anom1.png
+  - type: InstantAction
     event: !type:ActionAnomalyPulseEvent
-    useDelay: 30
\ No newline at end of file
index 680bd730f5ad244f29865a73d201de746382de1c..59c008352c2b1c5885f212f078da024bdb9d5b27 100644 (file)
@@ -1,26 +1,28 @@
-- type: entity
+- type: entity
+  parent: BaseMentalAction # allow reading laws when crit
   id: ActionViewLaws
   name: View Laws
   description: View the laws that you must follow.
   components:
-  - type: InstantAction
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
     itemIconStyle: NoItem
     icon:
       sprite: Interface/Actions/actions_borg.rsi
       state: state-laws
-    event: !type:ToggleLawsScreenEvent
     useDelay: 0.5
+  - type: InstantAction
+    event: !type:ToggleLawsScreenEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionSelectBorgType
   name: Select Cyborg Type
   components:
-  - type: InstantAction
+  - type: Action
     itemIconStyle: NoItem
     icon:
       sprite: Interface/Actions/actions_borg.rsi
       state: select-type
-    event: !type:BorgToggleSelectTypeEvent
     useDelay: 0.5
+  - type: InstantAction
+    event: !type:BorgToggleSelectTypeEvent
index aaad27d2b7d1a6715541eaab016b18d46a15dbbf..8dd3e1ece35ba182c70fc125e844960ddadd6699 100644 (file)
@@ -1,47 +1,50 @@
 # Actions added to mobs in crit.
 - type: entity
+  abstract: true
+  parent: BaseMentalAction
+  id: BaseCritAction
+  components:
+  - type: Action
+    itemIconStyle: NoItem
+
+- type: entity
+  parent: BaseCritAction
   id: ActionCritSuccumb
   name: Succumb
   description: Accept your fate.
   components:
-  - type: InstantAction
-    itemIconStyle: NoItem
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
+    startDelay: true
+    useDelay: 10
     icon:
       sprite: Mobs/Ghosts/ghost_human.rsi
       state: icon
+  - type: InstantAction
     event: !type:CritSuccumbEvent
-    startDelay: true
-    useDelay: 10
 
 - type: entity
+  parent: BaseCritAction
   id: ActionCritFakeDeath
   name: Fake Death
   description: Pretend to take your final breath while staying alive.
   components:
-  - type: InstantAction
-    itemIconStyle: NoItem
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
+    useDelay: 30
     icon:
       sprite: Interface/Actions/actions_crit.rsi
       state: fakedeath
+  - type: InstantAction
     event: !type:CritFakeDeathEvent
-    useDelay: 30
 
 - type: entity
+  parent: ActionCritSuccumb # for use delay
   id: ActionCritLastWords
   name: Say Last Words
   description: Whisper your last words to anyone nearby, and then succumb to your fate. You only have 30 characters to work with.
   components:
-  - type: InstantAction
-    itemIconStyle: NoItem
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
     icon:
       sprite: Interface/Actions/actions_crit.rsi
       state: lastwords
+  - type: InstantAction
     event: !type:CritLastWordsEvent
-    startDelay: true
-    useDelay: 10
index 9f80f18178e7b303e4b3a7291379fc04eb062cca..be127b61d1d99e9d3f321449ca1af49c7ca5ee5a 100644 (file)
@@ -1,24 +1,26 @@
 - type: entity
+  parent: BaseSuicideAction
   id: DionaGibAction
   name: Gib Yourself!
   description: Split apart into 3 nymphs.
   components:
-  - type: InstantAction
+  - type: Action
     icon:
       sprite: Mobs/Species/Diona/organs.rsi
       state: brain
+  - type: InstantAction
     event: !type:GibActionEvent {}
-    checkCanInteract: false
-    checkConsciousness: false
 
 - type: entity
+  parent: BaseAction
   id: DionaReformAction
   name: Reform
   description: Reform back into a whole Diona.
   components:
-  - type: InstantAction
+  - type: Action
+    useDelay: 600 # Once every 10 minutes. Keep them dead for a fair bit before reforming
     icon:
       sprite: Mobs/Species/Diona/parts.rsi
       state: full
+  - type: InstantAction
     event: !type:ReformEvent {}
-    useDelay: 600 # Once every 10 minutes. Keep them dead for a fair bit before reforming
index 5982c3daa2a205c21e2faa919d456f3366ae4e21..d3c1e95fc04dbad495fbaf0b0d0d2d96fe65b7d1 100644 (file)
@@ -1,14 +1,16 @@
-- type: entity
+- type: entity
+  parent: BaseAction
   id: ActionToggleInternals
   name: Toggle Internals
   description: Breathe from the equipped gas tank. Also requires equipped breath mask.
   components:
-  - type: InstantAction
+  - type: Action
+    useDelay: 1
     icon:
       sprite: Interface/Alerts/internals.rsi
       state: internal2
     iconOn:
       sprite: Interface/Alerts/internals.rsi
       state: internal1
+  - type: InstantAction
     event: !type:ToggleActionEvent
-    useDelay: 1
diff --git a/Resources/Prototypes/Actions/mapping.yml b/Resources/Prototypes/Actions/mapping.yml
new file mode 100644 (file)
index 0000000..937fc92
--- /dev/null
@@ -0,0 +1,48 @@
+- type: entity
+  abstract: true
+  parent: BaseMentalAction
+  id: BaseMappingAction
+  components:
+  - type: Action
+    clientExclusive: true
+
+- type: entity
+  parent: BaseMappingAction
+  id: BaseMappingDecalAction # not abstract but the event has to be set in code
+  components:
+  - type: TargetAction
+    repeat: true
+    range: -1
+  - type: WorldTargetAction
+    event: null # has to be set with SetEvent in DecalPlacementSystem
+
+- type: entity
+  parent: BaseMappingAction
+  id: BaseMappingSpawnAction # not abstract but the event has to be set in code
+  components:
+  - type: Action
+    icon: Tiles/cropped_parallax.png
+  - type: InstantAction
+    event: null # has to be set with SetEvent in MappingSystem
+
+- type: entity
+  parent: BaseMappingAction
+  id: ActionMappingEraser
+  name: action-name-mapping-erase
+  components:
+  - type: Action
+    icon: Interface/VerbIcons/delete.svg.192dpi.png
+  - type: InstantAction
+    event: !type:StartPlacementActionEvent
+      eraser: true
+
+# these are used for mapping actions yml files
+- type: entity
+  parent: BaseMappingSpawnAction
+  id: BaseMappingEntityAction # not abstract but the event has to be set in code
+  components:
+  - type: Action
+    autoPopulate: False
+    temporary: True
+  - type: InstantAction
+    event: null # has to be set with SetEvent in ActionsSystem
index 48092f9c5adfb626c492c0299f45ad1297af0097..1db758fc979ffcab22e7f70547f37679d5f8356e 100644 (file)
@@ -1,37 +1,48 @@
 - type: entity
+  abstract: true
+  parent: BaseAction
+  id: BaseMechAction
+  components:
+  - type: Action
+    itemIconStyle: NoItem
+
+- type: entity
+  parent: BaseMechAction
   id: ActionMechCycleEquipment
   name: Cycle
   description: Cycles currently selected equipment
   components:
-  - type: InstantAction
-    itemIconStyle: NoItem
+  - type: Action
+    useDelay: 0.5
     icon:
       sprite: Interface/Actions/actions_mecha.rsi
       state: mech_cycle_equip_on
+  - type: InstantAction
     event: !type:MechToggleEquipmentEvent
-    useDelay: 0.5
 
 - type: entity
+  parent: BaseMechAction
   id: ActionMechOpenUI
   name: Control Panel
   description: Opens the control panel for the mech
   components:
-  - type: InstantAction
-    itemIconStyle: NoItem
+  - type: Action
+    useDelay: 1
     icon:
       sprite: Interface/Actions/actions_mecha.rsi
       state: mech_view_stats
+  - type: InstantAction
     event: !type:MechOpenUiEvent
-    useDelay: 1
 
 - type: entity
+  parent: BaseMechAction
   id: ActionMechEject
   name: Eject
   description: Ejects the pilot from the mech
   components:
-  - type: InstantAction
-    itemIconStyle: NoItem
+  - type: Action
     icon:
       sprite: Interface/Actions/actions_mecha.rsi
       state: mech_eject
+  - type: InstantAction
     event: !type:MechEjectPilotEvent
index 47cb8a83f445eb17aad935c919791e2926926f3a..c1fcf8dd7a4fef77505f5d2f595e61f15943969c 100644 (file)
@@ -1,73 +1,82 @@
 # gloves
 - type: entity
+  parent: BaseToggleAction
   id: ActionToggleNinjaGloves
   name: Toggle ninja gloves
   description: Toggles all glove actions on left click. Includes your doorjack, draining power, stunning enemies and hacking certain computers.
   components:
-  - type: InstantAction
+  - type: Action
     priority: -13
-    event: !type:ToggleActionEvent {}
 
 # suit
 - type: entity
+  parent: BaseAction
   id: ActionCreateThrowingStar
   name: Create throwing star
   description: Channels suit power into creating a throwing star that deals extra stamina damage.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 0.5
     icon:
       sprite: Objects/Weapons/Throwable/throwing_star.rsi
       state: icon
     itemIconStyle: NoItem
     priority: -10
+  - type: InstantAction
     event: !type:CreateItemEvent {}
 
 - type: entity
+  parent: BaseAction
   id: ActionRecallKatana
   name: Recall katana
   description: Teleports the Energy Katana linked to this suit to its wearer, cost based on distance.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 1
     icon:
       sprite: Objects/Weapons/Melee/energykatana.rsi
       state: icon
     itemIconStyle: NoItem
     priority: -11
+  - type: InstantAction
     event: !type:RecallKatanaEvent {}
 
 - type: entity
+  parent: BaseAction
   id: ActionNinjaEmp
   name: EM Burst
   description: Disable any nearby technology with an electro-magnetic pulse.
   components:
-  - type: InstantAction
+  - type: Action
     icon:
       sprite: Objects/Weapons/Grenades/empgrenade.rsi
       state: icon
     itemIconStyle: BigAction
     priority: -13
+  - type: InstantAction
     event: !type:NinjaEmpEvent {}
 
 - type: entity
+  parent: BaseAction
   id: ActionTogglePhaseCloak
   name: Phase cloak
   description: Toggles your suit's phase cloak. Beware that if you are hit, all abilities are disabled for 5 seconds, including your cloak!
   components:
-  - type: InstantAction
+  - type: Action
     # have to plan (un)cloaking ahead of time
     useDelay: 5
     priority: -9
+  - type: InstantAction
     event: !type:ToggleActionEvent
 
 # katana
 - type: entity
+  parent: BaseAction
   id: ActionEnergyKatanaDash
   name: Katana dash
   description: Teleport to anywhere you can see, if your Energy Katana is in your hand.
   components:
-  - type: WorldTargetAction
+  - type: Action
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: blink
@@ -77,6 +86,8 @@
       params:
         volume: 5
     priority: -12
-    event: !type:DashEvent
+  - type: TargetAction
     checkCanAccess: false
     range: 0
+  - type: WorldTargetAction
+    event: !type:DashEvent
index 291400ab388421ae8d04ca1cd2750f311102e515..2f931e29e2b192f5c86a773eb30ae494f89a39b5 100644 (file)
@@ -1,4 +1,5 @@
-- type: entity
+- type: entity
+  parent: BaseAction
   id: ActionRevertPolymorph
   name: Revert
   description: Revert back into your original form.
@@ -7,54 +8,59 @@
     event: !type:RevertPolymorphActionEvent
 
 - type: entity
-  id: ActionPolymorph
+  abstract: true
+  parent: BaseAction
+  id: BaseActionPolymorph
   components:
+  - type: Action
+    itemIconStyle: NoItem
+    useDelay: 60
   - type: InstantAction
     event: !type:PolymorphActionEvent
-    itemIconStyle: NoItem
 
 - type: entity
+  parent: BaseActionPolymorph
   id: ActionPolymorphWizardSpider
   name: Spider Polymorph
   description: Polymorphs you into a Spider.
   components:
-  - type: InstantAction
-    useDelay: 60
-    event: !type:PolymorphActionEvent
-      protoId: WizardSpider
-    itemIconStyle: NoItem
+  - type: Action
     icon:
       sprite: Mobs/Animals/spider.rsi
       state: tarantula
+  - type: InstantAction
+    event: !type:PolymorphActionEvent
+      protoId: WizardSpider
 
 - type: entity
+  parent: BaseActionPolymorph
   id: ActionPolymorphWizardRod
   name: Rod Form
   description: CLANG!
   components:
-  - type: InstantAction
-    useDelay: 60
-    event: !type:PolymorphActionEvent
-      protoId: WizardRod
-    itemIconStyle: NoItem
+  - type: Action
     icon:
       sprite: Objects/Fun/immovable_rod.rsi
       state: icon
+  - type: InstantAction
+    event: !type:PolymorphActionEvent
+      protoId: WizardRod
 
 - type: entity
+  parent: BaseActionPolymorph
   id: ActionPolymorphJaunt
   name: Ethereal Jaunt
   description: Melt into the Ethereal Plane for a quick getaway!
   components:
   - type: Magic
-  - type: InstantAction
+  - type: Action
     useDelay: 30
-    event: !type:PolymorphActionEvent
-      protoId: Jaunt
-    itemIconStyle: NoItem
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: jaunt
+  - type: InstantAction
+    event: !type:PolymorphActionEvent
+      protoId: Jaunt
     # TODO: Effect ECS (from cardboard box)
   - type: ActionUpgrade
     effectedLevels:
       3: ActionPolymorphJauntIII
 
 - type: entity
-  id: ActionPolymorphJauntII
   parent: ActionPolymorphJaunt
+  id: ActionPolymorphJauntII
   name: Ethereal Jaunt II
   description: Melt into the Ethereal Plane for an even quicker getaway!
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 25
-    event: !type:PolymorphActionEvent
-      protoId: Jaunt
-    itemIconStyle: NoItem
-    icon:
-      sprite: Objects/Magic/magicactions.rsi
-      state: jaunt
 
 - type: entity
-  id: ActionPolymorphJauntIII
   parent: ActionPolymorphJaunt
+  id: ActionPolymorphJauntIII
   name: Ethereal Jaunt III
   description: Are you even tangible anymore?
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 20
-    event: !type:PolymorphActionEvent
-      protoId: Jaunt
-    itemIconStyle: NoItem
-    icon:
-      sprite: Objects/Magic/magicactions.rsi
-      state: jaunt
index dca491a99b53a8747336bc236c07217e668ae863..5904908a75bc5a8451e752f3b69c1e114282d6ff 100644 (file)
@@ -1,48 +1,58 @@
 - type: entity
+  parent: BaseAction
   id: ActionRevenantShop
   name: Shop
   description: Opens the ability shop.
   components:
-  - type: InstantAction
+  - type: Action
     icon: Interface/Actions/shop.png
+  - type: InstantAction
     event: !type:RevenantShopActionEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionRevenantDefile
   name: Defile
   description: Costs 30 Essence.
   components:
-  - type: InstantAction
+  - type: Action
+    useDelay: 15
     icon: Interface/Actions/defile.png
+  - type: InstantAction
     event: !type:RevenantDefileActionEvent
-    useDelay: 15
 
 - type: entity
+  parent: BaseAction
   id: ActionRevenantOverloadLights
   name: Overload Lights
   description: Costs 40 Essence.
   components:
-  - type: InstantAction
+  - type: Action
     icon: Interface/Actions/overloadlight.png
-    event: !type:RevenantOverloadLightsActionEvent
     useDelay: 20
+  - type: InstantAction
+    event: !type:RevenantOverloadLightsActionEvent
 
 #- type: entity
+#  parent: BaseAction
 #  id: ActionRevenantBlight
 #  name: Blight
 #  description: Costs 50 Essence.
 #  components:
-#  - type: InstantAction
+#  - type: Action
 #    icon: Interface/Actions/blight.png
-#    event: !type:RevenantBlightActionEvent
 #    useDelay: 20
+#  - type: InstantAction
+#    event: !type:RevenantBlightActionEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionRevenantMalfunction
   name: Malfunction
   description: Costs 60 Essence.
   components:
-  - type: InstantAction
+  - type: Action
     icon: Interface/Actions/malfunction.png
-    event: !type:RevenantMalfunctionActionEvent
     useDelay: 20
+  - type: InstantAction
+    event: !type:RevenantMalfunctionActionEvent
index ace9d914e47736eba3bb996830acd15e4941a314..0c479a24249db6324a3c407e51f06cd8875e0f28 100644 (file)
@@ -1,5 +1,6 @@
 # gloves
 - type: entity
+  parent: BaseAction
   id: ActionToggleKnuckleDustersStun
   name: Toggle stun knuckle dusters
   description: Toggles the duster's built in stun baton.
index c71ec74880c5db28562fc45001ec1a7af2fb36d1..24cb7646a8af179f1eb0682bb80f454f729ff578 100644 (file)
@@ -1,10 +1,12 @@
-- type: entity
+- type: entity
+  parent: BaseAction
   id: ActionConfigureMeleeSpeech
   name: Set Battlecry
   description: Set a custom battlecry for when you attack!
   components:
-  - type: InstantAction
+  - type: Action
     itemIconStyle: BigItem
     priority: -20
     useDelay: 1
+  - type: InstantAction
     event: !type:MeleeSpeechConfigureActionEvent
index fe37085ecabc5424cb5b708dd19bc04a8c60dd7b..da3785ed84f5eb7be850741b5d9aeabdbce39ef1 100644 (file)
@@ -1,19 +1,23 @@
 - type: entity
+  parent: BaseAction
   id: ActionSpiderWeb
   name: Spider Web
   description: Spawns a web that slows your prey down.
   components:
-  - type: InstantAction
+  - type: Action
     icon: Interface/Actions/web.png
-    event: !type:SpiderWebActionEvent
     useDelay: 25
+  - type: InstantAction
+    event: !type:SpiderWebActionEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionSericulture
   name: Weave silk
   description: Weave a bit of silk for use in arts and crafts.
   components:
-  - type: InstantAction
+  - type: Action
     icon: Interface/Actions/web.png
-    event: !type:SericultureActionEvent
     useDelay: 1
+  - type: InstantAction
+    event: !type:SericultureActionEvent
index 67d56262195492a46ed25783ee03a45e0fefe509..4dbaf07aabe8abe95da1d533369c070427bf1571 100644 (file)
@@ -1,28 +1,32 @@
-# Actions
+# Actions
 - type: entity
+  parent: BaseAction
   id: ActionJumpToCore
   name: Jump to core
   description: Sends your eye back to the core.
   components:
-  - type: InstantAction
+  - type: Action
     priority: -9
     itemIconStyle: BigAction
     icon:
       sprite: Interface/Actions/actions_ai.rsi
       state: ai_core
+  - type: InstantAction
     event: !type:JumpToCoreEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionSurvCameraLights
   name: Toggle camera lights
   description: Enable surveillance camera lights near wherever you're viewing.
   components:
-  - type: InstantAction
+  - type: Action
     priority: -5
     itemIconStyle: BigAction
     icon:
       sprite: Interface/Actions/actions_ai.rsi
       state: camera_light
+  - type: InstantAction
     event: !type:RelayedActionComponentChangeEvent
       components:
       - type: LightOnCollideCollider
 
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionAIViewLaws
   name: View Laws
   description: View the laws that you must follow.
   components:
-  - type: InstantAction
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
     priority: -3
     itemIconStyle: NoItem
     icon:
       sprite: Interface/Actions/actions_ai.rsi
       state: state_laws
-    event: !type:ToggleLawsScreenEvent
     useDelay: 0.5
+  - type: InstantAction
+    event: !type:ToggleLawsScreenEvent
index e1da9bcf1ff42b1499b7da7e2a0b8185e973b0a9..945070dea0a986f198e6228f95b6431b18c1b284 100644 (file)
 # base actions
 
+# base prototype for all action entities
+- type: entity
+  abstract: true
+  id: BaseAction
+  components:
+  - type: Action
+
+# an action that is done all in le head and cant be prevented by any means
+- type: entity
+  abstract: true
+  parent: BaseAction
+  id: BaseMentalAction
+  components:
+  - type: Action
+    checkCanInteract: false
+    checkConsciousness: false
+
 - type: entity
-  id: BaseSuicideAction
   abstract: true
+  parent: BaseMentalAction
+  id: BaseSuicideAction
   components:
   - type: ConfirmableAction
     popup: suicide-action-popup
 
+- type: entity
+  abstract: true
+  parent: BaseAction
+  id: BaseImplantAction
+  components:
+  - type: InstantAction
+    event: !type:ActivateImplantEvent
+
+- type: entity
+  abstract: true
+  parent: BaseAction
+  id: BaseToggleAction
+  components:
+  - type: InstantAction
+    event: !type:ToggleActionEvent
+
 # actions
 
 - type: entity
+  parent: BaseAction
   id: ActionScream
   name: Scream
   description: AAAAAAAAAAAAAAAAAAAAAAAAA
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 10
     icon: Interface/Actions/scream.png
-    event: !type:ScreamActionEvent
     checkCanInteract: false
+  - type: InstantAction
+    event: !type:ScreamActionEvent
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionTurnUndead
   name: Turn Undead
   description: Succumb to your infection and become a zombie.
   components:
-  - type: InstantAction
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
     icon: Interface/Actions/zombie-turn.png
+  - type: InstantAction
     event: !type:ZombifySelfActionEvent
 
 - type: entity
+  parent: BaseToggleAction
   id: ActionToggleLight
   name: Toggle Light
   description: Turn the light on and off.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 1
     icon: { sprite: Objects/Tools/flashlight.rsi, state: flashlight }
     iconOn: { sprite: Objects/Tools/flashlight.rsi, state: flashlight-on }
-    event: !type:ToggleActionEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionOpenStorageImplant
   name: Toggle Storage Implant
   description: Opens or closes the storage implant embedded under your skin
   components:
-  - type: InstantAction
+  - type: Action
     itemIconStyle: BigAction
     priority: -20
     icon:
       sprite: Clothing/Back/Backpacks/backpack.rsi
       state: icon
-    event: !type:OpenStorageImplantEvent
     useDelay: 1
+  - type: InstantAction
+    event: !type:OpenStorageImplantEvent
 
 - type: entity
-  parent: BaseSuicideAction
+  parent: [BaseSuicideAction, BaseImplantAction]
   id: ActionActivateMicroBomb
   name: Activate Microbomb
   description: Activates your internal microbomb, completely destroying you and your equipment
   components:
-  - type: InstantAction
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
     itemIconStyle: BigAction
     priority: -20
     icon:
       sprite: Actions/Implants/implants.rsi
       state: explosive
-    event: !type:ActivateImplantEvent
 
 - type: entity
-  parent: BaseSuicideAction
+  parent: [BaseSuicideAction, BaseImplantAction]
   id: ActionActivateDeathAcidifier
   name: Activate Death-Acidifier
   description: Activates your death-acidifier, completely melting you and your equipment
   components:
-  - type: InstantAction
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
     itemIconStyle: BigAction
     priority: -20
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: gib
-    event: !type:ActivateImplantEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionActivateFreedomImplant
   name: Break Free
   description: Activating your freedom implant will free you from any hand restraints
   components:
   - type: LimitedCharges
     maxCharges: 3
-  - type: InstantAction
+  - type: Action
     checkCanInteract: false
     itemIconStyle: BigAction
     priority: -20
     icon:
       sprite: Actions/Implants/implants.rsi
       state: freedom
+  - type: InstantAction
     event: !type:UseFreedomImplantEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionOpenUplinkImplant
   name: Open Uplink
   description: Opens the syndicate uplink embedded under your skin
   components:
-  - type: InstantAction
+  - type: Action
     itemIconStyle: BigAction
     priority: -20
     icon:
       sprite: Objects/Devices/communication.rsi
       state: old-radio
+  - type: InstantAction
     event: !type:OpenUplinkImplantEvent
 
 - type: entity
+  parent: BaseImplantAction
   id: ActionActivateEmpImplant
   name: Activate EMP
   description: Triggers a small EMP pulse around you
   components:
   - type: LimitedCharges
     maxCharges: 3
-  - type: InstantAction
+  - type: Action
     checkCanInteract: false
     useDelay: 5
     itemIconStyle: BigAction
     icon:
       sprite: Objects/Weapons/Grenades/empgrenade.rsi
       state: icon
-    event: !type:ActivateImplantEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionActivateScramImplant
   name: SCRAM!
   description: Randomly teleports you within a large distance.
   components:
   - type: LimitedCharges
     maxCharges: 2
-  - type: InstantAction
+  - type: Action
     checkCanInteract: false
     useDelay: 5
     itemIconStyle: BigAction
     icon:
       sprite: Structures/Specific/anomaly.rsi
       state: anom4
+  - type: InstantAction
     event: !type:UseScramImplantEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionActivateDnaScramblerImplant
   name: Scramble DNA
   description:  Randomly changes your name and appearance.
     popup: dna-scrambler-action-popup
   - type: LimitedCharges
     maxCharges: 1
-  - type: InstantAction
+  - type: Action
     itemIconStyle: BigAction
     priority: -20
     icon:
       sprite: Clothing/OuterClothing/Hardsuits/lingspacesuit.rsi
       state: icon
+  - type: InstantAction
     event: !type:UseDnaScramblerImplantEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionToggleSuitPiece
   name: Toggle Suit Piece
   description: Remember to equip the important pieces of your suit before going into action.
   components:
-  - type: InstantAction
+  - type: Action
     itemIconStyle: BigItem
     useDelay: 1 # equip noise spam.
+  - type: InstantAction
     event: !type:ToggleClothingEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionCombatModeToggle
   name: "[color=red]Combat Mode[/color]"
   description: Enter combat mode
   components:
-  - type: InstantAction
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
     icon: Interface/Actions/harmOff.png
     iconOn: Interface/Actions/harm.png
-    event: !type:ToggleCombatActionEvent
     priority: -100
+  - type: InstantAction
+    event: !type:ToggleCombatActionEvent
 
 - type: entity
-  id: ActionCombatModeToggleOff
   parent: ActionCombatModeToggle
-  name: "[color=red]Combat Mode[/color]"
-  description: Enter combat mode
+  id: ActionCombatModeToggleOff
   components:
-  - type: InstantAction
+  - type: Action
     enabled: false
     autoPopulate: false
-    priority: -100
 
 - type: entity
+  parent: BaseAction
   id: ActionChangeVoiceMask
   name: Set name
   description: Change the name others hear to something else.
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Interface/Actions/voice-mask.rsi, state: icon }
+  - type: InstantAction
     event: !type:VoiceMaskSetNameEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionVendingThrow
   name: Dispense Item
   description: Randomly dispense an item from your stock.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 30
+  - type: InstantAction
     event: !type:VendingMachineSelfDispenseEvent
 
 - type: entity
+  parent: BaseToggleAction
   id: ActionToggleBlock
   name: Block
   description: Raise or lower your shield.
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Objects/Weapons/Melee/shields.rsi, state: teleriot-icon }
-    iconOn: Objects/Weapons/Melee/shields.rsi/teleriot-on.png
-    event: !type:ToggleActionEvent
+    iconOn: { sprite: Objects/Weapons/Melee/shields.rsi, state: teleriot-on }
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionClearNetworkLinkOverlays
   name: Clear network link overlays
   description: Clear network link overlays.
   components:
-  - type: InstantAction
+  - type: Action
     clientExclusive: true
-    checkCanInteract: false
-    checkConsciousness: false
     temporary: true
     icon: { sprite: Objects/Tools/multitool.rsi, state: icon }
+  - type: InstantAction
     event: !type:ClearAllOverlaysEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionAnimalLayEgg
   name: Lay egg
   description: Uses hunger to lay an egg.
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Objects/Consumable/Food/egg.rsi, state: icon }
     useDelay: 60
+  - type: InstantAction
     event: !type:EggLayInstantActionEvent
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionSleep
   name: Sleep
   description: Go to sleep.
   components:
-  - type: InstantAction
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
     icon: { sprite: Clothing/Head/Hats/pyjamasyndicatered.rsi, state: icon }
+  - type: InstantAction
     event: !type:SleepActionEvent
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionWake
   name: Wake up
   description: Stop sleeping.
   components:
-  - type: InstantAction
-    icon: { sprite: Clothing/Head/Hats/pyjamasyndicatered.rsi, state: icon }
-    checkCanInteract: false
-    checkConsciousness: false
-    event: !type:WakeActionEvent
+  - type: Action
     startDelay: true
     useDelay: 2
+    icon: { sprite: Clothing/Head/Hats/pyjamasyndicatered.rsi, state: icon }
+  - type: InstantAction
+    event: !type:WakeActionEvent
 
 - type: entity
+  parent: BaseImplantAction
   id: ActionActivateHonkImplant
   name: Honk
   description: Activates your honking implant, which will produce the signature sound of the clown.
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Objects/Fun/bikehorn.rsi, state: icon }
-    event: !type:ActivateImplantEvent
-    useDelay: 1
 
 - type: entity
+  parent: BaseAction
   id: ActionFireStarter
   name: Ignite
   description: Ignites enemies in a radius around you.
   components:
-  - type: InstantAction
+  - type: Action
     priority: -1
     useDelay: 30
     icon: Interface/Actions/firestarter.png
+  - type: InstantAction
     event: !type:FireStarterActionEvent
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionToggleEyes
   name: Open/Close eyes
   description: Close your eyes to protect your peepers, or open your eyes to enjoy the pretty lights.
   components:
-  - type: InstantAction
+  - type: Action
     icon: Interface/Actions/eyeopen.png
     iconOn: Interface/Actions/eyeclose.png
-    event: !type:ToggleEyesActionEvent
     useDelay: 1 # so u cant give yourself and observers eyestrain by rapidly spamming the action
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: InstantAction
+    event: !type:ToggleEyesActionEvent
 
 - type: entity
+  parent: BaseToggleAction
   id: ActionToggleWagging
   name: Wagging Tail
   description: Start or stop wagging your tail.
   components:
-    - type: InstantAction
-      icon: { sprite: Mobs/Customization/reptilian_parts.rsi, state: tail_smooth_behind }
-      iconOn: { sprite: Mobs/Customization/reptilian_parts.rsi, state: tail_smooth_behind }
-      itemIconStyle: NoItem
-      useDelay: 1 # emote spam
-      event: !type:ToggleActionEvent
+  - type: Action
+    icon: { sprite: Mobs/Customization/reptilian_parts.rsi, state: tail_smooth_behind }
+    iconOn: { sprite: Mobs/Customization/reptilian_parts.rsi, state: tail_smooth_behind }
+    itemIconStyle: NoItem
+    useDelay: 1 # emote spam
 
 - type: entity
+  parent: BaseAction
   id: FakeMindShieldToggleAction
   name: '[color=green]Toggle Fake Mindshield[/color]'
   description: Turn the Fake Mindshield implant's transmission on/off
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Interface/Actions/actions_fakemindshield.rsi, state: icon }
     iconOn: { sprite: Interface/Actions/actions_fakemindshield.rsi, state: icon-on }
     itemIconStyle: NoItem
     useDelay: 1
+  - type: InstantAction
     event: !type:FakeMindShieldToggleEvent
 
 - type: entity
+  parent: BaseToggleAction
   id: ActionToggleParamedicSiren
   name: Toggle Paramedic Siren
   description: Toggles the paramedic siren on and off.
   components:
-  - type: InstantAction 
+  - type: Action
     icon:
       sprite: Clothing/OuterClothing/Hardsuits/paramed.rsi
       state: icon-siren
     useDelay: 1
     itemIconStyle: BigAction
-    event: !type:ToggleActionEvent
index a587294d760afb893246eea6579d25f726305f6d..4c38dde0d778db21ffd7d9d23be2d8e4418c406f 100644 (file)
       Hair: HEAD
 
 - type: entity
+  parent: BaseAction
   id: ActionToggleJusticeHelm
   name: Toggle Justice Helm
   description: Toggles the justice helm on and off.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 1
     itemIconStyle: BigItem
+  - type: InstantAction
     event: !type:ToggleActionEvent
index a7c9e701bac7cf36661223baf04c0dca247b4fcc..711a9b0513aa5c6a848c18c85eb99102071af02c 100644 (file)
     accent: OwOAccent
 
 - type: entity
-  categories: [ Actions, HideSpawnMenu ]
+  parent: BaseToggleAction
   id: ActionBecomeValid
   name: Become Valid
   description: "*notices your killsign* owo whats this"
-  components:
-  - type: InstantAction
-    event: !type:ToggleActionEvent
 
 - type: entity
   parent: ClothingHeadBase
index 31e91df4a725dca8ae45a0ebc4ef2a1475cba98f..a112e4f0def932bcea2303d4dcaea968dd5126b5 100644 (file)
   - type: Mask
 
 - type: entity
+  parent: BaseAction
   id: ActionToggleMask
   name: Toggle Mask
   description: Handy, but prevents insertion of pie into your pie hole.
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Clothing/Mask/gas.rsi, state: icon }
     iconOn: Interface/Default/blocked.png
+  - type: InstantAction
     event: !type:ToggleMaskEvent
 
 - type: entity
index 26071b51468d7ba1be515fd058cadc44cd9c0f8f..b25f78e6a40b6510c45fd8e2c8280ab27f8ea89b 100644 (file)
   - type: Stethoscope
 
 - type: entity
+  parent: BaseAction
   id: ActionStethoscope
   name: Listen with stethoscope
   components:
-  - type: EntityTargetAction
+  - type: Action
     icon:
       sprite: Clothing/Neck/Misc/stethoscope.rsi
       state: icon
-    event: !type:StethoscopeActionEvent
-    checkCanInteract: false
     priority: -1
     itemIconStyle: BigAction
+    checkCanInteract: false
+  - type: TargetAction
+  - type: EntityTargetAction
+    event: !type:StethoscopeActionEvent
 
 - type: entity
   parent: ClothingNeckBase
index 6f88dde59545580f2985b297a98b799b38dcac89..ad88dd1ae24b7355b61b267b1fc4f816dc78c65c 100644 (file)
     size: Normal
 
 - type: entity
+  parent: BaseAction
   id: ActionToggleMagboots
   name: Toggle Magboots
   description: Toggles the magboots on and off.
   components:
-  - type: InstantAction
+  - type: Action
     itemIconStyle: BigItem
+  - type: InstantAction
     event: !type:ToggleActionEvent
index a04a6453f97ac93cad54ca5afbdc5227ec14c7bc..fafb9a084603850575b20c6c71e9951e70c73f8f 100644 (file)
     tags: []
 
 - type: entity
+  parent: BaseAction
   id: ActionToggleSpeedBoots
   name: Toggle Speed Boots
   description: Toggles the speed boots on and off.
   components:
-  - type: InstantAction
+  - type: Action
     itemIconStyle: BigItem
+  - type: InstantAction
     event: !type:ToggleActionEvent
 
 - type: entity
index c4c131a7ab21aca9ef5494b55eedd8c4e9c360d5..672d29a637872859e2a96d755457f47feba29b8c 100644 (file)
       amount: 3
 
 - type: entity
+  parent: BaseAction
   id: ActionGoliathTentacle
   name: "[color=red]Tentacle Slam[/color]"
   description: Use your tentacles to grab and stun a target player!
   components:
-  - type: EntityWorldTargetAction
+  - type: Action
     raiseOnUser: true
     icon:
       sprite: Mobs/Aliens/Asteroid/goliath.rsi
       state: goliath_tentacle_wiggle
     sound:
       path: "/Audio/Weapons/slash.ogg"
-    event: !type:GoliathSummonTentacleAction
     useDelay: 8
+  - type: TargetAction
     range: 10
+  - type: WorldTargetAction
+    event: !type:GoliathSummonTentacleAction
 
 - type: entity
   id: GoliathTentacle
index 4ba7ac7c55887625ff9817c320a3a11c577fc733..5177d38fa4bf4d2ea3db9af6709b6c610234a4d1 100644 (file)
     IngotGold1: 5 #loot
 
 - type: entity
+  parent: BaseAction
   id: ActionRatKingRaiseArmy
   name: Raise Army
   description: Spend some hunger to summon an allied rat to help defend you.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 4
     icon:
       sprite: Interface/Actions/actions_rat_king.rsi
       state: ratKingArmy
+  - type: InstantAction
     event: !type:RatKingRaiseArmyActionEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionRatKingDomain
   name: Rat King's Domain
   description: Spend some hunger to release a cloud of ammonia into the air.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 6
     icon:
       sprite: Interface/Actions/actions_rat_king.rsi
       state: ratKingDomain
+  - type: InstantAction
     event: !type:RatKingDomainActionEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionRatKingOrderStay
   name: Stay
   description: Command your army to stand in place.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 1
     icon:
       sprite: Interface/Actions/actions_rat_king.rsi
     iconOn:
       sprite: Interface/Actions/actions_rat_king.rsi
       state: stay
-    event:
-      !type:RatKingOrderActionEvent
-        type: Stay
     priority: 5
+  - type: InstantAction
+    event: !type:RatKingOrderActionEvent
+      type: Stay
 
 - type: entity
+  parent: BaseAction
   id: ActionRatKingOrderFollow
   name: Follow
   description: Command your army to follow you around.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 1
     icon:
       sprite: Interface/Actions/actions_rat_king.rsi
     iconOn:
       sprite: Interface/Actions/actions_rat_king.rsi
       state: follow
-    event:
-      !type:RatKingOrderActionEvent
-      type: Follow
     priority: 6
+  - type: InstantAction
+    event: !type:RatKingOrderActionEvent
+      type: Follow
 
 - type: entity
+  parent: BaseAction
   id: ActionRatKingOrderCheeseEm
   name: Cheese 'Em
   description: Command your army to attack whoever you point at.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 1
     icon:
       sprite: Interface/Actions/actions_rat_king.rsi
     iconOn:
       sprite: Interface/Actions/actions_rat_king.rsi
       state: attack
-    event:
-      !type:RatKingOrderActionEvent
-      type: CheeseEm
     priority: 7
+  - type: InstantAction
+    event: !type:RatKingOrderActionEvent
+      type: CheeseEm
 
 - type: entity
+  parent: BaseAction
   id: ActionRatKingOrderLoose
   name: Loose
   description: Command your army to act at their own will.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 1
     icon:
       sprite: Interface/Actions/actions_rat_king.rsi
     iconOn:
       sprite: Interface/Actions/actions_rat_king.rsi
       state: loose
+    priority: 8
+  - type: InstantAction
     event:
       !type:RatKingOrderActionEvent
       type: Loose
-    priority: 8
index 2b18468ace5feb63e14332f549201cf58173e463..2304c0d386d0b02cb6eabd978e950aab9640ee8d 100644 (file)
   - type: BypassInteractionChecks
 
 - type: entity
-  id: ActionAGhostShowSolar
-  name: Solar Control Interface
-  description: View a Solar Control Interface.
+  abstract: true
+  parent: BaseAction
+  id: BaseAGhostAction
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Structures/Machines/parts.rsi, state: box_0 }
     iconOn: Structures/Machines/parts.rsi/box_2.png
     keywords: [ "AI", "console", "interface" ]
     priority: -10
+
+- type: entity
+  parent: BaseAGhostAction
+  id: ActionAGhostShowSolar
+  name: Solar Control Interface
+  description: View a Solar Control Interface.
+  components:
+  - type: InstantAction
     event: !type:ToggleIntrinsicUIEvent { key: enum.SolarControlConsoleUiKey.Key }
 
 - type: entity
+  parent: BaseAGhostAction
   id: ActionAGhostShowCommunications
   name: Communications Interface
   description: View a Communications Interface.
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Interface/Actions/actions_ai.rsi, state: comms_console }
     iconOn: Interface/Actions/actions_ai.rsi/comms_console.png
     keywords: [ "AI", "console", "interface" ]
     priority: -4
+  - type: InstantAction
     event: !type:ToggleIntrinsicUIEvent { key: enum.CommunicationsConsoleUiKey.Key }
 
 - type: entity
+  parent: BaseAGhostAction
   id: ActionAGhostShowRadar
   name: Mass Scanner Interface
   description: View a Mass Scanner Interface.
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Interface/Actions/actions_ai.rsi, state: mass_scanner }
     iconOn: Interface/Actions/actions_ai.rsi/mass_scanner.png
     keywords: [ "AI", "console", "interface" ]
     priority: -6
+  - type: InstantAction
     event: !type:ToggleIntrinsicUIEvent { key: enum.RadarConsoleUiKey.Key }
 
 - type: entity
+  parent: BaseAGhostAction
   id: ActionAGhostShowCargo
   name: Cargo Ordering Interface
   description: View a Cargo Ordering Interface.
   components:
   - type: InstantAction
-    icon: { sprite: Structures/Machines/parts.rsi, state: box_0 }
-    iconOn: Structures/Machines/parts.rsi/box_2.png
-    keywords: [ "AI", "console", "interface" ]
-    priority: -10
     event: !type:ToggleIntrinsicUIEvent { key: enum.CargoConsoleUiKey.Orders }
 
 - type: entity
+  parent: BaseAGhostAction
   id: ActionAGhostShowCrewMonitoring
   name: Crew Monitoring Interface
   description: View a Crew Monitoring Interface.
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Interface/Actions/actions_ai.rsi, state: crew_monitor }
     iconOn: Interface/Actions/actions_ai.rsi/crew_monitor.png
     keywords: [ "AI", "console", "interface" ]
     priority: -8
+  - type: InstantAction
     event: !type:ToggleIntrinsicUIEvent { key: enum.CrewMonitoringUIKey.Key }
 
 - type: entity
+  parent: BaseAGhostAction
   id: ActionAGhostShowStationRecords
   name: Station Records Interface
   description: View a Station Records Interface.
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Interface/Actions/actions_ai.rsi, state: station_records }
     iconOn: Interface/Actions/actions_ai.rsi/station_records.png
     keywords: [ "AI", "console", "interface" ]
     priority: -7
+  - type: InstantAction
     event: !type:ToggleIntrinsicUIEvent { key: enum.GeneralStationRecordConsoleKey.Key }
index 0f5b2ee3be5fc16095b2da45837470fc73bfc3c8..f3332ef0c515716586a3fcbe01bb15cd03a17b62 100644 (file)
         Brute: 12
 
 - type: entity
+  parent: BaseAction
   id: ActionSpawnRift
   name: Summon Carp Rift
   description: Summons a carp rift that will periodically spawns carps.
   components:
-  - type: InstantAction
+  - type: Action
     icon:
       sprite: Interface/Actions/carp_rift.rsi
       state: icon
-    event: !type:DragonSpawnRiftActionEvent
     useDelay: 1
     priority: 3
+  - type: InstantAction
+    event: !type:DragonSpawnRiftActionEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionDevour
   name: "[color=red]Devour[/color]"
   description: Attempt to break a structure with your jaws or swallow a creature.
   components:
-  - type: EntityTargetAction
+  - type: Action
     icon: { sprite : Interface/Actions/devour.rsi, state: icon }
     iconOn: { sprite : Interface/Actions/devour.rsi, state: icon-on }
-    event: !type:DevourActionEvent
     priority: 1
+  - type: TargetAction
+  - type: EntityTargetAction
+    event: !type:DevourActionEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionDragonsBreath
   name: "[color=orange]Dragon's Breath[/color]"
   description: Spew out flames at anyone foolish enough to attack you!
   components:
-  - type: WorldTargetAction
-    # TODO: actual sprite
+  - type: Action
+    # TODO: actual sprite and iconOn
     icon: { sprite : Objects/Weapons/Guns/Projectiles/magic.rsi, state: fireball }
-    event: !type:ActionGunShootEvent
+    itemIconStyle: BigAction
     priority: 2
+  - type: TargetAction
     checkCanAccess: false
     range: 0
-    itemIconStyle: BigAction
+  - type: WorldTargetAction
+    event: !type:ActionGunShootEvent
index 44212f640e0f3e61b64ded0d9555a6b72e00cdd7..0aee45e28edb069275ea3e942cf71b8e84d93c8f 100644 (file)
         task: SimpleHumanoidHostileCompound
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionToggleGuardian
   name: Toggle Guardian
   description: Either manifests the guardian or recalls it back into your body
   components:
-  - type: InstantAction
+  - type: Action
     icon: Interface/Actions/manifest.png
-    event: !type:GuardianToggleActionEvent
     useDelay: 2
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: InstantAction
+    event: !type:GuardianToggleActionEvent
index 9224d527252d2f3754981ccc76b773eb4a0fabb7..cb0cfdb693964d259e9afdc8304a2799f32549f1 100644 (file)
     - AllowGhostShownByEvent
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionGhostBoo
   name: Boo!
   description: Scare your crew members because of boredom!
   components:
-  - type: InstantAction
+  - type: Action
     icon: Interface/Actions/scream.png
     checkCanInteract: false
-    event: !type:BooActionEvent
     startDelay: true
     useDelay: 120
+  - type: InstantAction
+    event: !type:BooActionEvent
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionToggleLighting
   name: Toggle Lighting
   description: Toggle light rendering to better observe dark areas.
   components:
-  - type: InstantAction
+  - type: Action
     icon: Interface/VerbIcons/light.svg.192dpi.png
     clientExclusive: true
-    checkCanInteract: false
+  - type: InstantAction
     event: !type:ToggleLightingActionEvent
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionToggleFov
   name: Toggle FoV
   description: Toggles field-of-view in order to see what players see.
   components:
-  - type: InstantAction
+  - type: Action
     icon: Interface/VerbIcons/vv.svg.192dpi.png
     clientExclusive: true
-    checkCanInteract: false
+  - type: InstantAction
     event: !type:ToggleFoVActionEvent
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionToggleGhosts
   name: Toggle Ghosts
   description: Toggle the visibility of other ghosts.
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Mobs/Ghosts/ghost_human.rsi, state: icon }
     clientExclusive: true
-    checkCanInteract: false
+  - type: InstantAction
     event: !type:ToggleGhostsActionEvent
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionToggleGhostHearing
   name: Toggle Ghost Hearing
   description: Toggle between hearing all messages and hearing only radio & nearby messages.
   components:
-  - type: InstantAction
-    checkCanInteract: false
+  - type: Action
     icon:
       sprite: Clothing/Ears/Headsets/base.rsi
       state: icon
     iconOn: Interface/Actions/ghostHearingToggled.png
+  - type: InstantAction
     event: !type:ToggleGhostHearingActionEvent
index b6819a18b96462a8cf028c7cf2fb1da2743b6680..925bb3e86c20d06657620134cdd7a0741b645c7a 100644 (file)
@@ -12,7 +12,7 @@
       components:
       - Anchorable
       - Item
-      tags: 
+      tags:
       - Bot # for funny bot moments
     blacklist:
       components:
 
 # actions
 - type: entity
+  parent: BaseAction
   id: ActionDisguiseNoRot
   name: Toggle Rotation
   description: Use this to prevent your disguise from rotating, making it easier to hide in some scenarios.
   components:
-  - type: InstantAction
+  - type: Action
     icon: Interface/VerbIcons/refresh.svg.192dpi.png
     itemIconStyle: BigAction
+  - type: InstantAction
     event: !type:DisguiseToggleNoRotEvent
 
 - type: entity
+  parent: BaseAction
   id: ActionDisguiseAnchor
   name: Toggle Anchored
   description: For many objects you will want to be anchored to not be completely obvious.
   components:
-  - type: InstantAction
+  - type: Action
     icon:
       sprite: Objects/Tools/wrench.rsi
       state: icon
     itemIconStyle: BigAction
+  - type: InstantAction
     event: !type:DisguiseToggleAnchoredEvent
index 1f5136935a0ac9a12a0a79eb2846554c48ea1a35..699d1eec2df2b6b33f105d447d77f1a3c87359e8 100644 (file)
     node: potatoai
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionPAIOpenShop
   name: Software Catalog
   description: Install new software to assist your owner.
   components:
-  - type: InstantAction
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
     icon: Interface/Actions/shop.png
+  - type: InstantAction
     event: !type:PAIShopActionEvent
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionPAIMassScanner
   name: Mass Scanner
   description: View a mass scanner interface.
   components:
-  - type: InstantAction
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
     icon: { sprite: Interface/Actions/actions_ai.rsi, state: mass_scanner }
     itemIconStyle: NoItem
+  - type: InstantAction
     event: !type:OpenUiActionEvent
-        key: enum.RadarConsoleUiKey.Key
+      key: enum.RadarConsoleUiKey.Key
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionPAIPlayMidi
   name: Play MIDI
   description: Open your portable MIDI interface to soothe your owner.
   components:
-  - type: InstantAction
-    checkCanInteract: false
-    checkConsciousness: false
+  - type: Action
     icon: Interface/Actions/pai-midi.png
     itemIconStyle: NoItem
+  - type: InstantAction
     event: !type:OpenUiActionEvent
       key: enum.InstrumentUiKey.Key
 
 - type: entity
+  parent: BaseMentalAction
   id: ActionPAIOpenMap
   name: Open Map
   description: Open your map interface and guide your owner.
   components:
-    - type: InstantAction
-      checkCanInteract: false
-      checkConsciousness: false
-      icon: { sprite: Interface/Actions/pai-map.rsi, state: icon }
-      itemIconStyle: NoItem
-      event: !type:OpenUiActionEvent
-        key: enum.StationMapUiKey.Key
+  - type: Action
+    icon: { sprite: Interface/Actions/pai-map.rsi, state: icon }
+    itemIconStyle: NoItem
+  - type: InstantAction
+    event: !type:OpenUiActionEvent
+      key: enum.StationMapUiKey.Key
index 6539b4f8aa9efa44807dad60bdb1ad0212142eb5..8f4488ba51362cfe01ea2d78ea0caaea6aed0e85 100644 (file)
     sprite: Objects/Specific/Chapel/necronomicon.rsi
 
 - type: entity
+  parent: BaseAction
   id: ActionBibleSummon
   name: Summon familiar
   description: Summon a familiar that will aid you and gain humanlike intelligence once inhabited by a soul.
   components:
-  - type: InstantAction
+  - type: Action
     icon: { sprite: Clothing/Head/Hats/witch.rsi, state: icon }
-    event: !type:SummonActionEvent
     useDelay: 1
+  - type: InstantAction
+    event: !type:SummonActionEvent
index e73c44ff43a75c9be4c57fd58ca9e7e8e8c1bc50..f80ba1a0e3af34e140f95b94d60a5ae7e37050be 100644 (file)
       provided_container: !type:Container { }
 
 - type: entity
+  parent: BaseAction
   id: ActionBorgSwapModule
   name: Swap Module
   description: Select this module, enabling you to use the tools it provides.
   components:
-  - type: InstantAction
+  - type: Action
     itemIconStyle: BigAction
     useDelay: 0.5
+  - type: InstantAction
     event: !type:BorgModuleActionSelectedEvent
 
 - type: entity
index 6fa30b72653ba68d7861bc685fae4221cae24911..c634e86912b1558e1aae9f2b10c784b16e209768 100644 (file)
       Acidic: [Touch]
 
 - type: entity
+  parent: BaseAction
   id: ActionArtifactActivate
   name: Activate Artifact
   description: Activate yourself, causing chaos to those near you.
   components:
-  - type: InstantAction
+  - type: Action
     icon:
       sprite: Objects/Specific/Xenoarchaeology/xeno_artifacts.rsi
       state: ano29
     useDelay: 300
+  - type: InstantAction
     event: !type:ArtifactSelfActivateEvent
index e304d21b87e960e45f1f6085051a4377bf0ac2b5..682ee4ff0b5ddf7d1f9fa4a6d697f1ca1aad0f6e 100644 (file)
       price: 100
 
 - type: entity
+  parent: BaseAction
   id: ActionToggleJetpack
   name: Toggle jetpack
   description: Toggles the jetpack, giving you movement outside the station.
   components:
-  - type: InstantAction
+  - type: Action
     icon:
       sprite: Objects/Tanks/Jetpacks/blue.rsi
       state: icon
@@ -68,6 +69,7 @@
       sprite: Objects/Tanks/Jetpacks/blue.rsi
       state: icon-on
     useDelay: 1.0
+  - type: InstantAction
     event: !type:ToggleJetpackEvent
 
 #Empty blue
index 38c17e0517d5b09eab766d171df3ff8cba01fe9e..9b4481d168062a319f4d285858795381bef3bedf 100644 (file)
@@ -1,11 +1,17 @@
 - type: entity
+  parent: BaseEntitySpellAction
   id: ActionAnimateSpell
   name: Animate
   description: Bring an inanimate object to life!
   components:
-  - type: EntityTargetAction
+  - type: Action
     useDelay: 0
-    itemIconStyle: BigAction
+    sound: !type:SoundPathSpecifier
+      path: /Audio/Magic/staff_animation.ogg
+    icon:
+      sprite: Objects/Magic/magicactions.rsi
+      state: spell_default
+  - type: EntityTargetAction
     whitelist:
       components:
       - Animateable # Currently on: SeatBase, TableBase, ClosetBase, BaseMachine, ConstructibleMachine, BaseComputer, BaseItem, CrateGeneric, StorageTank, GasCanister
       - TegGenerator
       - TegCirculator
       - XenoArtifact
-    canTargetSelf: false
-    interactOnMiss: false
-    sound: !type:SoundPathSpecifier
-      path: /Audio/Magic/staff_animation.ogg
-    icon:
-      sprite: Objects/Magic/magicactions.rsi
-      state: spell_default
     event: !type:ChangeComponentsSpellEvent
       toAdd:
       - type: Animate
index 91db28bca2db156365ade75ee3ec6eab1d2e4dc7..e32bbac3d5adc556b52f56353874ddcf3882bf1f 100644 (file)
@@ -1,30 +1,34 @@
 - type: entity
+  parent: BaseAction
   id: ActionSummonGhosts
   name: Summon Ghosts
   description: Makes all current ghosts permanently visible
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 120
     itemIconStyle: BigAction
     icon:
       sprite: Mobs/Ghosts/ghost_human.rsi
       state: icon
+  - type: InstantAction
     event: !type:ToggleGhostVisibilityToAllEvent
 
 # TODO: Add Whitelist/Blacklist and Component support to EntitySpawnLists (to avoid making huge hardcoded lists like below).
 
 - type: entity
+  parent: BaseAction
   id: ActionSummonGuns
   name: Summon Guns
   description: AK47s for everyone! Places a random gun in front of everybody.
   components:
   - type: Magic
-  - type: InstantAction
+  - type: Action
     useDelay: 300
     itemIconStyle: BigAction
     icon:
       sprite: Objects/Weapons/Guns/Rifles/ak.rsi
       state: base
+  - type: InstantAction
     event: !type:RandomGlobalSpawnSpellEvent
       makeSurvivorAntagonist: true
       spawns:
     sentence: action-speech-spell-summon-guns
 
 - type: entity
+  parent: BaseAction
   id: ActionSummonMagic
   name: Summon Magic
   description: Places a random magical item in front of everybody. Nothing could go wrong!
   components:
   - type: Magic
-  - type: InstantAction
+  - type: Action
     useDelay: 300
     itemIconStyle: BigAction
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: magicmissile
+  - type: InstantAction
     event: !type:RandomGlobalSpawnSpellEvent
       makeSurvivorAntagonist: true
       spawns:
index ae71d8152674f8592d415891ebe26374f76362bc..c0c5a2eec68f25ef4f06b70eef57e810978d6e6e 100644 (file)
@@ -1,9 +1,10 @@
 - type: entity
+  parent: BaseAction
   id: ActionForceWall
   name: forcewall
   description: Creates a magical barrier.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 15
     itemIconStyle: BigAction
     sound: !type:SoundPathSpecifier
@@ -11,6 +12,7 @@
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: shield
+  - type: InstantAction
     event: !type:InstantSpawnSpellEvent
       prototype: WallForce
       posData: !type:TargetInFront
index e01c5a57af172fb063759d8a7a477621a005bb11..ca84465d2b5c8001cb23ebf1087857903fa9643a 100644 (file)
@@ -1,9 +1,10 @@
-- type: entity
+- type: entity
+  parent: BaseAction
   id: ActionKnock
   name: Knock
   description: This spell opens nearby doors.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 10
     itemIconStyle: BigAction
     sound: !type:SoundPathSpecifier
@@ -11,6 +12,7 @@
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: knock
+  - type: InstantAction
     event: !type:KnockSpellEvent
   - type: SpeakOnAction
     sentence: action-speech-spell-knock
index ae0bfa87bccb4ccf0fe6bcc328ad3e06e2281fff..8039f1207410b1bd28fa5e09808e835ae1dfc359 100644 (file)
@@ -1,22 +1,22 @@
 - type: entity
+  parent: BaseEntitySpellAction
   id: ActionMindSwap
   name: Mind Swap
   description: Exchange bodies with another person!
   components:
-  - type: EntityTargetAction
+  - type: Action
     useDelay: 300
-    itemIconStyle: BigAction
-    whitelist:
-      components:
-      - Body
-      - MindContainer
-    canTargetSelf: false
-    interactOnMiss: false
     sound: !type:SoundPathSpecifier
       path: /Audio/Magic/staff_animation.ogg
     icon:
       sprite: Mobs/Species/Human/organs.rsi
       state: brain
+  - type: EntityTargetAction
+    whitelist:
+      components:
+      - Body # this also allows borgs because that supercode uses Body for no reason
+      - PAI # intended to mindswap pAIs and AIs
+      - StationAiCore
     event: !type:MindSwapSpellEvent
   - type: SpeakOnAction
     sentence: action-speech-spell-mind-swap
index 1568b3ab6595ccc8704024cc79533f2fec40e556..87ed8772f0a04608f3cfa860ec0346911c9d4eac 100644 (file)
@@ -1,20 +1,23 @@
-- type: entity
+- type: entity
+  parent: BaseAction
   id: ActionFireball
   name: Fireball
   description: Fires an explosive fireball towards the clicked location.
   components:
   - type: Magic
-  - type: WorldTargetAction
+  - type: Action
     useDelay: 15
     itemIconStyle: BigAction
-    checkCanAccess: false
     raiseOnUser: true
-    range: 60
     sound: !type:SoundPathSpecifier
       path: /Audio/Magic/fireball.ogg
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: fireball
+  - type: TargetAction
+    range: 60
+    checkCanAccess: false
+  - type: WorldTargetAction
     event: !type:ProjectileSpellEvent
       prototype: ProjectileFireball
   - type: SpeakOnAction
       3: ActionFireballIII
 
 - type: entity
-  id: ActionFireballII
   parent: ActionFireball
+  id: ActionFireballII
   name: Fireball II
   description: Fires a fireball, but faster!
   components:
-  - type: WorldTargetAction
-    itemIconStyle: BigAction
-    checkCanAccess: false
-    raiseOnUser: true
-    range: 60
-    sound: !type:SoundPathSpecifier
-      path: /Audio/Magic/fireball.ogg
-    icon:
-      sprite: Objects/Magic/magicactions.rsi
-      state: fireball
-    event: !type:ProjectileSpellEvent
-      prototype: ProjectileFireball
-  - type: SpeakOnAction
-    sentence: action-speech-spell-fireball
+  - type: Action
+    useDelay: 10
 
 - type: entity
-  id: ActionFireballIII
   parent: ActionFireball
+  id: ActionFireballIII
   name: Fireball III
   description: The fastest fireball in the west!
   components:
-    - type: WorldTargetAction
-      itemIconStyle: BigAction
-      checkCanAccess: false
-      raiseOnUser: true
-      range: 60
-      sound: !type:SoundPathSpecifier
-        path: /Audio/Magic/fireball.ogg
-      icon:
-        sprite: Objects/Magic/magicactions.rsi
-        state: fireball
-      event: !type:ProjectileSpellEvent
-        prototype: ProjectileFireball
-    - type: SpeakOnAction
-      sentence: action-speech-spell-fireball
+  - type: Action
+    useDelay: 8
index c5bb96870db02edf1590fc73f6638a76d8d90e8c..72082c38348bd7323904c76d59107c4c0fa2a841 100644 (file)
@@ -1,9 +1,10 @@
 - type: entity
+  parent: BaseAction
   id: ActionItemRecall
   name: Mark Item
   description: Mark a held item to later summon into your hand.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 10
     raiseOnAction: true
     itemIconStyle: BigAction
@@ -17,5 +18,6 @@
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: item_recall
+  - type: InstantAction
     event: !type:OnItemRecallActionEvent
   - type: ItemRecall
index 77f919e6fff61a226f778b8266cd085746015a54..96a90ce2ec427e61453725e4b624f69c1325a23e 100644 (file)
@@ -1,4 +1,5 @@
-- type: entity
+- type: entity
+  parent: BaseAction
   id: ActionRepulse
   name: Repulse
   description: Pushes entities away from the user.
       components:
       - MobMover
       - Item
-  - type: InstantAction
+  - type: Action
     useDelay: 40
     raiseOnAction: true
     itemIconStyle: BigAction
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: repulse
+  - type: InstantAction
     event: !type:RepulseAttractActionEvent
index 7ba357e7c134341a92fc69b991601cf9f25e1b95..551a5bee009738864324f7e9302d8e276221e376 100644 (file)
@@ -1,55 +1,58 @@
-- type: entity
-  id: ActionFlashRune
-  name: Flash Rune
-  description: Summons a rune that flashes if used.
+- type: entity
+  abstract: true
+  parent: BaseAction
+  id: BaseRuneAction
   components:
-  - type: InstantAction
-    useDelay: 10
+  - type: Action
     itemIconStyle: BigAction
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: spell_default
+
+- type: entity
+  parent: BaseRuneAction
+  id: ActionFlashRune
+  name: Flash Rune
+  description: Summons a rune that flashes if used.
+  components:
+  - type: Action
+    useDelay: 10
+  - type: InstantAction
     event: !type:InstantSpawnSpellEvent
       prototype: FlashRune
 
 - type: entity
+  parent: BaseRuneAction
   id: ActionExplosionRune
   name: Explosion Rune
   description: Summons a rune that explodes if used.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 20
-    itemIconStyle: BigAction
-    icon:
-      sprite: Objects/Magic/magicactions.rsi
-      state: spell_default
+  - type: InstantAction
     event: !type:InstantSpawnSpellEvent
       prototype: ExplosionRune
 
 - type: entity
+  parent: BaseRuneAction
   id: ActionIgniteRune
   name: Ignite Rune
   description: Summons a rune that ignites if used.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 15
-    itemIconStyle: BigAction
-    icon:
-      sprite: Objects/Magic/magicactions.rsi
-      state: spell_default
+  - type: InstantAction
     event: !type:InstantSpawnSpellEvent
       prototype: IgniteRune
 
 - type: entity
+  parent: BaseRuneAction
   id: ActionStunRune
   name: Stun Rune
   description: Summons a rune that stuns if used.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 10
-    itemIconStyle: BigAction
-    icon:
-      sprite: Objects/Magic/magicactions.rsi
-      state: spell_default
+  - type: InstantAction
     event: !type:InstantSpawnSpellEvent
       prototype: StunRune
index 5c2860613609d65229b96d40c32a4e6c05fea750..a63f6e1ebfddca78a09dbee566754b29d4f20aa9 100644 (file)
@@ -1,14 +1,16 @@
-- type: entity
+- type: entity
+  parent: BaseAction
   id: ActionSmoke
   name: Smoke
   description: Summons smoke around the user.
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 10
     itemIconStyle: BigAction
     icon:
       sprite: Actions/smokeaction.rsi
       state: smokeaction
+  - type: InstantAction
     event: !type:InstantSpawnSpellEvent
       prototype: WizardSmoke
       posData: !type:TargetInFront
index fb6755212a975da18986399990cc733792f043b5..a151e6f028eac019ecb45037960a17cce1605ccb 100644 (file)
@@ -1,15 +1,18 @@
-- type: entity
+- type: entity
+  parent: BaseAction
   id: ActionSpawnMagicarpSpell
   name: Summon Magicarp
   description: This spell summons three Magi-Carp to your aid! May or may not turn on user.
   components:
-  - type: WorldTargetAction
+  - type: Action
     useDelay: 10
-    range: 4
     itemIconStyle: BigAction
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: spell_default
+  - type: TargetAction
+    range: 4
+  - type: WorldTargetAction
     event: !type:WorldSpawnSpellEvent
       prototypes:
       - id: MobCarpMagic
index ff43db866e745ac7b7dcaa2649094fd1e10c1df1..97ca55e06691b9552a4bdd5349e30c15c8e0b86f 100644 (file)
     - WizardWand
 
 - type: entity
+  parent: BaseAction
   id: ActionRgbLight
   components:
   - type: EntityTargetAction
     whitelist: { components: [ PointLight ] }
-    sound: /Audio/Magic/blink.ogg
     event: !type:ChangeComponentsSpellEvent
       toAdd:
       - type: RgbLightController
index 6e359cc611f2665dcb5cb7aa8c71d692a429def5..ab79a2f5d0cab8014a95434b7807647abd52a0b2 100644 (file)
@@ -1,43 +1,43 @@
-- type: entity
+- type: entity
+  parent: BaseAction
   id: ActionBlink
   name: Blink
   description: Teleport to the clicked location.
   components:
-  - type: WorldTargetAction
+  - type: Action
     useDelay: 10
-    range: 16 # default examine-range.
-    # ^ should probably add better validation that the clicked location is on the users screen somewhere,
+    itemIconStyle: BigAction
     sound: !type:SoundPathSpecifier
       path: /Audio/Magic/blink.ogg
-    itemIconStyle: BigAction
-    checkCanAccess: false
-    repeat: false
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: blink
+  - type: TargetAction
+    range: 16 # default examine-range.
+    # ^ should probably add better validation that the clicked location is on the users screen somewhere,
+    repeat: false
+    checkCanAccess: false
+  - type: WorldTargetAction
     event: !type:TeleportSpellEvent
 
 # TODO: Second level upgrade sometime that allows swapping with all objects
 - type: entity
+  parent: BaseEntitySpellAction
   id: ActionVoidApplause
   name: Void Applause
   description: Clap your hands and swap places with the target.
   components:
-  - type: EntityTargetAction
+  - type: Action
     useDelay: 15
-    range: 16
     sound: !type:SoundPathSpecifier
       path: /Audio/Magic/Eldritch/voidblink.ogg
-    itemIconStyle: BigAction
-    whitelist:
-      components:
-      - Body
-    canTargetSelf: false
-    interactOnMiss: false
-    checkCanAccess: false
-    repeat: false
     icon:
       sprite: Objects/Magic/Eldritch/eldritch_actions.rsi
       state: voidblink
+  - type: TargetAction
+    checkCanAccess: false
+    repeat: false
+    range: 16
+  - type: EntityTargetAction
     event: !type:VoidApplauseSpellEvent
       effect: EffectVoidBlink
index 3eba350dd0f26653828b66f68f73ad717f22cb37..f690288b0e7df700c40826589d625e0fc1cc5b94 100644 (file)
@@ -1,21 +1,40 @@
-- type: entity
-  id: ActionSmite
-  name: Smite
-  description: Instantly gibs a target.
+# Smite-like spell that you click on someone else to use
+- type: entity
+  abstract: true
+  parent: BaseAction
+  id: BaseEntitySpellAction
   components:
-  - type: EntityTargetAction
-    useDelay: 90
+  - type: Action
     itemIconStyle: BigAction
+  - type: TargetAction
+    interactOnMiss: false
+  - type: EntityTargetAction
     whitelist:
       components:
       - Body
     canTargetSelf: false
-    interactOnMiss: false
+
+- type: entity
+  abstract: true
+  parent: BaseEntitySpellAction
+  id: BaseSmiteAction
+  components:
+  - type: Magic
+    requiresClothes: true
+
+- type: entity
+  parent: BaseSmiteAction
+  id: ActionSmite
+  name: Smite
+  description: Instantly gibs a target.
+  components:
+  - type: Action
     sound: !type:SoundPathSpecifier
       path: /Audio/Magic/disintegrate.ogg
     icon:
       sprite: Objects/Magic/magicactions.rsi
       state: gib
+  - type: EntityTargetAction
     event: !type:SmiteSpellEvent
   - type: SpeakOnAction
     sentence: action-speech-spell-smite
 
 # For the Snail
 - type: entity
-  id: ActionSmiteNoReq
   parent: ActionSmite
+  id: ActionSmiteNoReq
   components:
   - type: Magic
+    requiresClothes: false
 
 - type: entity
+  parent: BaseSmiteAction
   id: ActionCluwne
   name: Cluwne's Curse
   description: Turns someone into a Cluwne!
   components:
-  - type: EntityTargetAction
+  - type: Action
     useDelay: 120
-    itemIconStyle: BigAction
-    whitelist:
-      components:
-      - Body
-    canTargetSelf: false
-    interactOnMiss: false
     sound: !type:SoundPathSpecifier
       path: /Audio/Items/brokenbikehorn.ogg
     icon:
       sprite: Clothing/Mask/cluwne.rsi
       state: icon
+  - type: EntityTargetAction
     event: !type:ChangeComponentsSpellEvent
       toAdd:
       - type: Cluwne
     requiresClothes: true
 
 - type: entity
+  parent: BaseSmiteAction
   id: ActionSlippery
   name: Slippery Slope
   description: Make someone slippery.
   components:
-  - type: EntityTargetAction
+  - type: Action
     useDelay: 60
-    itemIconStyle: BigAction
-    whitelist:
-      components:
-      - Body
-    canTargetSelf: false
-    interactOnMiss: false
     sound: !type:SoundPathSpecifier
       path: /Audio/Effects/slip.ogg
     icon:
       sprite: Objects/Specific/Janitorial/soap.rsi
       state: omega-4
+  - type: EntityTargetAction
     event: !type:ChangeComponentsSpellEvent
       toAdd:
       - type: Slippery
index c0074f496f76d1c36733bb7f60110d6b89f7ef59..e95e4a6a289fac6733777c91c0279fdc9467fba3 100644 (file)
@@ -1,14 +1,16 @@
-- type: entity
+- type: entity
+  parent: BaseAction
   id: ActionChargeSpell
   name: Charge
   description: Adds a charge back to your wand
   components:
-  - type: InstantAction
+  - type: Action
     useDelay: 30
     itemIconStyle: BigAction
     icon:
       sprite: Objects/Weapons/Guns/Basic/wands.rsi
       state: nothing
+  - type: InstantAction
     event: !type:ChargeSpellEvent
       charge: 1
   - type: SpeakOnAction
index 243dafabf2f834e7ad213148d6620670831a167c..3fb89a719ceac496b988e329b2ba89c60c28cb22 100644 (file)
     - RubberStampMime
 
 - type: entity
+  parent: BaseAction
   id: ActionMimeInvisibleWall
   name: Create Invisible Wall
   description: Create an invisible wall in front of you, if placeable there.
   components:
-  - type: InstantAction
+  - type: Action
     priority: -1
     useDelay: 30
     icon:
       sprite: Structures/Walls/solid.rsi
       state: full
+  - type: InstantAction
     event: !type:InvisibleWallActionEvent
index e7ab1b4bf3d2c04c27286d6bc51cba8ec9a5b93a..8437d4915b3d34e1097f09526f1ae1068ba0ef2b 100644 (file)
-- action: !type:InstantActionComponent
-    icon: /Textures/Tiles/cropped_parallax.png
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: AlignTileAny
-      tileId: Space
+- tileId: Space
   assignments:
   - 0: 8
-  name: space
-- action: !type:InstantActionComponent
-    icon: Interface/VerbIcons/delete.svg.192dpi.png
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      eraser: True
+- action: ActionMappingEraser
   assignments:
   - 0: 9
-  name: action-name-mapping-erase
-- action: !type:InstantActionComponent
-    icon: /Textures/Tiles/plating.png
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: AlignTileAny
-      tileId: Plating
+- tileId: Plating
   assignments:
   - 0: 7
-  name: plating
-- action: !type:InstantActionComponent
-    icon:
-      entity: Grille
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: Grille
+- tileId: FloorSteel
+  assignments:
+  - 0: 6
+- entity: Firelock
+  assignments:
+  - 0: 5
+- entity: Grille
   assignments:
   - 0: 4
-  name: Grille
-- action: !type:InstantActionComponent
-    icon:
-      entity: WallSolid
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: WallSolid
+- entity: ReinforcedWindow
   assignments:
-  - 0: 0
-  name: WallSolid
-- action: !type:InstantActionComponent
-    icon:
-      entity: Window
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: Window
+  - 0: 3
+- entity: Window
   assignments:
   - 0: 2
-  name: Window
-- action: !type:InstantActionComponent
-    icon:
-      entity: WallReinforced
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: WallReinforced
+- entity: WallReinforced
   assignments:
   - 0: 1
-  name: WallReinforced
-- action: !type:InstantActionComponent
-    icon:
-      entity: ReinforcedWindow
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: ReinforcedWindow
+- entity: WallSolid
   assignments:
-  - 0: 3
-  name: ReinforcedWindow
-- action: !type:InstantActionComponent
-    icon:
-      entity: Firelock
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: Firelock
-  assignments:
-  - 0: 5
-  name: Firelock
-- action: !type:InstantActionComponent
-    icon:
-      entity: GasPipeStraight
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: GasPipeStraight
+  - 0: 0
+- entity: GasPipeStraight
   assignments:
   - 1: 0
-  name: GasPipeStraight
-- action: !type:InstantActionComponent
-    icon:
-      entity: GasPipeBend
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: GasPipeBend
+- entity: GasPipeBend
   assignments:
   - 1: 1
-  name: GasPipeBend
-- action: !type:InstantActionComponent
-    icon:
-      entity: GasPipeTJunction
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: GasPipeTJunction
+- entity: GasPipeTJunction
   assignments:
   - 1: 2
-  name: GasPipeTJunction
-- action: !type:InstantActionComponent
-    icon:
-      entity: GasPipeFourway
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: GasPipeFourway
+- entity: GasPipeFourway
   assignments:
   - 1: 3
-  name: GasPipeFourway
-- action: !type:InstantActionComponent
-    icon:
-      entity: GasVentScrubber
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: GasVentScrubber
+- entity: GasVentScrubber
   assignments:
   - 1: 4
-  name: GasVentScrubber
-- action: !type:InstantActionComponent
-    icon:
-      entity: GasVentPump
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: GasVentPump
+- entity: GasVentPump
   assignments:
   - 1: 5
-  name: GasVentPump
-- action: !type:InstantActionComponent
-    icon:
-      entity: AirAlarm
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: AirAlarm
+- entity: AirAlarm
   assignments:
   - 1: 6
-  name: AirAlarm
-- action: !type:InstantActionComponent
-    icon:
-      entity: FireAlarm
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: FireAlarm
+- entity: FireAlarm
   assignments:
   - 1: 7
-  name: FireAlarm
-- action: !type:InstantActionComponent
-    icon:
-      entity: APCBasic
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: APCBasic
+- entity: APCBasic
   assignments:
   - 2: 3
-  name: APCBasic
-- action: !type:InstantActionComponent
-    icon:
-      entity: CableApcExtension
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: CableApcExtension
+- entity: CableApcExtension
   assignments:
   - 2: 0
-  name: CableApcExtension
-- action: !type:InstantActionComponent
-    icon:
-      entity: CableMV
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: CableMV
+- entity: CableMV
   assignments:
   - 2: 1
-  name: CableMV
-- action: !type:InstantActionComponent
-    icon:
-      entity: CableHV
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: CableHV
+- entity: CableHV
   assignments:
   - 2: 2
-  name: CableHV
-- action: !type:InstantActionComponent
-    icon:
-      entity: SubstationBasic
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: SubstationBasic
+- entity: SubstationBasic
   assignments:
   - 2: 4
-  name: SubstationBasic
-- action: !type:InstantActionComponent
-    icon:
-      entity: Poweredlight
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: Poweredlight
+- entity: Poweredlight
   assignments:
   - 2: 6
-  name: Poweredlight
-- action: !type:InstantActionComponent
-    icon:
-      entity: EmergencyLight
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: PlaceFree
-      entityType: EmergencyLight
+- entity: EmergencyLight
   assignments:
   - 2: 7
-  name: EmergencyLight
-- action: !type:InstantActionComponent
-    icon:
-      entity: SMESBasic
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: SMESBasic
+- entity: SMESBasic
   assignments:
   - 2: 5
-  name: SMESBasic
-- action: !type:InstantActionComponent
-    icon:
-      entity: TableWood
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: TableWood
+- entity: TableWood
   assignments:
   - 3: 0
-  name: TableWood
-- action: !type:InstantActionComponent
-    icon:
-      entity: Table
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: Table
+- entity: Table
   assignments:
   - 3: 1
-  name: Table
-- action: !type:InstantActionComponent
-    icon:
-      entity: ChairWood
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: ChairWood
+- entity: ChairWood
   assignments:
   - 3: 2
-  name: ChairWood
-- action: !type:InstantActionComponent
-    icon:
-      entity: Chair
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: Chair
+- entity: Chair
   assignments:
   - 3: 3
-  name: Chair
-- action: !type:InstantActionComponent
-    icon:
-      entity: ChairOfficeLight
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: ChairOfficeLight
+- entity: ChairOfficeLight
   assignments:
   - 3: 4
-  name: ChairOfficeLight
-- action: !type:InstantActionComponent
-    icon:
-      entity: StoolBar
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: StoolBar
+- entity: StoolBar
   assignments:
   - 3: 6
-  name: StoolBar
-- action: !type:InstantActionComponent
-    icon:
-      entity: Stool
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: Stool
+- entity: Stool
   assignments:
   - 3: 7
-  name: Stool
-- action: !type:InstantActionComponent
-    icon:
-      entity: Rack
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: Rack
+- entity: Rack
   assignments:
   - 3: 8
-  name: Rack
-- action: !type:InstantActionComponent
-    icon:
-      entity: ChairOfficeDark
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: ChairOfficeDark
+- entity: ChairOfficeDark
   assignments:
   - 3: 5
-  name: ChairOfficeDark
-- action: !type:InstantActionComponent
-    icon:
-      entity: LampGold
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: PlaceFree
-      entityType: LampGold
+- entity: LampGold
   assignments:
   - 3: 9
-  name: LampGold
-- action: !type:InstantActionComponent
-    icon:
-      entity: DisposalPipe
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: DisposalPipe
+- entity: DisposalPipe
   assignments:
   - 4: 0
-  name: DisposalPipe
-- action: !type:InstantActionComponent
-    icon:
-      entity: DisposalBend
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: DisposalBend
+- entity: DisposalBend
   assignments:
   - 4: 1
-  name: DisposalBend
-- action: !type:InstantActionComponent
-    icon:
-      entity: DisposalJunction
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: DisposalJunction
+- entity: DisposalJunction
   assignments:
   - 4: 2
-  name: DisposalJunction
-- action: !type:InstantActionComponent
-    icon:
-      entity: DisposalJunctionFlipped
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: DisposalJunctionFlipped
+- entity: DisposalJunctionFlipped
   assignments:
   - 4: 3
-  name: DisposalJunctionFlipped
-- action: !type:InstantActionComponent
-    icon:
-      entity: DisposalRouter
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: DisposalRouter
+- entity: DisposalRouter
   assignments:
   - 4: 4
-  name: DisposalRouter
-- action: !type:InstantActionComponent
-    icon:
-      entity: DisposalRouterFlipped
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: DisposalRouterFlipped
+- entity: DisposalRouterFlipped
   assignments:
   - 4: 5
-  name: DisposalRouterFlipped
-- action: !type:InstantActionComponent
-    icon:
-      entity: DisposalUnit
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: DisposalUnit
+- entity: DisposalUnit
   assignments:
   - 4: 6
-  name: DisposalUnit
-- action: !type:InstantActionComponent
-    icon:
-      entity: DisposalTrunk
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: DisposalTrunk
+- entity: DisposalTrunk
   assignments:
   - 4: 7
-  name: DisposalTrunk
-- action: !type:InstantActionComponent
-    icon:
-      entity: SignDisposalSpace
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: SignDisposalSpace
+- entity: SignDisposalSpace
   assignments:
   - 4: 8
-  name: SignDisposalSpace
-- action: !type:InstantActionComponent
-    icon:
-      entity: Windoor
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: Windoor
+- entity: Windoor
   assignments:
   - 5: 0
-  name: Windoor
-- action: !type:InstantActionComponent
-    icon:
-      entity: WindowDirectional
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: WindowDirectional
+- entity: WindowDirectional
   assignments:
   - 5: 1
   name: WindowDirectional
-- action: !type:InstantActionComponent
-    icon:
-      entity: WindowReinforcedDirectional
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: WindowReinforcedDirectional
+- entity: WindowReinforcedDirectional
   assignments:
   - 5: 2
-  name: WindowReinforcedDirectional
-- action: !type:InstantActionComponent
-    icon:
-      entity: PlasmaWindowDirectional
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: PlasmaWindowDirectional
+- entity: PlasmaWindowDirectional
   assignments:
   - 5: 3
-  name: PlasmaWindowDirectional
-- action: !type:InstantActionComponent
-    icon:
-      entity: Railing
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: Railing
+- entity: Railing
   assignments:
   - 5: 6
-  name: Railing
-- action: !type:InstantActionComponent
-    icon:
-      entity: RailingCorner
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: RailingCorner
+- entity: RailingCorner
   assignments:
   - 5: 7
   name: RailingCorner
-- action: !type:InstantActionComponent
-    icon:
-      entity: RailingCornerSmall
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: RailingCornerSmall
+- entity: RailingCornerSmall
   assignments:
   - 5: 8
-  name: RailingCornerSmall
-- action: !type:InstantActionComponent
-    icon:
-      entity: RailingRound
-    name: RailingRound
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: RailingRound
+- entity: RailingRound
   assignments:
   - 5: 9
-- action: !type:InstantActionComponent
-    icon:
-      entity: AirlockMaintLocked
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: AirlockMaintLocked
+- entity: AirlockMaintLocked
   assignments:
   - 6: 0
-  name: AirlockMaintLocked
-- action: !type:InstantActionComponent
-    icon:
-      entity: AirlockGlass
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: AirlockGlass
+- entity: AirlockGlass
   assignments:
   - 6: 1
-  name: AirlockGlass
-- action: !type:InstantActionComponent
-    icon:
-      entity: AirlockServiceLocked
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: AirlockServiceLocked
+- entity: AirlockServiceLocked
   assignments:
   - 6: 2
-  name: AirlockServiceLocked
-- action: !type:InstantActionComponent
-    icon:
-      entity: AirlockSecurityLocked
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: AirlockSecurityLocked
+- entity: AirlockSecurityLocked
   assignments:
   - 6: 3
-  name: AirlockSecurityLocked
-- action: !type:InstantActionComponent
-    icon:
-      entity: AirlockCommand
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: AirlockCommand
+- entity: AirlockCommand
   assignments:
   - 6: 4
-  name: AirlockCommand
-- action: !type:InstantActionComponent
-    icon:
-      entity: AirlockScience
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: AirlockScience
+- entity: AirlockScience
   assignments:
   - 6: 5
-  name: AirlockScience
-- action: !type:InstantActionComponent
-    icon:
-      entity: AirlockMedical
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: AirlockMedical
+- entity: AirlockMedical
   assignments:
   - 6: 6
-  name: AirlockMedical
-- action: !type:InstantActionComponent
-    icon:
-      entity: AirlockEngineering
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: AirlockEngineering
+- entity: AirlockEngineering
   assignments:
   - 6: 7
-  name: AirlockEngineering
-- action: !type:InstantActionComponent
-    icon:
-      entity: AirlockCargo
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: SnapgridCenter
-      entityType: AirlockCargo
+- entity: AirlockCargo
   assignments:
   - 6: 8
-  name: AirlockCargo
-- action: !type:WorldTargetActionComponent
-    keywords: []
-    repeat: True
-    checkCanAccess: False
-    range: -1
-    icon:
-      sprite: Decals/markings.rsi
-      state: bot_left
-    iconColor: '#EFB34196'
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:PlaceDecalActionEvent
-      snap: True
-      color: '#EFB34196'
-      decalId: BotLeft
-  assignments:
-  - 7: 0
-  name: BotLeft
-- action: !type:WorldTargetActionComponent
-    keywords: []
-    repeat: True
-    checkCanAccess: False
-    range: -1
-    icon:
-      sprite: Decals/markings.rsi
-      state: delivery
-    iconColor: '#EFB34196'
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:PlaceDecalActionEvent
-      snap: True
-      color: '#EFB34196'
-      decalId: Delivery
-  assignments:
-  - 7: 1
-  name: Delivery
-- action: !type:WorldTargetActionComponent
-    keywords: []
-    repeat: True
-    checkCanAccess: False
-    range: -1
-    icon:
-      sprite: Decals/markings.rsi
-      state: warn_full
-    iconColor: '#EFB34196'
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:PlaceDecalActionEvent
-      snap: True
-      color: '#EFB34196'
-      decalId: WarnFull
-  assignments:
-  - 7: 2
-  name: WarnFull
-- action: !type:WorldTargetActionComponent
-    keywords: []
-    repeat: True
-    checkCanAccess: False
-    range: -1
-    icon:
-      sprite: Decals/Overlays/greyscale.rsi
-      state: halftile_overlay
-    iconColor: '#EFB34196'
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:PlaceDecalActionEvent
-      snap: True
-      color: '#EFB34196'
-      decalId: HalfTileOverlayGreyscale
-  assignments:
-  - 7: 3
-  name: HalfTileOverlayGreyscale
-- action: !type:WorldTargetActionComponent
-    keywords: []
-    repeat: True
-    checkCanAccess: False
-    range: -1
-    icon:
-      sprite: Decals/Overlays/greyscale.rsi
-      state: halftile_overlay
-    iconColor: '#334E6DC8'
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:PlaceDecalActionEvent
-      snap: True
-      color: '#334E6DC8'
-      decalId: HalfTileOverlayGreyscale
-  assignments:
-  - 7: 4
-  name: HalfTileOverlayGreyscale (#334E6DC8, 0)
-- action: !type:WorldTargetActionComponent
-    keywords: []
-    repeat: True
-    checkCanAccess: False
-    range: -1
-    icon:
-      sprite: Decals/Overlays/greyscale.rsi
-      state: halftile_overlay
-    iconColor: '#52B4E996'
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:PlaceDecalActionEvent
-      snap: True
-      color: '#52B4E996'
-      decalId: HalfTileOverlayGreyscale
-  assignments:
-  - 7: 5
-  name: HalfTileOverlayGreyscale (#52B4E996, 0)
-- action: !type:WorldTargetActionComponent
-    keywords: []
-    repeat: True
-    checkCanAccess: False
-    range: -1
-    icon:
-      sprite: Decals/Overlays/greyscale.rsi
-      state: halftile_overlay
-    iconColor: '#9FED5896'
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:PlaceDecalActionEvent
-      snap: True
-      color: '#9FED5896'
-      decalId: HalfTileOverlayGreyscale
-  assignments:
-  - 7: 6
-  name: HalfTileOverlayGreyscale (#9FED5896, 0)
-- action: !type:WorldTargetActionComponent
-    keywords: []
-    repeat: True
-    checkCanAccess: False
-    range: -1
-    icon:
-      sprite: Decals/Overlays/greyscale.rsi
-      state: halftile_overlay
-    iconColor: '#DE3A3A96'
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:PlaceDecalActionEvent
-      snap: True
-      color: '#DE3A3A96'
-      decalId: HalfTileOverlayGreyscale
-  assignments:
-  - 7: 7
-  name: HalfTileOverlayGreyscale (#DE3A3A96, 0)
-- action: !type:WorldTargetActionComponent
-    keywords: []
-    repeat: True
-    checkCanAccess: False
-    range: -1
-    icon:
-      sprite: Decals/Overlays/greyscale.rsi
-      state: halftile_overlay
-    iconColor: '#D381C996'
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:PlaceDecalActionEvent
-      snap: True
-      color: '#D381C996'
-      decalId: HalfTileOverlayGreyscale
-  assignments:
-  - 7: 8
-  name: HalfTileOverlayGreyscale (#D381C996, 0)
-- action: !type:WorldTargetActionComponent
-    keywords: []
-    repeat: True
-    checkCanAccess: False
-    range: -1
-    icon:
-      sprite: Decals/Overlays/greyscale.rsi
-      state: halftile_overlay
-    iconColor: '#A4610696'
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:PlaceDecalActionEvent
-      snap: True
-      color: '#A4610696'
-      decalId: HalfTileOverlayGreyscale
-  assignments:
-  - 7: 9
-  name: HalfTileOverlayGreyscale (#A4610696, 0)
-- action: !type:InstantActionComponent
-    icon: /Textures/Tiles/steel.png
-    keywords: []
-    checkCanInteract: False
-    checkConsciousness: False
-    clientExclusive: True
-    autoPopulate: False
-    temporary: True
-    event: !type:StartPlacementActionEvent
-      placementOption: AlignTileAny
-      tileId: FloorSteel
-  assignments:
-  - 0: 6
-  name: steel floor
 ...