]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Action container rejig (#20260)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Sat, 23 Sep 2023 08:49:39 +0000 (04:49 -0400)
committerGitHub <noreply@github.com>
Sat, 23 Sep 2023 08:49:39 +0000 (18:49 +1000)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
50 files changed:
Content.Client/Actions/ActionsSystem.cs
Content.Client/Ghost/GhostSystem.cs
Content.Client/NetworkConfigurator/NetworkConfiguratorLinkOverlay.cs
Content.Client/NetworkConfigurator/Systems/NetworkConfiguratorSystem.cs
Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs
Content.Server/Actions/ActionOnInteractComponent.cs
Content.Server/Actions/ActionOnInteractSystem.cs
Content.Server/Animals/Components/EggLayerComponent.cs
Content.Server/Animals/Systems/EggLayerSystem.cs
Content.Server/Bed/BedSystem.cs
Content.Server/Bed/Sleep/SleepingSystem.cs
Content.Server/Dragon/DragonSystem.cs
Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
Content.Server/Ghost/GhostSystem.cs
Content.Server/Light/EntitySystems/HandheldLightSystem.cs
Content.Server/Magic/MagicSystem.cs
Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs
Content.Server/Polymorph/Systems/PolymorphSystem.cs
Content.Server/Revenant/EntitySystems/RevenantSystem.cs
Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
Content.Server/Store/Systems/StoreSystem.Ui.cs
Content.Server/UserInterface/IntrinsicUISystem.cs
Content.Server/Zombies/PendingZombieComponent.cs
Content.Shared/Actions/ActionContainerComponent.cs [new file with mode: 0644]
Content.Shared/Actions/ActionContainerSystem.cs [new file with mode: 0644]
Content.Shared/Actions/ActionEvents.cs
Content.Shared/Actions/ActionsComponent.cs
Content.Shared/Actions/BaseActionComponent.cs
Content.Shared/Actions/SharedActionsSystem.cs
Content.Shared/Bed/Sleep/SharedSleepingSystem.cs
Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs
Content.Shared/Clothing/MagbootsComponent.cs
Content.Shared/CombatMode/CombatModeComponent.cs
Content.Shared/Devour/SharedDevourSystem.cs
Content.Shared/Ghost/GhostComponent.cs
Content.Shared/Implants/Components/SubdermalImplantComponent.cs
Content.Shared/Implants/SharedSubdermalImplantSystem.cs
Content.Shared/Light/Components/UnpoweredFlashlightComponent.cs
Content.Shared/Mech/Components/MechComponent.cs
Content.Shared/Mech/EntitySystems/SharedMechSystem.cs
Content.Shared/Mobs/Components/MobStateActionsComponent.cs
Content.Shared/Mobs/Systems/MobStateActionsSystem.cs
Content.Shared/Movement/Components/JetpackComponent.cs
Content.Shared/Revenant/Components/RevenantComponent.cs
Content.Shared/Speech/Components/MeleeSpeechComponent.cs
Content.Shared/Spider/SharedSpiderSystem.cs
Content.Shared/Spider/SpiderComponent.cs
Resources/Prototypes/Actions/types.yml
Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml

index 07b0e6331d19d24f6f48a75556e8a5b49d904a21..41db0ad7ceb3684c7593b135840481e426910a58 100644 (file)
@@ -4,7 +4,6 @@ using Content.Shared.Actions;
 using JetBrains.Annotations;
 using Robust.Client.GameObjects;
 using Robust.Client.Player;
-using Robust.Shared.Containers;
 using Robust.Shared.ContentPack;
 using Robust.Shared.GameStates;
 using Robust.Shared.Input.Binding;
@@ -28,8 +27,8 @@ namespace Content.Client.Actions
         [Dependency] private readonly ISerializationManager _serialization = default!;
         [Dependency] private readonly MetaDataSystem _metaData = default!;
 
-        public event Action<EntityUid>? ActionAdded;
-        public event Action<EntityUid>? ActionRemoved;
+        public event Action<EntityUid>? OnActionAdded;
+        public event Action<EntityUid>? OnActionRemoved;
         public event OnActionReplaced? ActionReplaced;
         public event Action? ActionsUpdated;
         public event Action<ActionsComponent>? LinkActions;
@@ -37,11 +36,8 @@ namespace Content.Client.Actions
         public event Action? ClearAssignments;
         public event Action<List<SlotAssignment>>? AssignSlot;
 
-        /// <summary>
-        ///     Queue of entities with <see cref="ActionsComponent"/> that needs to be updated after
-        ///     handling a state.
-        /// </summary>
-        private readonly Queue<EntityUid> _actionHoldersQueue = new();
+        private readonly List<EntityUid> _removed = new();
+        private readonly List<(EntityUid, BaseActionComponent?)> _added = new();
 
         public override void Initialize()
         {
@@ -49,87 +45,148 @@ namespace Content.Client.Actions
             SubscribeLocalEvent<ActionsComponent, PlayerAttachedEvent>(OnPlayerAttached);
             SubscribeLocalEvent<ActionsComponent, PlayerDetachedEvent>(OnPlayerDetached);
             SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleComponentState);
+
+            SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState);
+            SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState);
+            SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState);
         }
 
-        public override void Dirty(EntityUid? actionId)
+        private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
         {
-            var action = GetActionData(actionId);
-            if (_playerManager.LocalPlayer?.ControlledEntity != action?.AttachedEntity)
+            if (args.Current is not InstantActionComponentState state)
                 return;
 
-            base.Dirty(actionId);
-            ActionsUpdated?.Invoke();
+            BaseHandleState<InstantActionComponent>(uid, component, state);
         }
 
-        private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
+        private void OnEntityTargetHandleState(EntityUid uid, EntityTargetActionComponent component, ref ComponentHandleState args)
         {
-            if (args.Current is not ActionsComponentState state)
+            if (args.Current is not EntityTargetActionComponentState state)
                 return;
 
-            component.Actions.Clear();
-            component.Actions.UnionWith(EnsureEntitySet<ActionsComponent>(state.Actions, uid));
+            component.Whitelist = state.Whitelist;
+            component.CanTargetSelf = state.CanTargetSelf;
+            BaseHandleState<EntityTargetActionComponent>(uid, component, state);
+        }
 
-            _actionHoldersQueue.Enqueue(uid);
+        private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent component, ref ComponentHandleState args)
+        {
+            if (args.Current is not WorldTargetActionComponentState state)
+                return;
+
+            BaseHandleState<WorldTargetActionComponent>(uid, component, state);
         }
 
-        protected override void AddActionInternal(EntityUid holderId, EntityUid actionId, BaseContainer container, ActionsComponent holder)
+        private void BaseHandleState<T>(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent
         {
-            // Sometimes the client receives actions from the server, before predicting that newly added components will add
-            // their own shared actions. Just in case those systems ever decided to directly access action properties (e.g.,
-            // action.Toggled), we will remove duplicates:
-            if (container.Contains(actionId))
-            {
-                ActionReplaced?.Invoke(actionId);
-            }
-            else
-            {
-                base.AddActionInternal(holderId, actionId, container, holder);
-            }
+            component.Icon = state.Icon;
+            component.IconOn = state.IconOn;
+            component.IconColor = state.IconColor;
+            component.Keywords = new HashSet<string>(state.Keywords);
+            component.Enabled = state.Enabled;
+            component.Toggled = state.Toggled;
+            component.Cooldown = state.Cooldown;
+            component.UseDelay = state.UseDelay;
+            component.Charges = state.Charges;
+            component.Container = EnsureEntity<T>(state.Container, uid);
+            component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid);
+            component.CheckCanInteract = state.CheckCanInteract;
+            component.ClientExclusive = state.ClientExclusive;
+            component.Priority = state.Priority;
+            component.AttachedEntity = EnsureEntity<T>(state.AttachedEntity, uid);
+            component.AutoPopulate = state.AutoPopulate;
+            component.Temporary = state.Temporary;
+            component.ItemIconStyle = state.ItemIconStyle;
+            component.Sound = state.Sound;
+
+            if (_playerManager.LocalPlayer?.ControlledEntity == component.AttachedEntity)
+                ActionsUpdated?.Invoke();
         }
 
-        public override void AddAction(EntityUid holderId, EntityUid actionId, EntityUid? provider, ActionsComponent? holder = null, BaseActionComponent? action = null, bool dirty = true, BaseContainer? actionContainer = null)
+        protected override void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null)
         {
-            if (!Resolve(holderId, ref holder, false))
+            if (!ResolveActionData(actionId, ref action))
                 return;
 
-            action ??= GetActionData(actionId);
-            if (action == null)
-            {
-                Log.Warning($"No {nameof(BaseActionComponent)} found on entity {actionId}");
+            base.UpdateAction(actionId, action);
+            if (_playerManager.LocalPlayer?.ControlledEntity != action.AttachedEntity)
                 return;
-            }
-
-            dirty &= !action.ClientExclusive;
-            base.AddAction(holderId, actionId, provider, holder, action, dirty, actionContainer);
 
-            if (holderId == _playerManager.LocalPlayer?.ControlledEntity)
-                ActionAdded?.Invoke(actionId);
+            ActionsUpdated?.Invoke();
         }
 
-        public override void RemoveAction(EntityUid holderId, EntityUid? actionId, ActionsComponent? comp = null, BaseActionComponent? action = null, bool dirty = true)
+        private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
         {
-            if (GameTiming.ApplyingState)
+            if (args.Current is not ActionsComponentState state)
                 return;
 
-            if (!Resolve(holderId, ref comp, false))
-                return;
+            _added.Clear();
+            _removed.Clear();
+            var stateEnts = EnsureEntitySet<ActionsComponent>(state.Actions, uid);
+            foreach (var act in component.Actions)
+            {
+                if (!stateEnts.Contains(act) && !IsClientSide(act))
+                    _removed.Add(act);
+            }
+            component.Actions.ExceptWith(_removed);
+
+            foreach (var actionId in stateEnts)
+            {
+                if (!actionId.IsValid())
+                    continue;
 
-            if (actionId == null)
+                if (!component.Actions.Add(actionId))
+                    continue;
+
+                TryGetActionData(actionId, out var action);
+                _added.Add((actionId, action));
+            }
+
+            if (_playerManager.LocalPlayer?.ControlledEntity != uid)
                 return;
 
-            action ??= GetActionData(actionId);
+            foreach (var action in _removed)
+            {
+                OnActionRemoved?.Invoke(action);
+            }
+
+            _added.Sort(ActionComparer);
+
+            foreach (var action in _added)
+            {
+                OnActionAdded?.Invoke(action.Item1);
+            }
+
+            ActionsUpdated?.Invoke();
+        }
+
+        public static int ActionComparer((EntityUid, BaseActionComponent?) a, (EntityUid, BaseActionComponent?) b)
+        {
+            var priorityA = a.Item2?.Priority ?? 0;
+            var priorityB = b.Item2?.Priority ?? 0;
+            if (priorityA != priorityB)
+                return priorityA - priorityB;
+
+            priorityA = a.Item2?.Container?.Id ?? 0;
+            priorityB = b.Item2?.Container?.Id ?? 0;
+            return priorityA - priorityB;
+        }
 
-            if (action is { ClientExclusive: false })
+        protected override void ActionAdded(EntityUid performer, EntityUid actionId, ActionsComponent comp,
+            BaseActionComponent action)
+        {
+            if (_playerManager.LocalPlayer?.ControlledEntity != performer)
                 return;
 
-            dirty &= !action?.ClientExclusive ?? true;
-            base.RemoveAction(holderId, actionId, comp, action, dirty);
+            OnActionAdded?.Invoke(actionId);
+        }
 
-            if (_playerManager.LocalPlayer?.ControlledEntity != holderId)
+        protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
+        {
+            if (_playerManager.LocalPlayer?.ControlledEntity != performer)
                 return;
 
-            if (action == null || action.AutoRemove)
-                ActionRemoved?.Invoke(actionId.Value);
+            OnActionRemoved?.Invoke(actionId);
         }
 
         public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()
@@ -180,9 +237,6 @@ namespace Content.Client.Actions
                 return;
             }
 
-            if (action.Provider != null && Deleted(action.Provider))
-                return;
-
             if (action is not InstantActionComponent instantAction)
                 return;
 
@@ -233,8 +287,8 @@ namespace Content.Client.Actions
 
                 var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
                 var actionId = Spawn(null);
-                AddComp<Component>(actionId, action);
-                AddAction(user, actionId, null);
+                AddComp(actionId, action);
+                AddActionDirect(user, actionId);
 
                 if (map.TryGet<ValueDataNode>("name", out var nameNode))
                     _metaData.SetEntityName(actionId, nameNode.Value);
@@ -254,95 +308,6 @@ namespace Content.Client.Actions
             AssignSlot?.Invoke(assignments);
         }
 
-        public override void Update(float frameTime)
-        {
-            base.Update(frameTime);
-
-            if (_actionHoldersQueue.Count == 0)
-                return;
-
-            var removed = new List<EntityUid>();
-            var added = new List<(EntityUid Id, BaseActionComponent Comp)>();
-            var query = GetEntityQuery<ActionsComponent>();
-            var queue = new Queue<EntityUid>(_actionHoldersQueue);
-            _actionHoldersQueue.Clear();
-
-            while (queue.TryDequeue(out var holderId))
-            {
-                if (!TryGetContainer(holderId, out var container) || container.ExpectedEntities.Count > 0)
-                {
-                    _actionHoldersQueue.Enqueue(holderId);
-                    continue;
-                }
-
-                if (!query.TryGetComponent(holderId, out var holder))
-                    continue;
-
-                removed.Clear();
-                added.Clear();
-
-                foreach (var (act, data) in holder.OldClientActions.ToList())
-                {
-                    if (data.ClientExclusive)
-                        continue;
-
-                    if (!holder.Actions.Contains(act))
-                    {
-                        holder.OldClientActions.Remove(act);
-                        if (data.AutoRemove)
-                            removed.Add(act);
-                    }
-                }
-
-                // Anything that remains is a new action
-                foreach (var newAct in holder.Actions)
-                {
-                    if (!TryGetActionData(newAct, out var serverData))
-                        continue;
-
-                    if (!holder.OldClientActions.ContainsKey(newAct))
-                        added.Add((newAct, serverData));
-
-                    holder.OldClientActions[newAct] = new ActionMetaData(serverData.ClientExclusive, serverData.AutoRemove);
-                }
-
-                if (_playerManager.LocalPlayer?.ControlledEntity != holderId)
-                    return;
-
-                foreach (var action in removed)
-                {
-                    ActionRemoved?.Invoke(action);
-                }
-
-                added.Sort(static (a, b) =>
-                {
-                    if (a.Comp.Priority != b.Comp.Priority)
-                        return a.Comp.Priority - b.Comp.Priority;
-
-                    if (a.Comp.Provider != b.Comp.Provider)
-                    {
-                        if (a.Comp.Provider == null)
-                            return -1;
-
-                        if (b.Comp.Provider == null)
-                            return 1;
-
-                        // uid to int casting... it says "Do NOT use this in content". You can't tell me what to do.
-                        return (int) a.Comp.Provider - (int) b.Comp.Provider;
-                    }
-
-                    return 0;
-                });
-
-                foreach (var action in added)
-                {
-                    ActionAdded?.Invoke(action.Item1);
-                }
-
-                ActionsUpdated?.Invoke();
-            }
-        }
-
         public record struct SlotAssignment(byte Hotbar, byte Slot, EntityUid ActionId);
     }
 }
index 944d4e0de0ddefd6348b6bba283dd20547cb1957..a5353921fa16ec0952dcc7c25f58dd0153d64a9f 100644 (file)
@@ -55,7 +55,7 @@ namespace Content.Client.Ghost
         {
             base.Initialize();
 
-            SubscribeLocalEvent<GhostComponent, ComponentInit>(OnGhostInit);
+            SubscribeLocalEvent<GhostComponent, ComponentStartup>(OnStartup);
             SubscribeLocalEvent<GhostComponent, ComponentRemove>(OnGhostRemove);
             SubscribeLocalEvent<GhostComponent, AfterAutoHandleStateEvent>(OnGhostState);
 
@@ -72,16 +72,10 @@ namespace Content.Client.Ghost
             SubscribeLocalEvent<GhostComponent, ToggleGhostsActionEvent>(OnToggleGhosts);
         }
 
-        private void OnGhostInit(EntityUid uid, GhostComponent component, ComponentInit args)
+        private void OnStartup(EntityUid uid, GhostComponent component, ComponentStartup args)
         {
             if (TryComp(uid, out SpriteComponent? sprite))
-            {
                 sprite.Visible = GhostVisibility;
-            }
-
-            _actions.AddAction(uid, ref component.ToggleLightingActionEntity, component.ToggleGhostsAction);
-            _actions.AddAction(uid, ref component.ToggleFoVActionEntity, component.ToggleFoVAction);
-            _actions.AddAction(uid, ref component.ToggleGhostsActionEntity, component.ToggleGhostsAction);
         }
 
         private void OnToggleLighting(EntityUid uid, GhostComponent component, ToggleLightingActionEvent args)
index d61bb469b36e6ce241c542f9b0c5f7195a8b8c9c..ca135f6ca202ecce612b8b8d9c058790b0189473 100644 (file)
@@ -14,7 +14,8 @@ public sealed class NetworkConfiguratorLinkOverlay : Overlay
     [Dependency] private readonly IRobustRandom _random = default!;
     private readonly DeviceListSystem _deviceListSystem;
 
-    private Dictionary<EntityUid, Color> _colors = new();
+    public Dictionary<EntityUid, Color> Colors = new();
+    public EntityUid? Action;
 
     public override OverlaySpace Space => OverlaySpace.WorldSpace;
 
@@ -25,11 +26,6 @@ public sealed class NetworkConfiguratorLinkOverlay : Overlay
         _deviceListSystem = _entityManager.System<DeviceListSystem>();
     }
 
-    public void ClearEntity(EntityUid uid)
-    {
-        _colors.Remove(uid);
-    }
-
     protected override void Draw(in OverlayDrawArgs args)
     {
         foreach (var tracker in _entityManager.EntityQuery<NetworkConfiguratorActiveLinkOverlayComponent>())
@@ -40,13 +36,13 @@ public sealed class NetworkConfiguratorLinkOverlay : Overlay
                 continue;
             }
 
-            if (!_colors.TryGetValue(tracker.Owner, out var color))
+            if (!Colors.TryGetValue(tracker.Owner, out var color))
             {
                 color = new Color(
                     _random.Next(0, 255),
                     _random.Next(0, 255),
                     _random.Next(0, 255));
-                _colors.Add(tracker.Owner, color);
+                Colors.Add(tracker.Owner, color);
             }
 
             var sourceTransform = _entityManager.GetComponent<TransformComponent>(tracker.Owner);
@@ -70,7 +66,7 @@ public sealed class NetworkConfiguratorLinkOverlay : Overlay
                     continue;
                 }
 
-                args.WorldHandle.DrawLine(sourceTransform.WorldPosition, linkTransform.WorldPosition, _colors[tracker.Owner]);
+                args.WorldHandle.DrawLine(sourceTransform.WorldPosition, linkTransform.WorldPosition, Colors[tracker.Owner]);
             }
         }
     }
index 48b378af040d3f78338879c1e1de376928f7c2a0..af1861dc47a0777e4344a51b664eac10e683835c 100644 (file)
@@ -3,6 +3,7 @@ using Content.Client.Actions;
 using Content.Client.Items;
 using Content.Client.Message;
 using Content.Client.Stylesheets;
+using Content.Shared.Actions;
 using Content.Shared.DeviceNetwork.Components;
 using Content.Shared.DeviceNetwork.Systems;
 using Content.Shared.Input;
@@ -61,26 +62,26 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
 
         if (!toggle)
         {
-            if (_overlay.HasOverlay<NetworkConfiguratorLinkOverlay>())
-            {
-                _overlay.GetOverlay<NetworkConfiguratorLinkOverlay>().ClearEntity(component.ActiveDeviceList.Value);
-            }
-
             RemComp<NetworkConfiguratorActiveLinkOverlayComponent>(component.ActiveDeviceList.Value);
-            if (!EntityQuery<NetworkConfiguratorActiveLinkOverlayComponent>().Any())
-            {
-                _overlay.RemoveOverlay<NetworkConfiguratorLinkOverlay>();
-                _actions.RemoveAction(_playerManager.LocalPlayer.ControlledEntity.Value, Action);
-            }
+            if (!_overlay.TryGetOverlay(out NetworkConfiguratorLinkOverlay? overlay))
+                return;
 
+            overlay.Colors.Remove(component.ActiveDeviceList.Value);
+            if (overlay.Colors.Count > 0)
+                return;
 
+            _actions.RemoveAction(overlay.Action);
+            _overlay.RemoveOverlay<NetworkConfiguratorLinkOverlay>();
             return;
         }
 
         if (!_overlay.HasOverlay<NetworkConfiguratorLinkOverlay>())
         {
-            _overlay.AddOverlay(new NetworkConfiguratorLinkOverlay());
-            _actions.AddAction(_playerManager.LocalPlayer.ControlledEntity.Value, Spawn(Action), null);
+            var overlay = new NetworkConfiguratorLinkOverlay();
+            _overlay.AddOverlay(overlay);
+            var player = _playerManager.LocalPlayer.ControlledEntity.Value;
+            overlay.Action = Spawn(Action);
+            _actions.AddActionDirect(player, overlay.Action.Value);
         }
 
         EnsureComp<NetworkConfiguratorActiveLinkOverlayComponent>(component.ActiveDeviceList.Value);
@@ -88,7 +89,7 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
 
     public void ClearAllOverlays()
     {
-        if (!_overlay.HasOverlay<NetworkConfiguratorLinkOverlay>())
+        if (!_overlay.TryGetOverlay(out NetworkConfiguratorLinkOverlay? overlay))
         {
             return;
         }
@@ -98,12 +99,8 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
             RemCompDeferred<NetworkConfiguratorActiveLinkOverlayComponent>(tracker.Owner);
         }
 
-        _overlay.RemoveOverlay<NetworkConfiguratorLinkOverlay>();
-
-        if (_playerManager.LocalPlayer?.ControlledEntity != null)
-        {
-            _actions.RemoveAction(_playerManager.LocalPlayer.ControlledEntity.Value, Action);
-        }
+        _actions.RemoveAction(overlay.Action);
+        _overlay.RemoveOverlay(overlay);
     }
 
     // hacky solution related to mapping
index 65817aaf4accbe07113682629c4fea7d9f6edb73..fcacc1b05275b3d29875798ca3b246e4ad461dd0 100644 (file)
@@ -111,8 +111,8 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
     {
         if (_actionsSystem != null)
         {
-            _actionsSystem.ActionAdded += OnActionAdded;
-            _actionsSystem.ActionRemoved += OnActionRemoved;
+            _actionsSystem.OnActionAdded += OnActionAdded;
+            _actionsSystem.OnActionRemoved += OnActionRemoved;
             _actionsSystem.ActionReplaced += OnActionReplaced;
             _actionsSystem.ActionsUpdated += OnActionsUpdated;
         }
@@ -319,8 +319,8 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
     {
         if (_actionsSystem != null)
         {
-            _actionsSystem.ActionAdded -= OnActionAdded;
-            _actionsSystem.ActionRemoved -= OnActionRemoved;
+            _actionsSystem.OnActionAdded -= OnActionAdded;
+            _actionsSystem.OnActionRemoved -= OnActionRemoved;
             _actionsSystem.ActionReplaced -= OnActionReplaced;
             _actionsSystem.ActionsUpdated -= OnActionsUpdated;
         }
@@ -522,8 +522,8 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         return filter switch
         {
             Filters.Enabled => action.Enabled,
-            Filters.Item => action.Provider != null && action.Provider != _playerManager.LocalPlayer?.ControlledEntity,
-            Filters.Innate => action.Provider == null || action.Provider == _playerManager.LocalPlayer?.ControlledEntity,
+            Filters.Item => action.Container != null && action.Container != _playerManager.LocalPlayer?.ControlledEntity,
+            Filters.Innate => action.Container == null || action.Container == _playerManager.LocalPlayer?.ControlledEntity,
             Filters.Instant => action is InstantActionComponent,
             Filters.Targeted => action is BaseTargetActionComponent,
             _ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null)
@@ -561,6 +561,9 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         if (_window is not { Disposed: false } || _actionsSystem == null)
             return;
 
+        if (_playerManager.LocalPlayer?.ControlledEntity is not { } player)
+            return;
+
         var search = _window.SearchBar.Text;
         var filters = _window.FilterButton.SelectedKeys;
         var actions = _actionsSystem.GetClientActions();
@@ -583,10 +586,10 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
             if (name.Contains(search, StringComparison.OrdinalIgnoreCase))
                 return true;
 
-            if (action.Comp.Provider == null || action.Comp.Provider == _playerManager.LocalPlayer?.ControlledEntity)
+            if (action.Comp.Container == null || action.Comp.Container == player)
                 return false;
 
-            var providerName = EntityManager.GetComponent<MetaDataComponent>(action.Comp.Provider.Value).EntityName;
+            var providerName = EntityManager.GetComponent<MetaDataComponent>(action.Comp.Container.Value).EntityName;
             return providerName.Contains(search, StringComparison.OrdinalIgnoreCase);
         });
 
@@ -744,11 +747,9 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
     {
         if (_actionsSystem != null && _actionsSystem.TryGetActionData(_menuDragHelper.Dragged?.ActionId, out var action))
         {
-            var entIcon = action.EntityIcon;
-
-            if (entIcon != null)
+            if (action.EntityIcon is {} entIcon)
             {
-                _dragShadow.Texture = EntityManager.GetComponent<SpriteComponent>(entIcon.Value).Icon?
+                _dragShadow.Texture = EntityManager.GetComponent<SpriteComponent>(entIcon).Icon?
                     .GetFrame(RsiDirection.South, 0);
             }
             else if (action.Icon != null)
@@ -902,6 +903,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
             return;
 
         var actions = _actionsSystem.GetClientActions().Where(action => action.Comp.AutoPopulate).ToList();
+        actions.Sort(ActionComparer);
 
         var offset = 0;
         var totalPages = _pages.Count;
@@ -965,11 +967,11 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         action.Toggled = true;
 
         // override "held-item" overlay
-        var provider = action.Provider;
+        var provider = action.Container;
 
         if (action.TargetingIndicator && _overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
         {
-            if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Provider != null)
+            if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Container != null)
             {
                 handOverlay.EntityOverride = provider;
             }
index bbda27f0a0b6ffc76c9434b4ffcc801fe58edb87..5d6ad5c8de4420b20a79b7fe7fcd08ca8d8abea4 100644 (file)
@@ -19,14 +19,6 @@ public class ActionButtonContainer : GridContainer
     public ActionButton this[int index]
     {
         get => (ActionButton) GetChild(index);
-        set
-        {
-            AddChild(value);
-            value.SetPositionInParent(index);
-            value.ActionPressed += ActionPressed;
-            value.ActionUnpressed += ActionUnpressed;
-            value.ActionFocusExited += ActionFocusExited;
-        }
     }
 
     public void SetActionData(params EntityUid?[] actionTypes)
@@ -63,6 +55,16 @@ public class ActionButtonContainer : GridContainer
         button.ActionFocusExited += ActionFocusExited;
     }
 
+    protected override void ChildRemoved(Control newChild)
+    {
+        if (newChild is not ActionButton button)
+            return;
+
+        button.ActionPressed -= ActionPressed;
+        button.ActionUnpressed -= ActionUnpressed;
+        button.ActionFocusExited -= ActionFocusExited;
+    }
+
     public bool TryGetButtonIndex(ActionButton button, out int position)
     {
         if (button.Parent != this)
index 9efe3d6ba038e988b944c35e5a6a793a65063c9c..3a7e15649b0e965960dc5d1197387de64c659c49 100644 (file)
@@ -20,8 +20,8 @@ namespace Content.Server.Actions;
 [RegisterComponent]
 public sealed partial class ActionOnInteractComponent : Component
 {
-    [DataField("actions", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
-    public List<string>? Actions;
+    [DataField(required:true)]
+    public List<EntProtoId>? Actions;
 
-    [DataField("actionEntities")] public List<EntityUid>? ActionEntities;
+    [DataField] public List<EntityUid>? ActionEntities;
 }
index 31f579f7ece173b2121eb99f3b23d0a1989def33..c9a5f4b5d09f0cb2771a059c1d8cf3e32e7c28c5 100644 (file)
@@ -13,6 +13,7 @@ public sealed class ActionOnInteractSystem : EntitySystem
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly SharedActionsSystem _actions = default!;
+    [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
 
     public override void Initialize()
     {
@@ -20,6 +21,19 @@ public sealed class ActionOnInteractSystem : EntitySystem
 
         SubscribeLocalEvent<ActionOnInteractComponent, ActivateInWorldEvent>(OnActivate);
         SubscribeLocalEvent<ActionOnInteractComponent, AfterInteractEvent>(OnAfterInteract);
+        SubscribeLocalEvent<ActionOnInteractComponent, MapInitEvent>(OnMapInit);
+    }
+
+    private void OnMapInit(EntityUid uid, ActionOnInteractComponent component, MapInitEvent args)
+    {
+        if (component.Actions == null)
+            return;
+
+        var comp = EnsureComp<ActionsContainerComponent>(uid);
+        foreach (var id in component.Actions)
+        {
+            _actionContainer.AddAction(uid, id, comp);
+        }
     }
 
     private void OnActivate(EntityUid uid, ActionOnInteractComponent component, ActivateInWorldEvent args)
@@ -35,7 +49,6 @@ public sealed class ActionOnInteractSystem : EntitySystem
         if (act.Event != null)
             act.Event.Performer = args.User;
 
-        act.Provider = uid;
         _actions.PerformAction(args.User, null, actId, act, act.Event, _timing.CurTime, false);
         args.Handled = true;
     }
@@ -65,7 +78,6 @@ public sealed class ActionOnInteractSystem : EntitySystem
                     entAct.Event.Target = args.Target.Value;
                 }
 
-                entAct.Provider = uid;
                 _actions.PerformAction(args.User, null, entActId, entAct, entAct.Event, _timing.CurTime, false);
                 args.Handled = true;
                 return;
@@ -91,7 +103,6 @@ public sealed class ActionOnInteractSystem : EntitySystem
             act.Event.Target = args.ClickLocation;
         }
 
-        act.Provider = uid;
         _actions.PerformAction(args.User, null, actId, act, act.Event, _timing.CurTime, false);
         args.Handled = true;
     }
index 551e665f72e188bb7758d0f245f5ef65ac53d726..33d14c8e360f2e0986ba3e6cf22a97286122ca1c 100644 (file)
@@ -48,4 +48,6 @@ public sealed partial class EggLayerComponent : Component
 
     [DataField("accumulatedFrametime")]
     public float AccumulatedFrametime;
+
+    [DataField] public EntityUid? Action;
 }
index abfd74ad01cd45878ee4963ec3f5e461607a6e9f..5189adb031ddbdd945cafe841cae98a8ce17307f 100644 (file)
@@ -23,7 +23,7 @@ public sealed class EggLayerSystem : EntitySystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<EggLayerComponent, ComponentInit>(OnComponentInit);
+        SubscribeLocalEvent<EggLayerComponent, MapInitEvent>(OnMapInit);
         SubscribeLocalEvent<EggLayerComponent, EggLayInstantActionEvent>(OnEggLayAction);
     }
 
@@ -50,12 +50,9 @@ public sealed class EggLayerSystem : EntitySystem
         }
     }
 
-    private void OnComponentInit(EntityUid uid, EggLayerComponent component, ComponentInit args)
+    private void OnMapInit(EntityUid uid, EggLayerComponent component, MapInitEvent args)
     {
-        if (string.IsNullOrWhiteSpace(component.EggLayAction))
-            return;
-
-        _actions.AddAction(uid, Spawn(component.EggLayAction), uid);
+        _actions.AddAction(uid, ref component.Action, component.EggLayAction);
         component.CurrentEggLayCooldown = _random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax);
     }
 
index c9795928e502c5dc7de16508d83da1af4bcbe739..915bf7de29ee46923e844f644e5aeea4b671a905 100644 (file)
@@ -43,14 +43,11 @@ namespace Content.Server.Bed
             {
                 AddComp<HealOnBuckleHealingComponent>(uid);
                 component.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(component.HealTime);
-                component.SleepAction = Spawn(SleepingSystem.SleepActionId);
-                _actionsSystem.AddAction(args.BuckledEntity, component.SleepAction.Value, null);
+                _actionsSystem.AddAction(args.BuckledEntity, ref component.SleepAction, SleepingSystem.SleepActionId, uid);
                 return;
             }
 
-            if (component.SleepAction != null)
-                _actionsSystem.RemoveAction(args.BuckledEntity, component.SleepAction.Value);
-
+            _actionsSystem.RemoveAction(args.BuckledEntity, component.SleepAction);
             _sleepingSystem.TryWaking(args.BuckledEntity);
             RemComp<HealOnBuckleHealingComponent>(uid);
         }
index 4f7b66d9e585a86c0882598af4a248262d8dc869..30e3a99eeab9cd53cbafb1257b4216905261f8d2 100644 (file)
@@ -23,9 +23,7 @@ namespace Content.Server.Bed.Sleep
     public sealed class SleepingSystem : SharedSleepingSystem
     {
         [Dependency] private readonly IGameTiming _gameTiming = default!;
-        [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
         [Dependency] private readonly IRobustRandom _robustRandom = default!;
-        [Dependency] private readonly ActionsSystem _actionsSystem = default!;
         [Dependency] private readonly PopupSystem _popupSystem = default!;
         [Dependency] private readonly SharedAudioSystem _audio = default!;
         [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
@@ -182,9 +180,8 @@ namespace Content.Server.Bed.Sleep
 
             var tryingToSleepEvent = new TryingToSleepEvent(uid);
             RaiseLocalEvent(uid, ref tryingToSleepEvent);
-            if (tryingToSleepEvent.Cancelled) return false;
-
-            _actionsSystem.RemoveAction(uid, SleepActionId);
+            if (tryingToSleepEvent.Cancelled)
+                return false;
 
             EnsureComp<SleepingComponent>(uid);
             return true;
index f132e0041e125d3f0d3b9d70c3a68c4138521c92..40039be50e2373de282d2432843545f5d77b790f 100644 (file)
@@ -47,7 +47,7 @@ public sealed partial class DragonSystem : EntitySystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<DragonComponent, ComponentStartup>(OnStartup);
+        SubscribeLocalEvent<DragonComponent, MapInitEvent>(OnInit);
         SubscribeLocalEvent<DragonComponent, ComponentShutdown>(OnShutdown);
         SubscribeLocalEvent<DragonComponent, DragonSpawnRiftActionEvent>(OnDragonRift);
         SubscribeLocalEvent<DragonComponent, RefreshMovementSpeedModifiersEvent>(OnDragonMove);
@@ -292,7 +292,7 @@ public sealed partial class DragonSystem : EntitySystem
             _audioSystem.Play(component.SoundRoar, Filter.Pvs(component.Owner, 4f, EntityManager), component.Owner, true, component.SoundRoar.Params);
     }
 
-    private void OnStartup(EntityUid uid, DragonComponent component, ComponentStartup args)
+    private void OnInit(EntityUid uid, DragonComponent component, MapInitEvent args)
     {
         Roar(component);
         _actionsSystem.AddAction(uid, ref component.SpawnRiftActionEntity, component.SpawnRiftAction);
index bdc40f102ef45e3c1022bab84726035e8681b6fa..82ae4b8fa660e4d53db352cc011c2cdcb4b99b93 100644 (file)
@@ -31,7 +31,6 @@ namespace Content.Server.GameTicking.Rules;
 
 public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
 {
-    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly IConfigurationManager _cfg = default!;
     [Dependency] private readonly IChatManager _chatManager = default!;
@@ -55,7 +54,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
 
         SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
         SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
-        SubscribeLocalEvent<ZombifyOnDeathComponent, ZombifySelfActionEvent>(OnZombifySelf);
+        SubscribeLocalEvent<PendingZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
     }
 
     private void OnRoundEndText(RoundEndTextAppendEvent ev)
@@ -191,10 +190,11 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
         InfectInitialPlayers(component);
     }
 
-    private void OnZombifySelf(EntityUid uid, ZombifyOnDeathComponent component, ZombifySelfActionEvent args)
+    private void OnZombifySelf(EntityUid uid, PendingZombieComponent component, ZombifySelfActionEvent args)
     {
         _zombie.ZombifyEntity(uid);
-        _action.RemoveAction(uid, ZombieRuleComponent.ZombifySelfActionPrototype);
+        if (component.Action != null)
+            Del(component.Action.Value);
     }
 
     private float GetInfectedFraction(bool includeOffStation = true, bool includeDead = false)
@@ -322,8 +322,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
             EnsureComp<ZombifyOnDeathComponent>(ownedEntity);
             EnsureComp<IncurableZombieComponent>(ownedEntity);
             var inCharacterName = MetaData(ownedEntity).EntityName;
-            var action = Spawn(ZombieRuleComponent.ZombifySelfActionPrototype);
-            _action.AddAction(mind.OwnedEntity.Value, action, null);
+            _action.AddAction(ownedEntity, ref pending.Action, ZombieRuleComponent.ZombifySelfActionPrototype, ownedEntity);
 
             var message = Loc.GetString("zombie-patientzero-role-greeting");
             var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
index 83c8cca0450c735c99c4e7f13233301866a9169f..c8a410b91fcfad0f30a6331812f30fd770a87065 100644 (file)
@@ -48,6 +48,7 @@ namespace Content.Server.Ghost
             base.Initialize();
 
             SubscribeLocalEvent<GhostComponent, ComponentStartup>(OnGhostStartup);
+            SubscribeLocalEvent<GhostComponent, MapInitEvent>(OnMapInit);
             SubscribeLocalEvent<GhostComponent, ComponentShutdown>(OnGhostShutdown);
 
             SubscribeLocalEvent<GhostComponent, ExaminedEvent>(OnGhostExamine);
@@ -121,18 +122,6 @@ namespace Content.Server.Ghost
 
             var time = _gameTiming.CurTime;
             component.TimeOfDeath = time;
-
-            // TODO ghost: remove once ghosts are persistent and aren't deleted when returning to body
-            var action = _actions.AddAction(uid, ref component.ActionEntity, component.Action);
-            if (action?.UseDelay != null)
-            {
-                action.Cooldown = (time, time + action.UseDelay.Value);
-                Dirty(component.ActionEntity!.Value, action);
-            }
-
-            _actions.AddAction(uid, ref component.ToggleLightingActionEntity, component.ToggleLightingAction);
-            _actions.AddAction(uid, ref component.ToggleFoVActionEntity, component.ToggleFoVAction);
-            _actions.AddAction(uid, ref component.ToggleGhostsActionEntity, component.ToggleGhostsAction);
         }
 
         private void OnGhostShutdown(EntityUid uid, GhostComponent component, ComponentShutdown args)
@@ -151,8 +140,7 @@ namespace Content.Server.Ghost
 
             // Entity can't see ghosts anymore.
             SetCanSeeGhosts(uid, false);
-
-            _actions.RemoveAction(uid, component.ActionEntity);
+            _actions.RemoveAction(uid, component.BooActionEntity);
         }
 
         private void SetCanSeeGhosts(EntityUid uid, bool canSee, EyeComponent? eyeComponent = null)
@@ -166,6 +154,21 @@ namespace Content.Server.Ghost
                 _eye.SetVisibilityMask(uid, eyeComponent.VisibilityMask & ~(int) VisibilityFlags.Ghost, eyeComponent);
         }
 
+        private void OnMapInit(EntityUid uid, GhostComponent component, MapInitEvent args)
+        {
+            if (_actions.AddAction(uid, ref component.BooActionEntity, out var act, component.BooAction)
+                && act.UseDelay != null)
+            {
+                var start = _gameTiming.CurTime;
+                var end = start + act.UseDelay.Value;
+                _actions.SetCooldown(component.BooActionEntity.Value, start, end);
+            }
+
+            _actions.AddAction(uid, ref component.ToggleLightingActionEntity, component.ToggleLightingAction);
+            _actions.AddAction(uid, ref component.ToggleFoVActionEntity, component.ToggleFoVAction);
+            _actions.AddAction(uid, ref component.ToggleGhostsActionEntity, component.ToggleGhostsAction);
+        }
+
         private void OnGhostExamine(EntityUid uid, GhostComponent component, ExaminedEvent args)
         {
             var timeSinceDeath = _gameTiming.RealTime.Subtract(component.TimeOfDeath);
index 3ff115d14e657fa3d9f07524df78a390c5a3c071..ab3a64a5f072740ec33b9e6e341f98be2ca15b4d 100644 (file)
@@ -22,6 +22,7 @@ namespace Content.Server.Light.EntitySystems
     {
         [Dependency] private readonly IPrototypeManager _proto = default!;
         [Dependency] private readonly ActionsSystem _actions = default!;
+        [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
         [Dependency] private readonly PopupSystem _popup = default!;
         [Dependency] private readonly PowerCellSystem _powerCell = default!;
         [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
@@ -97,7 +98,7 @@ namespace Content.Server.Light.EntitySystems
 
         private void OnMapInit(EntityUid uid, HandheldLightComponent component, MapInitEvent args)
         {
-            _actions.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction);
+            _actionContainer.EnsureAction(uid, ref component.ToggleActionEntity, component.ToggleAction);
         }
 
         private void OnShutdown(EntityUid uid, HandheldLightComponent component, ComponentShutdown args)
index c4ed74604d5e98088ad267e03ef35e395aee51d3..8ee034fd323f989c791b6d44eb8959fb84719c42 100644 (file)
@@ -46,12 +46,13 @@ public sealed class MagicSystem : EntitySystem
     [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly ChatSystem _chat = default!;
+    [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<SpellbookComponent, ComponentInit>(OnInit);
+        SubscribeLocalEvent<SpellbookComponent, MapInitEvent>(OnInit);
         SubscribeLocalEvent<SpellbookComponent, UseInHandEvent>(OnUse);
         SubscribeLocalEvent<SpellbookComponent, SpellbookDoAfterEvent>(OnDoAfter);
 
@@ -69,18 +70,36 @@ public sealed class MagicSystem : EntitySystem
         if (args.Handled || args.Cancelled)
             return;
 
-        _actionsSystem.AddActions(args.Args.User, component.Spells, component.LearnPermanently ? null : uid);
         args.Handled = true;
+        if (!component.LearnPermanently)
+        {
+            _actionsSystem.GrantActions(args.Args.User, component.Spells, uid);
+            return;
+        }
+
+        foreach (var (id, charges) in component.SpellActions)
+        {
+            EntityUid? actionId = null;
+            if (_actionsSystem.AddAction(uid, ref actionId, id))
+                _actionsSystem.SetCharges(actionId, charges < 0 ? null : charges);
+        }
+
+        component.SpellActions.Clear();
     }
 
-    private void OnInit(EntityUid uid, SpellbookComponent component, ComponentInit args)
+    private void OnInit(EntityUid uid, SpellbookComponent component, MapInitEvent args)
     {
-        //Negative charges means the spell can be used without it running out.
+        if (!component.LearnPermanently)
+            return;
+
         foreach (var (id, charges) in component.SpellActions)
         {
-            var spell = Spawn(id);
+            var spell = _actionContainer.AddAction(uid, id);
+            if (spell == null)
+                continue;
+
             _actionsSystem.SetCharges(spell, charges < 0 ? null : charges);
-            component.Spells.Add(spell);
+            component.Spells.Add(spell.Value);
         }
     }
 
index d295031a5b4231700e76c9a3e0f22356ffc06d84..c155c538d16681912768030b9ee4f8ea51cce528 100644 (file)
@@ -25,5 +25,7 @@ namespace Content.Server.Polymorph.Components
         /// </summary>
         [DataField("time")]
         public float Time;
+
+        [DataField] public EntityUid? Action;
     }
 }
index d630cbcad7380de2b59c04294e35566ce78b1253..6a90928a44e4ef038aaadc80eb20a7724f2cf507 100644 (file)
@@ -1,3 +1,4 @@
+using System.Linq;
 using Content.Server.Actions;
 using Content.Server.Humanoid;
 using Content.Server.Inventory;
@@ -29,6 +30,7 @@ namespace Content.Server.Polymorph.Systems
         [Dependency] private readonly IMapManager _mapManager = default!;
         [Dependency] private readonly IPrototypeManager _proto = default!;
         [Dependency] private readonly ActionsSystem _actions = default!;
+        [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
         [Dependency] private readonly AudioSystem _audio = default!;
         [Dependency] private readonly SharedBuckleSystem _buckle = default!;
         [Dependency] private readonly ContainerSystem _container = default!;
@@ -53,7 +55,7 @@ namespace Content.Server.Polymorph.Systems
 
             SubscribeLocalEvent<PolymorphableComponent, ComponentStartup>(OnStartup);
             SubscribeLocalEvent<PolymorphableComponent, PolymorphActionEvent>(OnPolymorphActionEvent);
-            SubscribeLocalEvent<PolymorphedEntityComponent, ComponentStartup>(OnStartup);
+            SubscribeLocalEvent<PolymorphedEntityComponent, MapInitEvent>(OnMapInit);
             SubscribeLocalEvent<PolymorphedEntityComponent, BeforeFullyEatenEvent>(OnBeforeFullyEaten);
             SubscribeLocalEvent<PolymorphedEntityComponent, BeforeFullySlicedEvent>(OnBeforeFullySliced);
             SubscribeLocalEvent<PolymorphedEntityComponent, RevertPolymorphActionEvent>(OnRevertPolymorphActionEvent);
@@ -85,7 +87,7 @@ namespace Content.Server.Polymorph.Systems
             Revert(uid, component);
         }
 
-        public void OnStartup(EntityUid uid, PolymorphedEntityComponent component, ComponentStartup args)
+        private void OnMapInit(EntityUid uid, PolymorphedEntityComponent component, MapInitEvent args)
         {
             if (!_proto.TryIndex(component.Prototype, out PolymorphPrototype? proto))
             {
@@ -98,14 +100,11 @@ namespace Content.Server.Polymorph.Systems
             if (proto.Forced)
                 return;
 
-            var actionId = Spawn(RevertPolymorphId);
-            if (_actions.TryGetActionData(actionId, out var action))
+            if (_actions.AddAction(uid, ref component.Action, out var action, RevertPolymorphId))
             {
                 action.EntityIcon = component.Parent;
                 action.UseDelay = TimeSpan.FromSeconds(proto.Delay);
             }
-
-            _actions.AddAction(uid, actionId, null, null, action);
         }
 
         private void OnBeforeFullyEaten(EntityUid uid, PolymorphedEntityComponent comp, BeforeFullyEatenEvent args)
@@ -332,20 +331,26 @@ namespace Content.Server.Polymorph.Systems
             if (!TryComp<PolymorphableComponent>(target, out var polycomp))
                 return;
 
+            polycomp.PolymorphActions ??= new Dictionary<string, EntityUid>();
+            if (polycomp.PolymorphActions.ContainsKey(id))
+                return;
+
             var entproto = _proto.Index<EntityPrototype>(polyproto.Entity);
-            var actionId = Spawn(RevertPolymorphId);
-            if (_actions.TryGetActionData(actionId, out var baseAction) &&
-                baseAction is InstantActionComponent action)
-            {
-                action.Event = new PolymorphActionEvent { Prototype = polyproto };
-                action.Icon = new SpriteSpecifier.EntityPrototype(polyproto.Entity);
-                _metaData.SetEntityName(actionId, Loc.GetString("polymorph-self-action-name", ("target", entproto.Name)));
-                _metaData.SetEntityDescription(actionId, Loc.GetString("polymorph-self-action-description", ("target", entproto.Name)));
 
-                polycomp.PolymorphActions ??= new Dictionary<string, EntityUid>();
-                polycomp.PolymorphActions.Add(id, actionId);
-                _actions.AddAction(target, actionId, target);
-            }
+            EntityUid? actionId = default!;
+            if (!_actions.AddAction(target, ref actionId, RevertPolymorphId, target))
+                return;
+
+            polycomp.PolymorphActions.Add(id, actionId.Value);
+            _metaData.SetEntityName(actionId.Value, Loc.GetString("polymorph-self-action-name", ("target", entproto.Name)));
+            _metaData.SetEntityDescription(actionId.Value, Loc.GetString("polymorph-self-action-description", ("target", entproto.Name)));
+
+            if (!_actions.TryGetActionData(actionId, out var baseAction))
+                return;
+
+            baseAction.Icon = new SpriteSpecifier.EntityPrototype(polyproto.Entity);
+            if (baseAction is InstantActionComponent action)
+                action.Event = new PolymorphActionEvent { Prototype = polyproto };
         }
 
         [PublicAPI]
index 3ff247d6f8a7b9229d83d34557f573cf65901912..0026533c4a1833af89fc055b394d2c6d644b9b69 100644 (file)
@@ -91,7 +91,7 @@ public sealed partial class RevenantSystem : EntitySystem
 
     private void OnMapInit(EntityUid uid, RevenantComponent component, MapInitEvent args)
     {
-        _action.AddAction(uid, Spawn(RevenantShopId), null);
+        _action.AddAction(uid, ref component.Action, RevenantShopId);
     }
 
     private void OnStatusAdded(EntityUid uid, RevenantComponent component, StatusEffectAddedEvent args)
index b0b1437dbed546521583e3e1df4be9fff47c19a7..31d826087d55242adb32eece5ad25b818a1c0a67 100644 (file)
@@ -59,11 +59,10 @@ public sealed partial class BorgSystem
     {
         var chassis = args.ChassisEnt;
 
-        var action = _actions.AddAction(chassis, ref component.ModuleSwapActionEntity, component.ModuleSwapActionId, uid);
-        if (action != null)
+        if (_actions.AddAction(chassis, ref component.ModuleSwapActionEntity, out var action, component.ModuleSwapActionId, uid))
         {
             action.EntityIcon = uid;
-            Dirty(component.ModuleSwapActionEntity!.Value, action);
+            Dirty(component.ModuleSwapActionEntity.Value, action);
         }
 
         if (!TryComp(chassis, out BorgChassisComponent? chassisComp))
index d2fccd1b9c327f2bebc1d5aa038d351e8bdb6033..9600ea6c8fef57f193f6435a027e0391946d7aeb 100644 (file)
@@ -163,7 +163,8 @@ public sealed partial class StoreSystem
         //give action
         if (!string.IsNullOrWhiteSpace(listing.ProductAction))
         {
-            _actions.AddAction(buyer, Spawn(listing.ProductAction), null);
+            // I guess we just allow duplicate actions?
+            _actions.AddAction(buyer, listing.ProductAction);
         }
 
         //broadcast event
index ce89974f631ef8a4e4b8e98e8998873a1da83172..bd449df5f5d879eac86fedccc91dbc573b953301 100644 (file)
@@ -12,7 +12,7 @@ public sealed class IntrinsicUISystem : EntitySystem
 
     public override void Initialize()
     {
-        SubscribeLocalEvent<IntrinsicUIComponent, ComponentStartup>(OnGetActions);
+        SubscribeLocalEvent<IntrinsicUIComponent, MapInitEvent>(InitActions);
         SubscribeLocalEvent<IntrinsicUIComponent, ToggleIntrinsicUIEvent>(OnActionToggle);
     }
 
@@ -21,14 +21,11 @@ public sealed class IntrinsicUISystem : EntitySystem
         args.Handled = InteractUI(uid, args.Key, component);
     }
 
-    private void OnGetActions(EntityUid uid, IntrinsicUIComponent component, ComponentStartup args)
+    private void InitActions(EntityUid uid, IntrinsicUIComponent component, MapInitEvent args)
     {
-        if (!TryComp<ActionsComponent>(uid, out var actions))
-            return;
-
         foreach (var entry in component.UIs)
         {
-            _actionsSystem.AddAction(uid, ref entry.ToggleActionEntity, entry.ToggleAction, null, actions);
+            _actionsSystem.AddAction(uid, ref entry.ToggleActionEntity, entry.ToggleAction);
         }
     }
 
index fb3d6debcd0d55dbeb73e3bdb581cef14d896ede..e112198711729202b90939084083473e7f91991b 100644 (file)
@@ -51,4 +51,6 @@ public sealed partial class PendingZombieComponent : Component
         "zombie-infection-warning",
         "zombie-infection-underway"
     };
+
+    [DataField] public EntityUid? Action;
 }
diff --git a/Content.Shared/Actions/ActionContainerComponent.cs b/Content.Shared/Actions/ActionContainerComponent.cs
new file mode 100644 (file)
index 0000000..c18d1ea
--- /dev/null
@@ -0,0 +1,17 @@
+using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Actions;
+
+/// <summary>
+/// This component indicates that this entity contains actions inside of some container.
+/// </summary>
+[NetworkedComponent, RegisterComponent]
+[Access(typeof(ActionContainerSystem), typeof(SharedActionsSystem))]
+public sealed partial class ActionsContainerComponent : Component
+{
+    public const string ContainerId = "actions";
+
+    [ViewVariables]
+    public Container Container = default!;
+}
diff --git a/Content.Shared/Actions/ActionContainerSystem.cs b/Content.Shared/Actions/ActionContainerSystem.cs
new file mode 100644 (file)
index 0000000..f7446ae
--- /dev/null
@@ -0,0 +1,214 @@
+using System.Diagnostics.CodeAnalysis;
+using Robust.Shared.Containers;
+using Robust.Shared.Network;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Actions;
+
+/// <summary>
+/// Handles storing & spawning action entities in a container.
+/// </summary>
+public sealed class ActionContainerSystem : EntitySystem
+{
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+    [Dependency] private readonly SharedActionsSystem _actions = default!;
+    [Dependency] private readonly INetManager _netMan = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<ActionsContainerComponent, ComponentInit>(OnInit);
+        SubscribeLocalEvent<ActionsContainerComponent, ComponentShutdown>(OnShutdown);
+        SubscribeLocalEvent<ActionsContainerComponent, EntRemovedFromContainerMessage>(OnEntityRemoved);
+        SubscribeLocalEvent<ActionsContainerComponent, EntInsertedIntoContainerMessage>(OnEntityInserted);
+    }
+
+    /// <summary>
+    /// Spawns a new action entity and adds it to the given container.
+    /// </summary>
+    public EntityUid? AddAction(EntityUid uid, string actionPrototypeId, ActionsContainerComponent? comp = null)
+    {
+        EntityUid? result = default;
+        EnsureAction(uid, ref result, actionPrototypeId, comp);
+        return result;
+    }
+
+    /// <summary>
+    /// Ensures that a given entityUid refers to a valid entity action contained by the given container.
+    /// If the entity does not exist, it will attempt to spawn a new action.
+    /// Returns false if the given entity exists, but is not in a valid state.
+    /// </summary>
+    public bool EnsureAction(EntityUid uid,
+        [NotNullWhen(true)] ref EntityUid? actionId,
+        string actionPrototypeId,
+        ActionsContainerComponent? comp = null)
+    {
+        return EnsureAction(uid, ref actionId, out _, actionPrototypeId, comp);
+    }
+
+    /// <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,
+        string? actionPrototypeId,
+        ActionsContainerComponent? comp = null)
+    {
+        action = null;
+
+        DebugTools.Assert(comp == null || comp.Owner == uid);
+        comp ??= EnsureComp<ActionsContainerComponent>(uid);
+
+        if (Exists(actionId))
+        {
+            if (!comp.Container.Contains(actionId.Value))
+            {
+                Log.Error($"Action {ToPrettyString(actionId.Value)} is not contained in the expected container {ToPrettyString(uid)}");
+                return false;
+            }
+
+            if (!_actions.TryGetActionData(actionId, out action))
+                return false;
+
+            DebugTools.Assert(Transform(actionId.Value).ParentUid == uid);
+            DebugTools.Assert(_container.IsEntityInContainer(actionId.Value));
+            DebugTools.Assert(action.Container == uid);
+            return true;
+        }
+
+        // Null prototypes are never valid entities, they mean that someone didn't provide a proper prototype.
+        if (actionPrototypeId == null)
+            return false;
+
+        // Client cannot predict entity spawning.
+        if (_netMan.IsClient && !IsClientSide(uid))
+            return false;
+
+        actionId = Spawn(actionPrototypeId);
+        if (AddAction(uid, actionId.Value, action, comp) && _actions.TryGetActionData(actionId, out action))
+            return true;
+
+        Del(actionId.Value);
+        actionId = null;
+        return false;
+    }
+
+    /// <summary>
+    /// Adds a pre-existing action to an action container.
+    /// </summary>
+    public bool AddAction(EntityUid uid, EntityUid actionId, BaseActionComponent? action = null, ActionsContainerComponent? comp = null)
+    {
+        if (!_actions.ResolveActionData(actionId, ref action))
+            return false;
+
+        if (action.Container != null)
+        {
+            Log.Error($"Attempted to insert an action {ToPrettyString(actionId)} that was already in a container {ToPrettyString(action.Container.Value)}");
+            return false;
+        }
+
+        DebugTools.Assert(comp == null || comp.Owner == uid);
+        comp ??= EnsureComp<ActionsContainerComponent>(uid);
+        if (!comp.Container.Insert(actionId))
+        {
+            Log.Error($"Failed to insert action {ToPrettyString(actionId)} 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);
+
+        return true;
+    }
+
+    private void OnInit(EntityUid uid, ActionsContainerComponent component, ComponentInit args)
+    {
+        component.Container = _container.EnsureContainer<Container>(uid, ActionsContainerComponent.ContainerId);
+    }
+
+    private void OnShutdown(EntityUid uid, ActionsContainerComponent component, ComponentShutdown args)
+    {
+        component.Container.Shutdown();
+    }
+
+    private void OnEntityInserted(EntityUid uid, ActionsContainerComponent component, EntInsertedIntoContainerMessage args)
+    {
+        if (args.Container.ID != ActionsContainerComponent.ContainerId)
+            return;
+
+        if (!_actions.TryGetActionData(args.Entity, out var data))
+            return;
+
+        DebugTools.Assert(data.AttachedEntity == null || data.Container != EntityUid.Invalid);
+        DebugTools.Assert(data.Container == null || data.Container == uid);
+
+        data.Container = uid;
+        Dirty(uid, component);
+
+        var ev = new ActionAddedEvent(args.Entity, data);
+        RaiseLocalEvent(uid, ref ev);
+    }
+
+    private void OnEntityRemoved(EntityUid uid, ActionsContainerComponent component, EntRemovedFromContainerMessage args)
+    {
+        if (args.Container.ID != ActionsContainerComponent.ContainerId)
+            return;
+
+        // Actions should only be getting removed while terminating or moving outside of PVS range.
+        DebugTools.Assert(Terminating(args.Entity)
+                          || _netMan.IsServer // I love gibbing code
+                          || _timing.ApplyingState);
+
+        if (!_actions.TryGetActionData(args.Entity, out var data, false))
+            return;
+
+        // No event - the only entity that should care about this is the entity that the action was provided to.
+        if (data.AttachedEntity != null)
+            _actions.RemoveAction(data.AttachedEntity.Value, args.Entity, null, data);
+
+        var ev = new ActionRemovedEvent(args.Entity, data);
+        RaiseLocalEvent(uid, ref ev);
+
+        if (_netMan.IsServer)
+        {
+            // TODO Actions
+            // log an error or warning here once gibbing code is fixed.
+            QueueDel(uid);
+        }
+    }
+}
+
+/// <summary>
+/// Raised directed at an action container when a new action entity gets inserted.
+/// </summary>
+[ByRefEvent]
+public readonly struct ActionAddedEvent
+{
+    public readonly EntityUid Action;
+    public readonly BaseActionComponent Component;
+
+    public ActionAddedEvent(EntityUid action, BaseActionComponent component)
+    {
+        Action = action;
+        Component = component;
+    }
+}
+
+/// <summary>
+/// Raised directed at an action container when an action entity gets removed.
+/// </summary>
+[ByRefEvent]
+public readonly struct ActionRemovedEvent
+{
+    public readonly EntityUid Action;
+    public readonly BaseActionComponent Component;
+
+    public ActionRemovedEvent(EntityUid action, BaseActionComponent component)
+    {
+        Action = action;
+        Component = component;
+    }
+}
\ No newline at end of file
index c6f873c78a38a0ea0da43381daf361a9645d9604..72a566b8c88ac6f4f71b8c59b0ab8d02ccaa2b04 100644 (file)
@@ -2,7 +2,6 @@ using Content.Shared.Hands;
 using Content.Shared.Inventory;
 using Content.Shared.Inventory.Events;
 using Robust.Shared.Map;
-using Robust.Shared.Network;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Actions;
@@ -18,8 +17,7 @@ namespace Content.Shared.Actions;
 /// </remarks>
 public sealed class GetItemActionsEvent : EntityEventArgs
 {
-    private readonly IEntityManager _entities;
-    private readonly INetManager _net;
+    private readonly ActionContainerSystem _system;
     public readonly SortedSet<EntityUid> Actions = new();
 
     /// <summary>
@@ -27,6 +25,12 @@ public sealed class GetItemActionsEvent : EntityEventArgs
     /// </summary>
     public EntityUid User;
 
+    /// <summary>
+    /// The entity that is being asked to provide the actions. This is used as a default argument to <see cref="AddAction(ref System.Nullable{Robust.Shared.GameObjects.EntityUid},string,Robust.Shared.GameObjects.EntityUid)"/>.
+    /// I.e., if a new action needs to be spawned, then it will be inserted into this entity unless otherwise specified.
+    /// </summary>
+    public EntityUid Provider;
+
     /// <summary>
     ///     Slot flags for the inventory slot that this item got equipped to. Null if not in a slot (i.e., if equipped to hands).
     /// </summary>
@@ -37,25 +41,36 @@ public sealed class GetItemActionsEvent : EntityEventArgs
     /// </summary>
     public bool InHands => SlotFlags == null;
 
-    public GetItemActionsEvent(IEntityManager entities, INetManager net, EntityUid user, SlotFlags? slotFlags = null)
+    public GetItemActionsEvent(ActionContainerSystem system, EntityUid user, EntityUid provider, SlotFlags? slotFlags = null)
     {
-        _entities = entities;
-        _net = net;
+        _system = system;
         User = user;
+        Provider = provider;
         SlotFlags = slotFlags;
     }
 
-    public void AddAction(ref EntityUid? actionId, string? prototypeId)
+    /// <summary>
+    /// Grant the given action. If the EntityUid does not refer to a valid action entity, it will create a new action and
+    /// store it in <see cref="container"/>.
+    /// </summary>
+    public void AddAction(ref EntityUid? actionId, string prototypeId, EntityUid container)
     {
-        if (_entities.Deleted(actionId))
-        {
-            if (string.IsNullOrWhiteSpace(prototypeId) || _net.IsClient)
-                return;
+        if (_system.EnsureAction(container, ref actionId, prototypeId))
+            Actions.Add(actionId.Value);
+    }
 
-            actionId = _entities.Spawn(prototypeId);
-        }
+    /// <summary>
+    /// Grant the given action. If the EntityUid does not refer to a valid action entity, it will create a new action and
+    /// store it in <see cref="Provider"/>.
+    /// </summary>
+    public void AddAction(ref EntityUid? actionId, string prototypeId)
+    {
+        AddAction(ref actionId, prototypeId, Provider);
+    }
 
-        Actions.Add(actionId.Value);
+    public void AddAction(EntityUid actionId)
+    {
+        Actions.Add(actionId);
     }
 }
 
index f7db07a85a8ffbbca5a6f725cc1bb1ecf75c0e15..b810e98d4d323dc941669700e009f4442ab167ae 100644 (file)
@@ -9,13 +9,10 @@ namespace Content.Shared.Actions;
 public sealed partial class ActionsComponent : Component
 {
     /// <summary>
-    ///     Handled on the client to track added and removed actions.
+    /// List of actions currently granted to this entity.
+    /// On the client, this may contain a mixture of client-side and networked entities.
     /// </summary>
-    [ViewVariables] public readonly Dictionary<EntityUid, ActionMetaData> OldClientActions = new();
-
-    [ViewVariables] public readonly HashSet<EntityUid> Actions = new();
-
-    public override bool SendOnlyToOwner => true;
+    [DataField] public HashSet<EntityUid> Actions = new();
 }
 
 [Serializable, NetSerializable]
@@ -29,7 +26,7 @@ public sealed class ActionsComponentState : ComponentState
     }
 }
 
-public readonly record struct ActionMetaData(bool ClientExclusive, bool AutoRemove);
+public readonly record struct ActionMetaData(bool ClientExclusive);
 
 /// <summary>
 ///     Determines how the action icon appears in the hotbar for item actions.
index 5580c19e199ed09f441a5753b45e3e413c075295..a21b801d3cc9a27b008c3e3364d0841803a31933 100644 (file)
@@ -5,6 +5,9 @@ using Robust.Shared.Utility;
 namespace Content.Shared.Actions;
 
 // TODO this should be an IncludeDataFields of each action component type, not use inheritance
+
+// TODO add access attribute. Need to figure out what to do with decal & mapping actions.
+// [Access(typeof(SharedActionsSystem))]
 public abstract partial class BaseActionComponent : Component
 {
     public abstract BaseActionEvent? BaseEvent { get; }
@@ -46,11 +49,13 @@ 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]
     public bool Toggled;
 
     /// <summary>
     ///     The current cooldown on the action.
     /// </summary>
+    // TODO serialization
     public (TimeSpan Start, TimeSpan End)? Cooldown;
 
     /// <summary>
@@ -65,21 +70,34 @@ public abstract partial class BaseActionComponent : Component
     [DataField("charges")] public int? Charges;
 
     /// <summary>
-    ///     The entity that enables / provides this action. If the action is innate, this may be the user themselves. If
-    ///     this action has no provider (e.g., mapping tools), the this will result in broadcast events.
+    /// 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>
-    public EntityUid? Provider;
+    [Access(typeof(ActionContainerSystem), typeof(SharedActionsSystem))]
+    [DataField]
+    public EntityUid? Container;
 
     /// <summary>
-    ///     Entity to use for the action icon. Defaults to using <see cref="Provider"/>.
+    ///     Entity to use for the action icon. If no entity is provided and the <see cref="Container"/> differs from
+    ///     <see cref="AttachedEntity"/>, then it will default to using <see cref="Container"/>
     /// </summary>
     public EntityUid? EntityIcon
     {
-        get => _entityIcon ?? Provider;
-        set => _entityIcon = value;
+        get
+        {
+            if (EntIcon != null)
+                return EntIcon;
+
+            if (AttachedEntity != Container)
+                return Container;
+
+            return null;
+        }
+        set => EntIcon = value;
     }
 
-    private EntityUid? _entityIcon;
+    [DataField]
+    public EntityUid? EntIcon;
 
     /// <summary>
     ///     Whether the action system should block this action if the user cannot currently interact. Some spells or
@@ -88,7 +106,7 @@ public abstract partial class BaseActionComponent : Component
     [DataField("checkCanInteract")] public bool CheckCanInteract = true;
 
     /// <summary>
-    ///     If true, will simply execute the action locally without sending to the server.
+    ///     If true, this will cause the action to only execute locally without ever notifying the server.
     /// </summary>
     [DataField("clientExclusive")] public bool ClientExclusive = false;
 
@@ -107,24 +125,10 @@ public abstract partial class BaseActionComponent : Component
     /// </summary>
     [DataField("autoPopulate")] public bool AutoPopulate = true;
 
-
     /// <summary>
-    ///     Whether or not to automatically remove this action to the action bar when it becomes unavailable.
+    ///     Temporary actions are deleted when they get removed a <see cref="ActionsComponent"/>.
     /// </summary>
-    [DataField("autoRemove")] public bool AutoRemove = true;
-
-    /// <summary>
-    ///     Temporary actions are removed from the action component when removed from the action-bar/GUI. Currently,
-    ///     should only be used for client-exclusive actions (server is not notified).
-    /// </summary>
-    /// <remarks>
-    ///     Currently there is no way for a player to just voluntarily remove actions. They can hide them from the
-    ///     toolbar, but not actually remove them. This is undesirable for things like dynamically added mapping
-    ///     entity-selection actions, as the # of actions would just keep increasing.
-    /// </remarks>
     [DataField("temporary")] public bool Temporary;
-    // TODO re-add support for this
-    // UI refactor seems to have just broken it.
 
     /// <summary>
     ///     Determines the appearance of the entity-icon for actions that are enabled via some entity.
@@ -149,20 +153,22 @@ public abstract class BaseActionComponentState : ComponentState
     public (TimeSpan Start, TimeSpan End)? Cooldown;
     public TimeSpan? UseDelay;
     public int? Charges;
-    public NetEntity? Provider;
+    public NetEntity? Container;
     public NetEntity? EntityIcon;
     public bool CheckCanInteract;
     public bool ClientExclusive;
     public int Priority;
     public NetEntity? AttachedEntity;
     public bool AutoPopulate;
-    public bool AutoRemove;
     public bool Temporary;
     public ItemActionIconStyle ItemIconStyle;
     public SoundSpecifier? Sound;
 
     protected BaseActionComponentState(BaseActionComponent component, IEntityManager entManager)
     {
+        Container = entManager.GetNetEntity(component.Container);
+        EntityIcon = entManager.GetNetEntity(component.EntIcon);
+        AttachedEntity = entManager.GetNetEntity(component.AttachedEntity);
         Icon = component.Icon;
         IconOn = component.IconOn;
         IconColor = component.IconColor;
@@ -172,20 +178,10 @@ public abstract class BaseActionComponentState : ComponentState
         Cooldown = component.Cooldown;
         UseDelay = component.UseDelay;
         Charges = component.Charges;
-
-        // TODO ACTION REFACTOR fix bugs
-        if (entManager.TryGetNetEntity(component.Provider, out var provider))
-            Provider = provider;
-        if (entManager.TryGetNetEntity(component.EntityIcon, out var icon))
-            EntityIcon = icon;
-        if (entManager.TryGetNetEntity(component.AttachedEntity, out var attached))
-            AttachedEntity = attached;
-
         CheckCanInteract = component.CheckCanInteract;
         ClientExclusive = component.ClientExclusive;
         Priority = component.Priority;
         AutoPopulate = component.AutoPopulate;
-        AutoRemove = component.AutoRemove;
         Temporary = component.Temporary;
         ItemIconStyle = component.ItemIconStyle;
         Sound = component.Sound;
index dc0afc8af7626afd74c2f60ae675062daa11d9ca..f32e0dd29cab329a82fc2a294a26ce6339304191 100644 (file)
@@ -10,7 +10,6 @@ using Content.Shared.Inventory.Events;
 using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
 using Robust.Shared.Map;
-using Robust.Shared.Network;
 using Robust.Shared.Timing;
 using Robust.Shared.Utility;
 
@@ -18,18 +17,15 @@ namespace Content.Shared.Actions;
 
 public abstract class SharedActionsSystem : EntitySystem
 {
-    private const string ActionContainerId = "ActionContainer";
-    private const string ProvidedActionContainerId = "ProvidedActionContainer";
-
     [Dependency] protected readonly IGameTiming GameTiming = default!;
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
-    [Dependency] private readonly INetManager _net = default!;
     [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
     [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
     [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
     [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+    [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
 
     public override void Initialize()
     {
@@ -40,26 +36,16 @@ public abstract class SharedActionsSystem : EntitySystem
         SubscribeLocalEvent<ActionsComponent, DidUnequipEvent>(OnDidUnequip);
         SubscribeLocalEvent<ActionsComponent, DidUnequipHandEvent>(OnHandUnequipped);
 
-        SubscribeLocalEvent<ActionsComponent, MapInitEvent>(OnActionsMapInit);
         SubscribeLocalEvent<ActionsComponent, ComponentGetState>(OnActionsGetState);
-        SubscribeLocalEvent<ActionsComponent, ComponentShutdown>(OnActionsShutdown);
 
         SubscribeLocalEvent<InstantActionComponent, ComponentGetState>(OnInstantGetState);
         SubscribeLocalEvent<EntityTargetActionComponent, ComponentGetState>(OnEntityTargetGetState);
         SubscribeLocalEvent<WorldTargetActionComponent, ComponentGetState>(OnWorldTargetGetState);
 
-        SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState);
-        SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState);
-        SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState);
-
         SubscribeLocalEvent<InstantActionComponent, GetActionDataEvent>(OnGetActionData);
         SubscribeLocalEvent<EntityTargetActionComponent, GetActionDataEvent>(OnGetActionData);
         SubscribeLocalEvent<WorldTargetActionComponent, GetActionDataEvent>(OnGetActionData);
 
-        SubscribeLocalEvent<InstantActionComponent, EntGotRemovedFromContainerMessage>(OnEntGotRemovedFromContainer);
-        SubscribeLocalEvent<EntityTargetActionComponent, EntGotRemovedFromContainerMessage>(OnEntGotRemovedFromContainer);
-        SubscribeLocalEvent<WorldTargetActionComponent, EntGotRemovedFromContainerMessage>(OnEntGotRemovedFromContainer);
-
         SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest);
     }
 
@@ -78,117 +64,43 @@ public abstract class SharedActionsSystem : EntitySystem
         args.State = new WorldTargetActionComponentState(component, EntityManager);
     }
 
-    private void BaseHandleState<T>(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent
-    {
-        component.Icon = state.Icon;
-        component.IconOn = state.IconOn;
-        component.IconColor = state.IconColor;
-        component.Keywords = new HashSet<string>(state.Keywords);
-        component.Enabled = state.Enabled;
-        component.Toggled = state.Toggled;
-        component.Cooldown = state.Cooldown;
-        component.UseDelay = state.UseDelay;
-        component.Charges = state.Charges;
-        component.Provider = EnsureEntity<T>(state.Provider, uid);
-        component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid);
-        component.CheckCanInteract = state.CheckCanInteract;
-        component.ClientExclusive = state.ClientExclusive;
-        component.Priority = state.Priority;
-        component.AttachedEntity = EnsureEntity<T>(state.AttachedEntity, uid);
-        component.AutoPopulate = state.AutoPopulate;
-        component.AutoRemove = state.AutoRemove;
-        component.Temporary = state.Temporary;
-        component.ItemIconStyle = state.ItemIconStyle;
-        component.Sound = state.Sound;
-    }
-
-    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;
-
-        BaseHandleState<EntityTargetActionComponent>(uid, component, state);
-        component.Whitelist = state.Whitelist;
-        component.CanTargetSelf = state.CanTargetSelf;
-    }
-
-    private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent component, ref ComponentHandleState args)
-    {
-        if (args.Current is not WorldTargetActionComponentState state)
-            return;
-
-        BaseHandleState<WorldTargetActionComponent>(uid, component, state);
-    }
-
     private void OnGetActionData<T>(EntityUid uid, T component, ref GetActionDataEvent args) where T : BaseActionComponent
     {
         args.Action = component;
     }
 
-    private void OnEntGotRemovedFromContainer<T>(EntityUid uid, T component, EntGotRemovedFromContainerMessage args) where T : BaseActionComponent
-    {
-        if (args.Container.ID != ProvidedActionContainerId)
-            return;
-
-        if (TryComp(component.AttachedEntity, out ActionsComponent? actions))
-        {
-            actions.Actions.Remove(uid);
-            Dirty(component.AttachedEntity.Value, actions);
-
-            if (TryGetActionData(uid, out var action))
-                action.AttachedEntity = null;
-        }
-    }
-
-    public BaseActionComponent? GetActionData(EntityUid? actionId)
+    public bool TryGetActionData(
+        [NotNullWhen(true)] EntityUid? uid,
+        [NotNullWhen(true)] out BaseActionComponent? result,
+        bool logError = true)
     {
-        if (actionId == null)
-            return null;
+        result = null;
+        if (!Exists(uid))
+            return false;
 
-        // TODO split up logic between each action component with different subscriptions
-        // good luck future coder
         var ev = new GetActionDataEvent();
-        RaiseLocalEvent(actionId.Value, ref ev);
-        return ev.Action;
-    }
+        RaiseLocalEvent(uid.Value, ref ev);
+        result = ev.Action;
 
-    public bool TryGetActionData(
-        [NotNullWhen(true)] EntityUid? actionId,
-        [NotNullWhen(true)] out BaseActionComponent? action)
-    {
-        action = null;
-        return actionId != null && (action = GetActionData(actionId)) != null;
-    }
+        if (result != null)
+            return true;
 
-    protected Container EnsureContainer(EntityUid holderId, EntityUid? providerId)
-    {
-        return providerId == null
-            ? _containerSystem.EnsureContainer<Container>(holderId, ActionContainerId)
-            : _containerSystem.EnsureContainer<Container>(providerId.Value, ProvidedActionContainerId);
+        Log.Error($"Failed to get action from action entity: {ToPrettyString(uid.Value)}");
+        return false;
     }
 
-    protected bool TryGetContainer(
-        EntityUid holderId,
-        [NotNullWhen(true)] out BaseContainer? container,
-        ContainerManagerComponent? containerManager = null)
+    public bool ResolveActionData(
+        [NotNullWhen(true)] EntityUid? uid,
+        [NotNullWhen(true)] ref BaseActionComponent? result,
+        bool logError = true)
     {
-        return _containerSystem.TryGetContainer(holderId, ActionContainerId, out container, containerManager);
-    }
+        if (result != null)
+        {
+            DebugTools.Assert(result.Owner == uid);
+            return true;
+        }
 
-    protected bool TryGetProvidedContainer(
-        EntityUid providerId,
-        [NotNullWhen(true)] out BaseContainer? container,
-        ContainerManagerComponent? containerManager = null)
-    {
-        return _containerSystem.TryGetContainer(providerId, ProvidedActionContainerId, out container, containerManager);
+        return TryGetActionData(uid, out result, logError);
     }
 
     public void SetCooldown(EntityUid? actionId, TimeSpan start, TimeSpan end)
@@ -196,8 +108,7 @@ public abstract class SharedActionsSystem : EntitySystem
         if (actionId == null)
             return;
 
-        var action = GetActionData(actionId);
-        if (action == null)
+        if (!TryGetActionData(actionId, out var action))
             return;
 
         action.Cooldown = (start, end);
@@ -218,25 +129,9 @@ public abstract class SharedActionsSystem : EntitySystem
     }
 
     #region ComponentStateManagement
-    public virtual void Dirty(EntityUid? actionId)
+    protected virtual void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null)
     {
-        if (!TryGetActionData(actionId, out var action))
-            return;
-
-        Dirty(actionId.Value, action);
-
-        if (action.AttachedEntity == null)
-            return;
-
-        var ent = action.AttachedEntity;
-
-        if (!TryComp(ent, out ActionsComponent? comp))
-        {
-            action.AttachedEntity = null;
-            return;
-        }
-
-        Dirty(action.AttachedEntity.Value, comp);
+        // See client-side code.
     }
 
     public void SetToggled(EntityUid? actionId, bool toggled)
@@ -248,6 +143,7 @@ public abstract class SharedActionsSystem : EntitySystem
         }
 
         action.Toggled = toggled;
+        UpdateAction(actionId, action);
         Dirty(actionId.Value, action);
     }
 
@@ -260,6 +156,7 @@ public abstract class SharedActionsSystem : EntitySystem
         }
 
         action.Enabled = enabled;
+        UpdateAction(actionId, action);
         Dirty(actionId.Value, action);
     }
 
@@ -272,25 +169,15 @@ public abstract class SharedActionsSystem : EntitySystem
         }
 
         action.Charges = charges;
+        UpdateAction(actionId, action);
         Dirty(actionId.Value, action);
     }
 
-    private void OnActionsMapInit(EntityUid uid, ActionsComponent component, MapInitEvent args)
-    {
-        EnsureContainer(uid, null);
-    }
-
     private void OnActionsGetState(EntityUid uid, ActionsComponent component, ref ComponentGetState args)
     {
         args.State = new ActionsComponentState(GetNetEntitySet(component.Actions));
     }
 
-    private void OnActionsShutdown(EntityUid uid, ActionsComponent component, ComponentShutdown args)
-    {
-        if (TryGetContainer(uid, out var container))
-            container.Shutdown(EntityManager);
-    }
-
     #endregion
 
     #region Execution
@@ -321,8 +208,12 @@ public abstract class SharedActionsSystem : EntitySystem
             return;
         }
 
-        var action = GetActionData(actionEnt);
-        if (action == null || !action.Enabled)
+        if (!TryGetActionData(actionEnt, out var action))
+            return;
+
+        DebugTools.Assert(action.AttachedEntity == user);
+
+        if (!action.Enabled)
             return;
 
         var curTime = GameTiming.CurTime;
@@ -349,16 +240,8 @@ public abstract class SharedActionsSystem : EntitySystem
                 if (!ValidateEntityTarget(user, entityTarget, entityAction))
                     return;
 
-                if (action.Provider == null)
-                {
-                    _adminLogger.Add(LogType.Action,
-                        $"{ToPrettyString(user):user} is performing the {name:action} action targeted at {ToPrettyString(entityTarget):target}.");
-                }
-                else
-                {
-                    _adminLogger.Add(LogType.Action,
-                        $"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(action.Provider.Value):provider}) targeted at {ToPrettyString(entityTarget):target}.");
-                }
+                _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)
                 {
@@ -381,16 +264,8 @@ public abstract class SharedActionsSystem : EntitySystem
                 if (!ValidateWorldTarget(user, entityCoordinatesTarget, worldAction))
                     return;
 
-                if (action.Provider == null)
-                {
-                    _adminLogger.Add(LogType.Action,
-                        $"{ToPrettyString(user):user} is performing the {name:action} action targeted at {entityCoordinatesTarget:target}.");
-                }
-                else
-                {
-                    _adminLogger.Add(LogType.Action,
-                        $"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(action.Provider.Value):provider}) targeted at {entityCoordinatesTarget:target}.");
-                }
+                _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)
                 {
@@ -404,16 +279,8 @@ public abstract class SharedActionsSystem : EntitySystem
                 if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
                     return;
 
-                if (action.Provider == null)
-                {
-                    _adminLogger.Add(LogType.Action,
-                        $"{ToPrettyString(user):user} is performing the {name:action} action.");
-                }
-                else
-                {
-                    _adminLogger.Add(LogType.Action,
-                        $"{ToPrettyString(user):user} is performing the {name:action} action provided by {ToPrettyString(action.Provider.Value):provider}.");
-                }
+                _adminLogger.Add(LogType.Action,
+                    $"{ToPrettyString(user):user} is performing the {name:action} action provided by {ToPrettyString(action.Container ?? user):provider}.");
 
                 performEvent = instantAction.Event;
                 break;
@@ -493,17 +360,18 @@ public abstract class SharedActionsSystem : EntitySystem
 
         var toggledBefore = action.Toggled;
 
+        // Note that attached entity is allowed to be null here.
+        if (action.AttachedEntity != null && action.AttachedEntity != performer)
+        {
+            Log.Error($"{ToPrettyString(performer)} is attempting to perform an action {ToPrettyString(actionId)} that is attached to another entity {ToPrettyString(action.AttachedEntity.Value)}");
+            return;
+        }
+
         if (actionEvent != null)
         {
             // This here is required because of client-side prediction (RaisePredictiveEvent results in event re-use).
             actionEvent.Handled = false;
-            var provider = action.Provider;
-
-            if (provider == null)
-                RaiseLocalEvent(performer, (object) actionEvent, broadcast: true);
-            else
-                RaiseLocalEvent(provider.Value, (object) actionEvent, broadcast: true);
-
+            RaiseLocalEvent(action.Container ?? performer, (object) actionEvent, broadcast: true);
             handled = actionEvent.Handled;
         }
 
@@ -540,91 +408,129 @@ public abstract class SharedActionsSystem : EntitySystem
     #endregion
 
     #region AddRemoveActions
+
+    public EntityUid? AddAction(EntityUid performer,
+        string? actionPrototypeId,
+        EntityUid container = default,
+        ActionsComponent? component = null)
+    {
+        EntityUid? actionId = null;
+        AddAction(performer, ref actionId, out _, actionPrototypeId, container, component);
+        return actionId;
+    }
+
     /// <summary>
-    ///     Add an action to an action holder.
+    ///     Adds an action to an action holder. If the given entity does not exist, it will attempt to spawn one.
     ///     If the holder has no actions component, this will give them one.
     /// </summary>
-    public BaseActionComponent? AddAction(EntityUid holderId, ref EntityUid? actionId, string? actionPrototypeId, EntityUid? provider = null, ActionsComponent? holderComp = null)
-    {
-        if (Deleted(actionId))
-        {
-            if (_net.IsClient)
-                return null;
-
-            if (string.IsNullOrWhiteSpace(actionPrototypeId))
-                return null;
-
-            actionId = Spawn(actionPrototypeId);
-        }
+    /// <param name="performer">Entity to receive the actions</param>
+    /// <param name="actionId">Action entity to add</param>
+    /// <param name="component">The <see cref="performer"/>'s action component of </param>
+    /// <param name="actionPrototypeId">The action entity prototype id to use if <see cref="actionId"/> is invalid.</param>
+    /// <param name="container">The entity that contains/enables this action (e.g., flashlight)..</param>
+    public bool AddAction(EntityUid performer,
+        [NotNullWhen(true)] ref EntityUid? actionId,
+        string? actionPrototypeId,
+        EntityUid container = default,
+        ActionsComponent? component = null)
+    {
+        return AddAction(performer, ref actionId, out _, actionPrototypeId, container, component);
+    }
+
+    /// <inheritdoc cref="AddAction(Robust.Shared.GameObjects.EntityUid,ref System.Nullable{Robust.Shared.GameObjects.EntityUid},string?,Robust.Shared.GameObjects.EntityUid,Content.Shared.Actions.ActionsComponent?)"/>
+    public bool AddAction(EntityUid performer,
+        [NotNullWhen(true)] ref EntityUid? actionId,
+        [NotNullWhen(true)] out BaseActionComponent? action,
+        string? actionPrototypeId,
+        EntityUid container = default,
+        ActionsComponent? component = null)
+    {
+        if (!container.IsValid())
+            container = performer;
+
+        if (!_actionContainer.EnsureAction(container, ref actionId, out action, actionPrototypeId))
+            return false;
 
-        AddAction(holderId, actionId.Value, provider, holderComp);
-        return GetActionData(actionId);
+        return AddActionDirect(performer, actionId.Value, component, action);
     }
 
     /// <summary>
-    ///     Add an action to an action holder.
-    ///     If the holder has no actions component, this will give them one.
+    ///     Adds a pre-existing action.
     /// </summary>
-    /// <param name="holderId">Entity to receive the actions</param>
-    /// <param name="actionId">Action entity to add</param>
-    /// <param name="provider">The entity that enables these actions (e.g., flashlight). May be null (innate actions).</param>
-    /// <param name="holder">Component of <see cref="holderId"/></param>
-    /// <param name="action">Component of <see cref="actionId"/></param>
-    /// <param name="actionContainer">Action container of <see cref="holderId"/></param>
-    public virtual void AddAction(EntityUid holderId, EntityUid actionId, EntityUid? provider, ActionsComponent? holder = null, BaseActionComponent? action = null, bool dirty = true, BaseContainer? actionContainer = null)
-    {
-        action ??= GetActionData(actionId);
-        // TODO remove when action subscriptions are split up
-        if (action == null)
+    public bool AddAction(EntityUid performer,
+        EntityUid actionId,
+        EntityUid container,
+        ActionsComponent? comp = null,
+        BaseActionComponent? action = null,
+        ActionsContainerComponent? containerComp = null
+        )
+    {
+        if (!ResolveActionData(actionId, ref action))
+            return false;
+
+        if (action.Container != container
+            || !Resolve(container, ref containerComp)
+            || !containerComp.Container.Contains(actionId))
         {
-            Log.Warning($"No {nameof(BaseActionComponent)} found on entity {actionId}");
-            return;
+            Log.Error($"Attempted to add an action with an invalid container: {ToPrettyString(actionId)}");
+            return false;
         }
 
-        holder ??= EnsureComp<ActionsComponent>(holderId);
-        action.Provider = provider;
-        action.AttachedEntity = holderId;
-        Dirty(actionId, action);
+        return AddActionDirect(performer, actionId, comp, action);
+    }
 
-        actionContainer ??= EnsureContainer(holderId, provider);
-        AddActionInternal(holderId, actionId, actionContainer, holder);
+    /// <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)
+    {
+        if (!ResolveActionData(actionId, ref action))
+            return false;
+
+        DebugTools.Assert(action.Container == null ||
+                          (TryComp(action.Container, out ActionsContainerComponent? containerComp)
+                           && containerComp.Container.Contains(actionId)));
 
-        if (dirty)
-            Dirty(holderId, holder);
+        DebugTools.Assert(comp == null || comp.Owner == performer);
+        comp ??= EnsureComp<ActionsComponent>(performer);
+        action.AttachedEntity = performer;
+        comp.Actions.Add(actionId);
+        Dirty(actionId, action);
+        Dirty(performer, comp);
+        ActionAdded(performer, actionId, comp, action);
+        return true;
     }
 
-    protected virtual void AddActionInternal(EntityUid holderId, EntityUid actionId, BaseContainer container, ActionsComponent holder)
+    /// <summary>
+    /// This method gets called after a new action got added.
+    /// </summary>
+    protected virtual void ActionAdded(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
     {
-        container.Insert(actionId);
-        holder.Actions.Add(actionId);
-        Dirty(holderId, holder);
+        // See client-side system for UI code.
     }
 
     /// <summary>
-    ///     Add actions to an action component. If the entity has no action component, this will give them one.
+    ///     Grant pre-existing actions. If the entity has no action component, this will give them one.
     /// </summary>
-    /// <param name="holderId">Entity to receive the actions</param>
+    /// <param name="performer">Entity to receive the actions</param>
     /// <param name="actions">The actions to add</param>
-    /// <param name="provider">The entity that enables these actions (e.g., flashlight). May be null (innate actions).</param>
-    public void AddActions(EntityUid holderId, IEnumerable<EntityUid> actions, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
+    /// <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)
     {
-        comp ??= EnsureComp<ActionsComponent>(holderId);
+        if (!Resolve(container, ref containerComp))
+            return;
 
-        var allClientExclusive = true;
-        var container = EnsureContainer(holderId, provider);
+        DebugTools.Assert(comp == null || comp.Owner == performer);
+        comp ??= EnsureComp<ActionsComponent>(performer);
 
         foreach (var actionId in actions)
         {
-            var action = GetActionData(actionId);
-            if (action == null)
-                continue;
-
-            AddAction(holderId, actionId, provider, comp, action, false, container);
-            allClientExclusive = allClientExclusive && action.ClientExclusive;
+            AddAction(performer, actionId, container, comp, containerComp: containerComp);
         }
-
-        if (dirty && !allClientExclusive)
-            Dirty(holderId, comp);
     }
 
     public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetActions(EntityUid holderId, ActionsComponent? actions = null)
@@ -644,74 +550,72 @@ public abstract class SharedActionsSystem : EntitySystem
     /// <summary>
     ///     Remove any actions that were enabled by some other entity. Useful when unequiping items that grant actions.
     /// </summary>
-    public void RemoveProvidedActions(EntityUid holderId, EntityUid provider, ActionsComponent? comp = null)
+    public void RemoveProvidedActions(EntityUid performer, EntityUid container, ActionsComponent? comp = null)
     {
-        if (!Resolve(holderId, ref comp, false))
+        if (!Resolve(performer, ref comp, false))
             return;
 
-        if (!TryGetProvidedContainer(provider, out var container))
-            return;
-
-        foreach (var actionId in container.ContainedEntities.ToArray())
+        foreach (var actionId in comp.Actions.ToArray())
         {
-            var action = GetActionData(actionId);
-            if (action?.Provider == provider)
-                RemoveAction(holderId, actionId, comp, dirty: false);
-        }
+            if (!TryGetActionData(actionId, out var action))
+                return;
 
-        Dirty(holderId, comp);
+            if (action.Container == container)
+                RemoveAction(performer, actionId, comp);
+        }
     }
 
-    public virtual void RemoveAction(EntityUid holderId, EntityUid? actionId, ActionsComponent? comp = null, BaseActionComponent? action = null, bool dirty = true)
+    public void RemoveAction(EntityUid? actionId)
     {
-        if (actionId == null ||
-            !Resolve(holderId, ref comp, false) ||
-            TerminatingOrDeleted(actionId.Value))
-        {
+        if (actionId == null)
             return;
-        }
 
-        action ??= GetActionData(actionId);
-
-        if (TryGetContainer(holderId, out var container) && container.Contains(actionId.Value))
-            QueueDel(actionId.Value);
-
-        comp.Actions.Remove(actionId.Value);
-
-        if (action != null)
-        {
-            action.AttachedEntity = null;
-            Dirty(actionId.Value, action);
-        }
+        if (!TryGetActionData(actionId, out var action))
+            return;
 
-        if (dirty)
-            Dirty(holderId, comp);
+        if (!TryComp(action.AttachedEntity, out ActionsComponent? comp))
+            return;
 
-        DebugTools.Assert(Transform(actionId.Value).ParentUid.IsValid());
+        RemoveAction(action.AttachedEntity.Value, actionId, comp, action);
     }
 
-    /// <summary>
-    ///     Removes all actions with the given prototype id.
-    /// </summary>
-    public void RemoveAction(EntityUid holderId, string actionPrototypeId, ActionsComponent? holderComp = null)
+    public void RemoveAction(EntityUid performer, EntityUid? actionId, ActionsComponent? comp = null, BaseActionComponent? action = null)
     {
-        if (!Resolve(holderId, ref holderComp, false))
+        if (actionId == null)
             return;
 
-        var actions = new List<(EntityUid Id, BaseActionComponent Comp)>();
-        foreach (var (id, comp) in GetActions(holderId))
-        {
-            if (Prototype(id)?.ID == actionPrototypeId)
-                actions.Add((id, comp));
-        }
+        if (!ResolveActionData(actionId, ref action))
+            return;
 
-        if (actions.Count == 0)
+        if (!Resolve(performer, ref comp, false))
+        {
+            DebugTools.AssertNull(action.AttachedEntity);
             return;
+        }
 
-        foreach (var action in actions)
+        if (action.AttachedEntity == null)
         {
-            RemoveAction(holderId, action.Id, holderComp, action.Comp);
+            // action was already removed?
+            DebugTools.Assert(!comp.Actions.Contains(actionId.Value) || GameTiming.ApplyingState);
+            return;
         }
+
+        DebugTools.Assert(action.AttachedEntity == performer);
+        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);
+    }
+
+    /// <summary>
+    /// This method gets called after an action got removed.
+    /// </summary>
+    protected virtual void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
+    {
+        // See client-side system for UI code.
     }
 
     #endregion
@@ -719,34 +623,55 @@ public abstract class SharedActionsSystem : EntitySystem
     #region EquipHandlers
     private void OnDidEquip(EntityUid uid, ActionsComponent component, DidEquipEvent args)
     {
-        var ev = new GetItemActionsEvent(EntityManager, _net, args.Equipee, args.SlotFlags);
+        if (GameTiming.ApplyingState)
+            return;
+
+        var ev = new GetItemActionsEvent(_actionContainer, args.Equipee, args.Equipment, args.SlotFlags);
         RaiseLocalEvent(args.Equipment, ev);
 
         if (ev.Actions.Count == 0)
             return;
 
-        AddActions(args.Equipee, ev.Actions, args.Equipment, component);
+        GrantActions(args.Equipee, ev.Actions, args.Equipment, component);
     }
 
     private void OnHandEquipped(EntityUid uid, ActionsComponent component, DidEquipHandEvent args)
     {
-        var ev = new GetItemActionsEvent(EntityManager, _net, args.User);
+        if (GameTiming.ApplyingState)
+            return;
+
+        var ev = new GetItemActionsEvent(_actionContainer, args.User, args.Equipped);
         RaiseLocalEvent(args.Equipped, ev);
 
         if (ev.Actions.Count == 0)
             return;
 
-        AddActions(args.User, ev.Actions, args.Equipped, component);
+        GrantActions(args.User, ev.Actions, args.Equipped, component);
     }
 
     private void OnDidUnequip(EntityUid uid, ActionsComponent component, DidUnequipEvent args)
     {
+        if (GameTiming.ApplyingState)
+            return;
+
         RemoveProvidedActions(uid, args.Equipment, component);
     }
 
     private void OnHandUnequipped(EntityUid uid, ActionsComponent component, DidUnequipHandEvent args)
     {
+        if (GameTiming.ApplyingState)
+            return;
+
         RemoveProvidedActions(uid, args.Unequipped, component);
     }
     #endregion
+
+    public void SetEntityIcon(EntityUid uid, EntityUid? icon, BaseActionComponent? action = null)
+    {
+        if (!Resolve(uid, ref action))
+            return;
+
+        action.EntityIcon = icon;
+        Dirty(uid, action);
+    }
 }
index bb9c8dcf438471fa6dcfafc394083f6d9154a260..ce6ae4795cd472e829d741ecdcdc5060d145e87f 100644 (file)
@@ -20,7 +20,7 @@ namespace Content.Server.Bed.Sleep
         public override void Initialize()
         {
             base.Initialize();
-            SubscribeLocalEvent<SleepingComponent, ComponentStartup>(OnStartup);
+            SubscribeLocalEvent<SleepingComponent, MapInitEvent>(OnMapInit);
             SubscribeLocalEvent<SleepingComponent, ComponentShutdown>(OnShutdown);
             SubscribeLocalEvent<SleepingComponent, SpeakAttemptEvent>(OnSpeakAttempt);
             SubscribeLocalEvent<SleepingComponent, CanSeeAttemptEvent>(OnSeeAttempt);
@@ -33,24 +33,20 @@ namespace Content.Server.Bed.Sleep
             Dirty(uid, component);
         }
 
-        private void OnStartup(EntityUid uid, SleepingComponent component, ComponentStartup args)
+        private void OnMapInit(EntityUid uid, SleepingComponent component, MapInitEvent args)
         {
             var ev = new SleepStateChangedEvent(true);
             RaiseLocalEvent(uid, ev);
             _blindableSystem.UpdateIsBlind(uid);
+            _actionsSystem.AddAction(uid, ref component.WakeAction, WakeActionId, uid);
 
-            if (_net.IsClient)
-                return;
-
-            component.WakeAction = Spawn(WakeActionId);
+            // TODO remove hardcoded time.
             _actionsSystem.SetCooldown(component.WakeAction, _gameTiming.CurTime, _gameTiming.CurTime + TimeSpan.FromSeconds(15));
-            _actionsSystem.AddAction(uid, component.WakeAction.Value, null);
         }
 
         private void OnShutdown(EntityUid uid, SleepingComponent component, ComponentShutdown args)
         {
             _actionsSystem.RemoveAction(uid, component.WakeAction);
-
             var ev = new SleepStateChangedEvent(false);
             RaiseLocalEvent(uid, ev);
             _blindableSystem.UpdateIsBlind(uid);
index 53f7284bbefcb90c17be968325f6196411a822be..ba006abfdad9cd6501b694fa0464bdb30410a6e0 100644 (file)
@@ -18,6 +18,7 @@ public sealed class ToggleableClothingSystem : EntitySystem
 {
     [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
     [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
+    [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
     [Dependency] private readonly InventorySystem _inventorySystem = default!;
     [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
     [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
@@ -258,10 +259,12 @@ public sealed class ToggleableClothingSystem : EntitySystem
 
     private void OnGetActions(EntityUid uid, ToggleableClothingComponent component, GetItemActionsEvent args)
     {
-        if (component.ClothingUid == null || (args.SlotFlags & component.RequiredFlags) != component.RequiredFlags)
-            return;
-
-        args.AddAction(ref component.ActionEntity, component.Action);
+        if (component.ClothingUid != null
+            && component.ActionEntity != null
+            && (args.SlotFlags & component.RequiredFlags) == component.RequiredFlags)
+        {
+            args.AddAction(component.ActionEntity.Value);
+        }
     }
 
     private void OnInit(EntityUid uid, ToggleableClothingComponent component, ComponentInit args)
@@ -275,7 +278,7 @@ public sealed class ToggleableClothingSystem : EntitySystem
     /// </summary>
     private void OnMapInit(EntityUid uid, ToggleableClothingComponent component, MapInitEvent args)
     {
-        if (component.Container!.ContainedEntity is EntityUid ent)
+        if (component.Container!.ContainedEntity is {} ent)
         {
             DebugTools.Assert(component.ClothingUid == ent, "Unexpected entity present inside of a toggleable clothing container.");
             return;
@@ -295,11 +298,8 @@ public sealed class ToggleableClothingSystem : EntitySystem
             component.Container.Insert(component.ClothingUid.Value, EntityManager, ownerTransform: xform);
         }
 
-        if (_actionsSystem.TryGetActionData(component.ActionEntity, out var action))
-        {
-            action.EntityIcon = component.ClothingUid;
-            _actionsSystem.Dirty(component.ActionEntity);
-        }
+        if (_actionContainer.EnsureAction(uid, ref component.ActionEntity, out var action, component.Action))
+            _actionsSystem.SetEntityIcon(component.ActionEntity.Value, component.ClothingUid, action);
     }
 }
 
index f90a5576c5f517d9eb5fa7aa3a65ea4342f3e49a..0d0d59f89f5fff656f78ab3a666842b2251232d7 100644 (file)
@@ -8,10 +8,10 @@ namespace Content.Shared.Clothing;
 [Access(typeof(SharedMagbootsSystem))]
 public sealed partial class MagbootsComponent : Component
 {
-    [DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>), required: true)]
-    public string? ToggleAction;
+    [DataField]
+    public EntProtoId ToggleAction = "ActionToggleMagboots";
 
-    [DataField("toggleActionEntity")]
+    [DataField, AutoNetworkedField]
     public EntityUid? ToggleActionEntity;
 
     [DataField("on"), AutoNetworkedField]
index 12d1cf264a1daf2b1a7d279a08c8d19ac4809795..ace8105b9996903886e586549dcb76ead38c55da 100644 (file)
@@ -35,7 +35,7 @@ namespace Content.Shared.CombatMode
         [DataField("combatToggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
         public string CombatToggleAction = "ActionCombatModeToggle";
 
-        [DataField("combatToggleActionEntity")]
+        [DataField, AutoNetworkedField]
         public EntityUid? CombatToggleActionEntity;
 
         [ViewVariables(VVAccess.ReadWrite), DataField("isInCombatMode"), AutoNetworkedField]
index cef8ad8de14ff5aa84ca7b4115ffe98f688109a9..192fd200789689ac43095e7c2e7fb8a2f96e7c6f 100644 (file)
@@ -21,11 +21,11 @@ public abstract class SharedDevourSystem : EntitySystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<DevourerComponent, ComponentStartup>(OnStartup);
+        SubscribeLocalEvent<DevourerComponent, MapInitEvent>(OnInit);
         SubscribeLocalEvent<DevourerComponent, DevourActionEvent>(OnDevourAction);
     }
 
-    protected void OnStartup(EntityUid uid, DevourerComponent component, ComponentStartup args)
+    protected void OnInit(EntityUid uid, DevourerComponent component, MapInitEvent args)
     {
         //Devourer doesn't actually chew, since he sends targets right into his stomach.
         //I did it mom, I added ERP content into upstream. Legally!
index 63fc64c6ce8e8727323d6e0563961b1d40b3f1ea..e58cb3a16f3fe55c4790bd3ffaa43d9042a41df2 100644 (file)
@@ -17,19 +17,19 @@ public sealed partial class GhostComponent : Component
     [DataField("toggleLightingAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
     public string ToggleLightingAction = "ActionToggleLighting";
 
-    [DataField("toggleLightingActionEntity")]
+    [DataField, AutoNetworkedField]
     public EntityUid? ToggleLightingActionEntity;
 
     [DataField("toggleFovAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
     public string ToggleFoVAction = "ActionToggleFov";
 
-    [DataField("toggleFovActionEntity")]
+    [DataField, AutoNetworkedField]
     public EntityUid? ToggleFoVActionEntity;
 
     [DataField("toggleGhostsAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
     public string ToggleGhostsAction = "ActionToggleGhosts";
 
-    [DataField("toggleGhostsActionEntity")]
+    [DataField, AutoNetworkedField]
     public EntityUid? ToggleGhostsActionEntity;
 
     [ViewVariables(VVAccess.ReadWrite), DataField("timeOfDeath", customTypeSerializer:typeof(TimeOffsetSerializer))]
@@ -41,10 +41,11 @@ public sealed partial class GhostComponent : Component
     [DataField("booMaxTargets")]
     public int BooMaxTargets = 3;
 
-    [DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string Action = "ActionGhostBoo";
+    [DataField]
+    public EntProtoId BooAction = "ActionGhostBoo";
 
-    [DataField("actionEntity")] public EntityUid? ActionEntity;
+    [DataField, AutoNetworkedField]
+    public EntityUid? BooActionEntity;
 
     // TODO: instead of this funny stuff just give it access and update in system dirtying when needed
     [ViewVariables(VVAccess.ReadWrite)]
index 7f7484f51cc6f1049193e1dd90e175b278beab17..b2fdb14e4c5176899e97697ee37ef91b6ce81d78 100644 (file)
@@ -1,7 +1,6 @@
 using Content.Shared.Actions;
-using Content.Shared.Radio;
 using Robust.Shared.GameStates;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Implants.Components;
 
@@ -10,7 +9,7 @@ namespace Content.Shared.Implants.Components;
 /// The actions can be activated via an action, a passive ability (ie tracking), or a reactive ability (ie on death) or some sort of combination
 /// They're added and removed with implanters
 /// </summary>
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class SubdermalImplantComponent : Component
 {
     /// <summary>
@@ -18,19 +17,22 @@ public sealed partial class SubdermalImplantComponent : Component
     /// </summary>
     [ViewVariables(VVAccess.ReadWrite)]
     [DataField("implantAction")]
-    public string? ImplantAction;
+    public EntProtoId? ImplantAction;
+
+    [DataField, AutoNetworkedField]
+    public EntityUid? Action;
 
     /// <summary>
     /// The entity this implant is inside
     /// </summary>
-    [ViewVariables]
+    [ViewVariables, AutoNetworkedField]
     public EntityUid? ImplantedEntity;
 
     /// <summary>
     /// Should this implant be removeable?
     /// </summary>
     [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("permanent")]
+    [DataField("permanent"), AutoNetworkedField]
     public bool Permanent = false;
 }
 
index 6d43c3dea110e9100a8638e7ffcbcc78e9401f18..55db027225619564b29ecfa52b110dcd8ca6632f 100644 (file)
@@ -37,8 +37,7 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
 
         if (!string.IsNullOrWhiteSpace(component.ImplantAction))
         {
-            var action = Spawn(component.ImplantAction);
-            _actionsSystem.AddAction(component.ImplantedEntity.Value, action, uid);
+            _actionsSystem.AddAction(component.ImplantedEntity.Value, ref component.Action, component.ImplantAction, uid);
         }
 
         //replace micro bomb with macro bomb
index e2146abe2f813ac9e05223f50eb299de51b04980..1b0701edd2c9214a36d12f9bc8517a0c15ec3829 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.Decals;
 using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 
@@ -9,24 +10,25 @@ namespace Content.Shared.Light.Components;
 /// This is simplified version of <see cref="HandheldLightComponent"/>.
 /// It doesn't consume any power and can be toggle only by verb.
 /// </summary>
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class UnpoweredFlashlightComponent : Component
 {
     [DataField("toggleFlashlightSound")]
     public SoundSpecifier ToggleSound = new SoundPathSpecifier("/Audio/Items/flashlight_pda.ogg");
 
-    [ViewVariables] public bool LightOn = false;
+    [DataField, AutoNetworkedField]
+    public bool LightOn = false;
 
-    [DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string? ToggleAction = "ActionToggleLight";
+    [DataField]
+    public EntProtoId ToggleAction = "ActionToggleLight";
 
-    [DataField("toggleActionEntity")] public EntityUid? ToggleActionEntity;
+    [DataField, AutoNetworkedField]
+    public EntityUid? ToggleActionEntity;
 
     /// <summary>
     ///  <see cref="ColorPalettePrototype"/> ID that determines the list
     /// of colors to select from when we get emagged
     /// </summary>
-    [DataField("emaggedColorsPrototype")]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public string EmaggedColorsPrototype = "Emagged";
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public ProtoId<ColorPalettePrototype> EmaggedColorsPrototype = "Emagged";
 }
index 2f889a8d49d22a5f99d665c7137c830fc5a97049..ec8fc62dfba3dbfd7054d74352fc70bef790d9c6 100644 (file)
@@ -157,6 +157,10 @@ public sealed partial class MechComponent : Component
     [DataField("brokenState")]
     public string? BrokenState;
     #endregion
+
+    [DataField] public EntityUid? MechCycleActionEntity;
+    [DataField] public EntityUid? MechUiActionEntity;
+    [DataField] public EntityUid? MechEjectActionEntity;
 }
 
 /// <summary>
index 026f73172964e4b444c6b91b7096e18bb0c4a1cb..7111c67779a34b36120e6e6b4332f41cfd33ce53 100644 (file)
@@ -179,10 +179,9 @@ public abstract class SharedMechSystem : EntitySystem
         if (_net.IsClient)
             return;
 
-        _actions.AddAction(pilot, Spawn(component.MechCycleAction), mech);
-        _actions.AddAction(pilot, Spawn(component.MechUiAction),
-            mech);
-        _actions.AddAction(pilot, Spawn(component.MechEjectAction), mech);
+        _actions.AddAction(pilot, ref component.MechCycleActionEntity, component.MechCycleAction, mech);
+        _actions.AddAction(pilot, ref component.MechUiActionEntity, component.MechUiAction, mech);
+        _actions.AddAction(pilot, ref component.MechEjectActionEntity, component.MechEjectAction, mech);
     }
 
     private void RemoveUser(EntityUid mech, EntityUid pilot)
index 8c72ee618d87118772b20d31a561f51c5a70d20b..2fae0b046a3fadd3c292fe1bf4ec67fc9d103acb 100644 (file)
@@ -24,4 +24,6 @@ public sealed partial class MobStateActionsComponent : Component
     /// </example>
     [DataField("actions")]
     public Dictionary<MobState, List<string>> Actions = new();
+
+    [DataField] public List<EntityUid> GrantedActions = new();
 }
index 1e5695229e58aeb5893fc6752c8357bb0b722800..735a3a6625660bd428956084021ee8b967fd4e9e 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Shared.Actions;
 using Content.Shared.Mobs.Components;
-using Robust.Shared.Network;
 
 namespace Content.Shared.Mobs.Systems;
 
@@ -9,7 +8,6 @@ namespace Content.Shared.Mobs.Systems;
 /// </summary>
 public sealed class MobStateActionsSystem : EntitySystem
 {
-    [Dependency] private readonly INetManager _net = default!;
     [Dependency] private readonly SharedActionsSystem _actions = default!;
 
     /// <inheritdoc/>
@@ -20,33 +18,25 @@ public sealed class MobStateActionsSystem : EntitySystem
 
     private void OnMobStateChanged(EntityUid uid, MobStateActionsComponent component, MobStateChangedEvent args)
     {
-        if (_net.IsClient)
+        if (!TryComp<ActionsComponent>(uid, out var action))
             return;
 
-        if (!TryComp<ActionsComponent>(uid, out var action))
+        foreach (var act in component.GrantedActions)
+        {
+            Del(act);
+        }
+        component.GrantedActions.Clear();
+
+        if (!component.Actions.TryGetValue(args.NewMobState, out var toGrant))
             return;
 
-        foreach (var (state, acts) in component.Actions)
+        foreach (var id in toGrant)
         {
-            if (state != args.NewMobState && state != args.OldMobState)
-                continue;
-
-            foreach (var item in acts)
-            {
-                if (state == args.OldMobState)
-                {
-                    // Don't remove actions that would be getting readded anyway
-                    if (component.Actions.TryGetValue(args.NewMobState, out var value)
-                        && value.Contains(item))
-                        continue;
-
-                    _actions.RemoveAction(uid, item, action);
-                }
-                else if (state == args.NewMobState)
-                {
-                    _actions.AddAction(uid, Spawn(item), null, action);
-                }
-            }
+            EntityUid? act = null;
+            if (_actions.AddAction(uid, ref act, id, uid, action))
+                component.GrantedActions.Add(act.Value);
         }
+
+        Dirty(uid, component);
     }
 }
index 73a8267500d9e5fd7435b2ad10810272493b46ef..0215e5a861f6a52430168b4257311e40a5c66821 100644 (file)
@@ -10,8 +10,7 @@ public sealed partial class JetpackComponent : Component
     [ViewVariables(VVAccess.ReadWrite), DataField("moleUsage")]
     public float MoleUsage = 0.012f;
 
-    [DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string? ToggleAction = "ActionToggleJetpack";
+    [DataField] public EntProtoId ToggleAction = "ActionToggleJetpack";
 
     [DataField("toggleActionEntity")] public EntityUid? ToggleActionEntity;
 
index 7b146e42367c14dd11b65c8d9f04a37c4590b9a1..9e92762b18297f1f18d464642cc168dfea75b23c 100644 (file)
@@ -194,4 +194,6 @@ public sealed partial class RevenantComponent : Component
     [DataField("harvestingState")]
     public string HarvestingState = "harvesting";
     #endregion
+
+    [DataField] public EntityUid? Action;
 }
index c457fec43f8cd928a0290459ad08564de62a22cf..e9bea22e2d23d8f4f9901d0b9795f52c89871609 100644 (file)
@@ -27,8 +27,7 @@ public sealed partial class MeleeSpeechComponent : Component
     [AutoNetworkedField]
     public int MaxBattlecryLength = 12;
 
-    [DataField("configureAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string? ConfigureAction = "ActionConfigureMeleeSpeech";
+    [DataField] public EntProtoId  ConfigureAction = "ActionConfigureMeleeSpeech";
 
     /// <summary>
     /// The action to open the battlecry UI
index f6ee5711e74dd5d6be94a7cadb91e9978ee1b2b4..2795d64b939a928b38067882a00d61969da738e3 100644 (file)
@@ -15,20 +15,18 @@ public abstract class SharedSpiderSystem : EntitySystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<SpiderComponent, ComponentStartup>(OnSpiderStartup);
+        SubscribeLocalEvent<SpiderComponent, MapInitEvent>(OnInit);
         SubscribeLocalEvent<SpiderWebObjectComponent, ComponentStartup>(OnWebStartup);
     }
 
-    private void OnSpiderStartup(EntityUid uid, SpiderComponent component, ComponentStartup args)
+    private void OnInit(EntityUid uid, SpiderComponent component, MapInitEvent args)
     {
-        if (_net.IsClient)
-            return;
-
-        _action.AddAction(uid, Spawn(component.WebAction), null);
+        _action.AddAction(uid, ref component.Action, component.WebAction, uid);
     }
 
     private void OnWebStartup(EntityUid uid, SpiderWebObjectComponent component, ComponentStartup args)
     {
+        // TODO dont use this. use some general random appearance system
         _appearance.SetData(uid, SpiderWebVisuals.Variant, _robustRandom.Next(1, 3));
     }
 }
index 8d1a4772e271e888fd78ce3de2721e7e94df9d94..42213adcb10b802bdddd60f1ddc93341b7c1fd5a 100644 (file)
@@ -16,6 +16,8 @@ public sealed partial class SpiderComponent : Component
     [ViewVariables(VVAccess.ReadWrite)]
     [DataField("webAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
     public string WebAction = "ActionSpiderWeb";
+
+    [DataField] public EntityUid? Action;
 }
 
 public sealed partial class SpiderWebActionEvent : InstantActionEvent { }
index ec02081c4e7a7461ece5c09745497e8f05bfd0dc..295a8d8e0243623cb452a4864480f922317fa8e4 100644 (file)
   noSpawn: true
   components:
   - type: InstantAction
+    clientExclusive: true
+    checkCanInteract: false
+    temporary: true
     icon: { sprite: Objects/Tools/multitool.rsi, state: icon }
     event: !type:ClearAllOverlaysEvent
 
index 4d647843c41a2cccad0b2f99eb9a4b8e49e40e5d..2b8f4202731608efbbd8c85bce1e318b0a177c31 100644 (file)
@@ -12,7 +12,6 @@
     - type: Clothing
       sprite: Clothing/Shoes/Boots/magboots.rsi
     - type: Magboots
-      toggleAction: ActionToggleMagboots
     - type: ClothingSpeedModifier
       walkModifier: 0.85
       sprintModifier: 0.8