From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sat, 23 Sep 2023 08:49:39 +0000 (-0400) Subject: Action container rejig (#20260) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=684b334806739fc613752d79ed52954164851072;p=space-station-14.git Action container rejig (#20260) Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> --- diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 07b0e6331d..41db0ad7ce 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -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? ActionAdded; - public event Action? ActionRemoved; + public event Action? OnActionAdded; + public event Action? OnActionRemoved; public event OnActionReplaced? ActionReplaced; public event Action? ActionsUpdated; public event Action? LinkActions; @@ -37,11 +36,8 @@ namespace Content.Client.Actions public event Action? ClearAssignments; public event Action>? AssignSlot; - /// - /// Queue of entities with that needs to be updated after - /// handling a state. - /// - private readonly Queue _actionHoldersQueue = new(); + private readonly List _removed = new(); + private readonly List<(EntityUid, BaseActionComponent?)> _added = new(); public override void Initialize() { @@ -49,87 +45,148 @@ namespace Content.Client.Actions SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(HandleComponentState); + + SubscribeLocalEvent(OnInstantHandleState); + SubscribeLocalEvent(OnEntityTargetHandleState); + SubscribeLocalEvent(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(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(state.Actions, uid)); + component.Whitelist = state.Whitelist; + component.CanTargetSelf = state.CanTargetSelf; + BaseHandleState(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(uid, component, state); } - protected override void AddActionInternal(EntityUid holderId, EntityUid actionId, BaseContainer container, ActionsComponent holder) + private void BaseHandleState(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(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(state.Container, uid); + component.EntityIcon = EnsureEntity(state.EntityIcon, uid); + component.CheckCanInteract = state.CheckCanInteract; + component.ClientExclusive = state.ClientExclusive; + component.Priority = state.Priority; + component.AttachedEntity = EnsureEntity(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(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(actionNode, notNullableOverride: true); var actionId = Spawn(null); - AddComp(actionId, action); - AddAction(user, actionId, null); + AddComp(actionId, action); + AddActionDirect(user, actionId); if (map.TryGet("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(); - var added = new List<(EntityUid Id, BaseActionComponent Comp)>(); - var query = GetEntityQuery(); - var queue = new Queue(_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); } } diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs index 944d4e0de0..a5353921fa 100644 --- a/Content.Client/Ghost/GhostSystem.cs +++ b/Content.Client/Ghost/GhostSystem.cs @@ -55,7 +55,7 @@ namespace Content.Client.Ghost { base.Initialize(); - SubscribeLocalEvent(OnGhostInit); + SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnGhostRemove); SubscribeLocalEvent(OnGhostState); @@ -72,16 +72,10 @@ namespace Content.Client.Ghost SubscribeLocalEvent(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) diff --git a/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkOverlay.cs b/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkOverlay.cs index d61bb469b3..ca135f6ca2 100644 --- a/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkOverlay.cs +++ b/Content.Client/NetworkConfigurator/NetworkConfiguratorLinkOverlay.cs @@ -14,7 +14,8 @@ public sealed class NetworkConfiguratorLinkOverlay : Overlay [Dependency] private readonly IRobustRandom _random = default!; private readonly DeviceListSystem _deviceListSystem; - private Dictionary _colors = new(); + public Dictionary Colors = new(); + public EntityUid? Action; public override OverlaySpace Space => OverlaySpace.WorldSpace; @@ -25,11 +26,6 @@ public sealed class NetworkConfiguratorLinkOverlay : Overlay _deviceListSystem = _entityManager.System(); } - public void ClearEntity(EntityUid uid) - { - _colors.Remove(uid); - } - protected override void Draw(in OverlayDrawArgs args) { foreach (var tracker in _entityManager.EntityQuery()) @@ -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(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]); } } } diff --git a/Content.Client/NetworkConfigurator/Systems/NetworkConfiguratorSystem.cs b/Content.Client/NetworkConfigurator/Systems/NetworkConfiguratorSystem.cs index 48b378af04..af1861dc47 100644 --- a/Content.Client/NetworkConfigurator/Systems/NetworkConfiguratorSystem.cs +++ b/Content.Client/NetworkConfigurator/Systems/NetworkConfiguratorSystem.cs @@ -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()) - { - _overlay.GetOverlay().ClearEntity(component.ActiveDeviceList.Value); - } - RemComp(component.ActiveDeviceList.Value); - if (!EntityQuery().Any()) - { - _overlay.RemoveOverlay(); - _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(); return; } if (!_overlay.HasOverlay()) { - _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(component.ActiveDeviceList.Value); @@ -88,7 +89,7 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem public void ClearAllOverlays() { - if (!_overlay.HasOverlay()) + if (!_overlay.TryGetOverlay(out NetworkConfiguratorLinkOverlay? overlay)) { return; } @@ -98,12 +99,8 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem RemCompDeferred(tracker.Owner); } - _overlay.RemoveOverlay(); - - if (_playerManager.LocalPlayer?.ControlledEntity != null) - { - _actions.RemoveAction(_playerManager.LocalPlayer.ControlledEntity.Value, Action); - } + _actions.RemoveAction(overlay.Action); + _overlay.RemoveOverlay(overlay); } // hacky solution related to mapping diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs index 65817aaf4a..fcacc1b052 100644 --- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs +++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs @@ -111,8 +111,8 @@ public sealed class ActionUIController : UIController, IOnStateChanged 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(action.Comp.Provider.Value).EntityName; + var providerName = EntityManager.GetComponent(action.Comp.Container.Value).EntityName; return providerName.Contains(search, StringComparison.OrdinalIgnoreCase); }); @@ -744,11 +747,9 @@ public sealed class ActionUIController : UIController, IOnStateChanged(entIcon.Value).Icon? + _dragShadow.Texture = EntityManager.GetComponent(entIcon).Icon? .GetFrame(RsiDirection.South, 0); } else if (action.Icon != null) @@ -902,6 +903,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged action.Comp.AutoPopulate).ToList(); + actions.Sort(ActionComparer); var offset = 0; var totalPages = _pages.Count; @@ -965,11 +967,11 @@ public sealed class ActionUIController : UIController, IOnStateChanged(out var handOverlay)) { - if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Provider != null) + if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Container != null) { handOverlay.EntityOverride = provider; } diff --git a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs index bbda27f0a0..5d6ad5c8de 100644 --- a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs +++ b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs @@ -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) diff --git a/Content.Server/Actions/ActionOnInteractComponent.cs b/Content.Server/Actions/ActionOnInteractComponent.cs index 9efe3d6ba0..3a7e15649b 100644 --- a/Content.Server/Actions/ActionOnInteractComponent.cs +++ b/Content.Server/Actions/ActionOnInteractComponent.cs @@ -20,8 +20,8 @@ namespace Content.Server.Actions; [RegisterComponent] public sealed partial class ActionOnInteractComponent : Component { - [DataField("actions", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List? Actions; + [DataField(required:true)] + public List? Actions; - [DataField("actionEntities")] public List? ActionEntities; + [DataField] public List? ActionEntities; } diff --git a/Content.Server/Actions/ActionOnInteractSystem.cs b/Content.Server/Actions/ActionOnInteractSystem.cs index 31f579f7ec..c9a5f4b5d0 100644 --- a/Content.Server/Actions/ActionOnInteractSystem.cs +++ b/Content.Server/Actions/ActionOnInteractSystem.cs @@ -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(OnActivate); SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(EntityUid uid, ActionOnInteractComponent component, MapInitEvent args) + { + if (component.Actions == null) + return; + + var comp = EnsureComp(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; } diff --git a/Content.Server/Animals/Components/EggLayerComponent.cs b/Content.Server/Animals/Components/EggLayerComponent.cs index 551e665f72..33d14c8e36 100644 --- a/Content.Server/Animals/Components/EggLayerComponent.cs +++ b/Content.Server/Animals/Components/EggLayerComponent.cs @@ -48,4 +48,6 @@ public sealed partial class EggLayerComponent : Component [DataField("accumulatedFrametime")] public float AccumulatedFrametime; + + [DataField] public EntityUid? Action; } diff --git a/Content.Server/Animals/Systems/EggLayerSystem.cs b/Content.Server/Animals/Systems/EggLayerSystem.cs index abfd74ad01..5189adb031 100644 --- a/Content.Server/Animals/Systems/EggLayerSystem.cs +++ b/Content.Server/Animals/Systems/EggLayerSystem.cs @@ -23,7 +23,7 @@ public sealed class EggLayerSystem : EntitySystem { base.Initialize(); - SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(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); } diff --git a/Content.Server/Bed/BedSystem.cs b/Content.Server/Bed/BedSystem.cs index c9795928e5..915bf7de29 100644 --- a/Content.Server/Bed/BedSystem.cs +++ b/Content.Server/Bed/BedSystem.cs @@ -43,14 +43,11 @@ namespace Content.Server.Bed { AddComp(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(uid); } diff --git a/Content.Server/Bed/Sleep/SleepingSystem.cs b/Content.Server/Bed/Sleep/SleepingSystem.cs index 4f7b66d9e5..30e3a99eea 100644 --- a/Content.Server/Bed/Sleep/SleepingSystem.cs +++ b/Content.Server/Bed/Sleep/SleepingSystem.cs @@ -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(uid); return true; diff --git a/Content.Server/Dragon/DragonSystem.cs b/Content.Server/Dragon/DragonSystem.cs index f132e0041e..40039be50e 100644 --- a/Content.Server/Dragon/DragonSystem.cs +++ b/Content.Server/Dragon/DragonSystem.cs @@ -47,7 +47,7 @@ public sealed partial class DragonSystem : EntitySystem { base.Initialize(); - SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnDragonRift); SubscribeLocalEvent(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); diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs index bdc40f102e..82ae4b8fa6 100644 --- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs @@ -31,7 +31,6 @@ namespace Content.Server.GameTicking.Rules; public sealed class ZombieRuleSystem : GameRuleSystem { - [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 SubscribeLocalEvent(OnStartAttempt); SubscribeLocalEvent(OnRoundEndText); - SubscribeLocalEvent(OnZombifySelf); + SubscribeLocalEvent(OnZombifySelf); } private void OnRoundEndText(RoundEndTextAppendEvent ev) @@ -191,10 +190,11 @@ public sealed class ZombieRuleSystem : GameRuleSystem 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 EnsureComp(ownedEntity); EnsureComp(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)); diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 83c8cca045..c8a410b91f 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -48,6 +48,7 @@ namespace Content.Server.Ghost base.Initialize(); SubscribeLocalEvent(OnGhostStartup); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnGhostShutdown); SubscribeLocalEvent(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); diff --git a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs index 3ff115d14e..ab3a64a5f0 100644 --- a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs +++ b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs @@ -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) diff --git a/Content.Server/Magic/MagicSystem.cs b/Content.Server/Magic/MagicSystem.cs index c4ed74604d..8ee034fd32 100644 --- a/Content.Server/Magic/MagicSystem.cs +++ b/Content.Server/Magic/MagicSystem.cs @@ -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(OnInit); + SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnUse); SubscribeLocalEvent(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); } } diff --git a/Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs b/Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs index d295031a5b..c155c538d1 100644 --- a/Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs +++ b/Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs @@ -25,5 +25,7 @@ namespace Content.Server.Polymorph.Components /// [DataField("time")] public float Time; + + [DataField] public EntityUid? Action; } } diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs index d630cbcad7..6a90928a44 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs @@ -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(OnStartup); SubscribeLocalEvent(OnPolymorphActionEvent); - SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnBeforeFullyEaten); SubscribeLocalEvent(OnBeforeFullySliced); SubscribeLocalEvent(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(target, out var polycomp)) return; + polycomp.PolymorphActions ??= new Dictionary(); + if (polycomp.PolymorphActions.ContainsKey(id)) + return; + var entproto = _proto.Index(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(); - 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] diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs index 3ff247d6f8..0026533c4a 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs @@ -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) diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs index b0b1437dbe..31d826087d 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs @@ -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)) diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index d2fccd1b9c..9600ea6c8f 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -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 diff --git a/Content.Server/UserInterface/IntrinsicUISystem.cs b/Content.Server/UserInterface/IntrinsicUISystem.cs index ce89974f63..bd449df5f5 100644 --- a/Content.Server/UserInterface/IntrinsicUISystem.cs +++ b/Content.Server/UserInterface/IntrinsicUISystem.cs @@ -12,7 +12,7 @@ public sealed class IntrinsicUISystem : EntitySystem public override void Initialize() { - SubscribeLocalEvent(OnGetActions); + SubscribeLocalEvent(InitActions); SubscribeLocalEvent(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(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); } } diff --git a/Content.Server/Zombies/PendingZombieComponent.cs b/Content.Server/Zombies/PendingZombieComponent.cs index fb3d6debcd..e112198711 100644 --- a/Content.Server/Zombies/PendingZombieComponent.cs +++ b/Content.Server/Zombies/PendingZombieComponent.cs @@ -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 index 0000000000..c18d1ead62 --- /dev/null +++ b/Content.Shared/Actions/ActionContainerComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.Containers; +using Robust.Shared.GameStates; + +namespace Content.Shared.Actions; + +/// +/// This component indicates that this entity contains actions inside of some container. +/// +[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 index 0000000000..f7446ae1e6 --- /dev/null +++ b/Content.Shared/Actions/ActionContainerSystem.cs @@ -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; + +/// +/// Handles storing & spawning action entities in a container. +/// +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(OnInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnEntityRemoved); + SubscribeLocalEvent(OnEntityInserted); + } + + /// + /// Spawns a new action entity and adds it to the given container. + /// + public EntityUid? AddAction(EntityUid uid, string actionPrototypeId, ActionsContainerComponent? comp = null) + { + EntityUid? result = default; + EnsureAction(uid, ref result, actionPrototypeId, comp); + return result; + } + + /// + /// 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. + /// + public bool EnsureAction(EntityUid uid, + [NotNullWhen(true)] ref EntityUid? actionId, + string actionPrototypeId, + ActionsContainerComponent? comp = null) + { + return EnsureAction(uid, ref actionId, out _, actionPrototypeId, comp); + } + + /// + 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(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; + } + + /// + /// Adds a pre-existing action to an action container. + /// + 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(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(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); + } + } +} + +/// +/// Raised directed at an action container when a new action entity gets inserted. +/// +[ByRefEvent] +public readonly struct ActionAddedEvent +{ + public readonly EntityUid Action; + public readonly BaseActionComponent Component; + + public ActionAddedEvent(EntityUid action, BaseActionComponent component) + { + Action = action; + Component = component; + } +} + +/// +/// Raised directed at an action container when an action entity gets removed. +/// +[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 diff --git a/Content.Shared/Actions/ActionEvents.cs b/Content.Shared/Actions/ActionEvents.cs index c6f873c78a..72a566b8c8 100644 --- a/Content.Shared/Actions/ActionEvents.cs +++ b/Content.Shared/Actions/ActionEvents.cs @@ -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; /// public sealed class GetItemActionsEvent : EntityEventArgs { - private readonly IEntityManager _entities; - private readonly INetManager _net; + private readonly ActionContainerSystem _system; public readonly SortedSet Actions = new(); /// @@ -27,6 +25,12 @@ public sealed class GetItemActionsEvent : EntityEventArgs /// public EntityUid User; + /// + /// The entity that is being asked to provide the actions. This is used as a default argument to . + /// I.e., if a new action needs to be spawned, then it will be inserted into this entity unless otherwise specified. + /// + public EntityUid Provider; + /// /// Slot flags for the inventory slot that this item got equipped to. Null if not in a slot (i.e., if equipped to hands). /// @@ -37,25 +41,36 @@ public sealed class GetItemActionsEvent : EntityEventArgs /// 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) + /// + /// 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 . + /// + 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); - } + /// + /// 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 . + /// + 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); } } diff --git a/Content.Shared/Actions/ActionsComponent.cs b/Content.Shared/Actions/ActionsComponent.cs index f7db07a85a..b810e98d4d 100644 --- a/Content.Shared/Actions/ActionsComponent.cs +++ b/Content.Shared/Actions/ActionsComponent.cs @@ -9,13 +9,10 @@ namespace Content.Shared.Actions; public sealed partial class ActionsComponent : Component { /// - /// 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. /// - [ViewVariables] public readonly Dictionary OldClientActions = new(); - - [ViewVariables] public readonly HashSet Actions = new(); - - public override bool SendOnlyToOwner => true; + [DataField] public HashSet 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); /// /// Determines how the action icon appears in the hotbar for item actions. diff --git a/Content.Shared/Actions/BaseActionComponent.cs b/Content.Shared/Actions/BaseActionComponent.cs index 5580c19e19..a21b801d3c 100644 --- a/Content.Shared/Actions/BaseActionComponent.cs +++ b/Content.Shared/Actions/BaseActionComponent.cs @@ -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 , but it will also be /// automatically toggled for targeted-actions while selecting a target. /// + [DataField] public bool Toggled; /// /// The current cooldown on the action. /// + // TODO serialization public (TimeSpan Start, TimeSpan End)? Cooldown; /// @@ -65,21 +70,34 @@ public abstract partial class BaseActionComponent : Component [DataField("charges")] public int? Charges; /// - /// The entity that enables / provides this action. If the action is innate, this may be the user themselves. If - /// this action has no provider (e.g., mapping tools), the this will result in broadcast events. + /// The entity that contains this action. If the action is innate, this may be the user themselves. + /// This should almost always be non-null. /// - public EntityUid? Provider; + [Access(typeof(ActionContainerSystem), typeof(SharedActionsSystem))] + [DataField] + public EntityUid? Container; /// - /// Entity to use for the action icon. Defaults to using . + /// Entity to use for the action icon. If no entity is provided and the differs from + /// , then it will default to using /// 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; /// /// 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; /// - /// 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. /// [DataField("clientExclusive")] public bool ClientExclusive = false; @@ -107,24 +125,10 @@ public abstract partial class BaseActionComponent : Component /// [DataField("autoPopulate")] public bool AutoPopulate = true; - /// - /// 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 . /// - [DataField("autoRemove")] public bool AutoRemove = true; - - /// - /// Temporary actions are removed from the action component when removed from the action-bar/GUI. Currently, - /// should only be used for client-exclusive actions (server is not notified). - /// - /// - /// Currently there is no way for a player to just voluntarily remove actions. They can hide them from the - /// toolbar, but not actually remove them. This is undesirable for things like dynamically added mapping - /// entity-selection actions, as the # of actions would just keep increasing. - /// [DataField("temporary")] public bool Temporary; - // TODO re-add support for this - // UI refactor seems to have just broken it. /// /// 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; diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index dc0afc8af7..f32e0dd29c 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -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(OnDidUnequip); SubscribeLocalEvent(OnHandUnequipped); - SubscribeLocalEvent(OnActionsMapInit); SubscribeLocalEvent(OnActionsGetState); - SubscribeLocalEvent(OnActionsShutdown); SubscribeLocalEvent(OnInstantGetState); SubscribeLocalEvent(OnEntityTargetGetState); SubscribeLocalEvent(OnWorldTargetGetState); - SubscribeLocalEvent(OnInstantHandleState); - SubscribeLocalEvent(OnEntityTargetHandleState); - SubscribeLocalEvent(OnWorldTargetHandleState); - SubscribeLocalEvent(OnGetActionData); SubscribeLocalEvent(OnGetActionData); SubscribeLocalEvent(OnGetActionData); - SubscribeLocalEvent(OnEntGotRemovedFromContainer); - SubscribeLocalEvent(OnEntGotRemovedFromContainer); - SubscribeLocalEvent(OnEntGotRemovedFromContainer); - SubscribeAllEvent(OnActionRequest); } @@ -78,117 +64,43 @@ public abstract class SharedActionsSystem : EntitySystem args.State = new WorldTargetActionComponentState(component, EntityManager); } - private void BaseHandleState(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(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(state.Provider, uid); - component.EntityIcon = EnsureEntity(state.EntityIcon, uid); - component.CheckCanInteract = state.CheckCanInteract; - component.ClientExclusive = state.ClientExclusive; - component.Priority = state.Priority; - component.AttachedEntity = EnsureEntity(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(uid, component, state); - } - - private void OnEntityTargetHandleState(EntityUid uid, EntityTargetActionComponent component, ref ComponentHandleState args) - { - if (args.Current is not EntityTargetActionComponentState state) - return; - - BaseHandleState(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(uid, component, state); - } - private void OnGetActionData(EntityUid uid, T component, ref GetActionDataEvent args) where T : BaseActionComponent { args.Action = component; } - private void OnEntGotRemovedFromContainer(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(holderId, ActionContainerId) - : _containerSystem.EnsureContainer(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; + } + /// - /// 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. /// - 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); - } + /// Entity to receive the actions + /// Action entity to add + /// The 's action component of + /// The action entity prototype id to use if is invalid. + /// The entity that contains/enables this action (e.g., flashlight).. + 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); + } + + /// + 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); } /// - /// Add an action to an action holder. - /// If the holder has no actions component, this will give them one. + /// Adds a pre-existing action. /// - /// Entity to receive the actions - /// Action entity to add - /// The entity that enables these actions (e.g., flashlight). May be null (innate actions). - /// Component of - /// Component of - /// Action container of - 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(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); + /// + /// Adds a pre-existing action. This also bypasses the requirement that the given action must be stored in a + /// valid action container. + /// + 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(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) + /// + /// This method gets called after a new action got added. + /// + 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. } /// - /// 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. /// - /// Entity to receive the actions + /// Entity to receive the actions /// The actions to add - /// The entity that enables these actions (e.g., flashlight). May be null (innate actions). - public void AddActions(EntityUid holderId, IEnumerable actions, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true) + /// The entity that enables these actions (e.g., flashlight). May be null (innate actions). + public void GrantActions(EntityUid performer, IEnumerable actions, EntityUid container, ActionsComponent? comp = null, ActionsContainerComponent? containerComp = null) { - comp ??= EnsureComp(holderId); + if (!Resolve(container, ref containerComp)) + return; - var allClientExclusive = true; - var container = EnsureContainer(holderId, provider); + DebugTools.Assert(comp == null || comp.Owner == performer); + comp ??= EnsureComp(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 /// /// Remove any actions that were enabled by some other entity. Useful when unequiping items that grant actions. /// - 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); } - /// - /// Removes all actions with the given prototype id. - /// - 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); + } + + /// + /// This method gets called after an action got removed. + /// + 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); + } } diff --git a/Content.Shared/Bed/Sleep/SharedSleepingSystem.cs b/Content.Shared/Bed/Sleep/SharedSleepingSystem.cs index bb9c8dcf43..ce6ae4795c 100644 --- a/Content.Shared/Bed/Sleep/SharedSleepingSystem.cs +++ b/Content.Shared/Bed/Sleep/SharedSleepingSystem.cs @@ -20,7 +20,7 @@ namespace Content.Server.Bed.Sleep public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnSpeakAttempt); SubscribeLocalEvent(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); diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs index 53f7284bbe..ba006abfda 100644 --- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -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 /// 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); } } diff --git a/Content.Shared/Clothing/MagbootsComponent.cs b/Content.Shared/Clothing/MagbootsComponent.cs index f90a5576c5..0d0d59f89f 100644 --- a/Content.Shared/Clothing/MagbootsComponent.cs +++ b/Content.Shared/Clothing/MagbootsComponent.cs @@ -8,10 +8,10 @@ namespace Content.Shared.Clothing; [Access(typeof(SharedMagbootsSystem))] public sealed partial class MagbootsComponent : Component { - [DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer), required: true)] - public string? ToggleAction; + [DataField] + public EntProtoId ToggleAction = "ActionToggleMagboots"; - [DataField("toggleActionEntity")] + [DataField, AutoNetworkedField] public EntityUid? ToggleActionEntity; [DataField("on"), AutoNetworkedField] diff --git a/Content.Shared/CombatMode/CombatModeComponent.cs b/Content.Shared/CombatMode/CombatModeComponent.cs index 12d1cf264a..ace8105b99 100644 --- a/Content.Shared/CombatMode/CombatModeComponent.cs +++ b/Content.Shared/CombatMode/CombatModeComponent.cs @@ -35,7 +35,7 @@ namespace Content.Shared.CombatMode [DataField("combatToggleAction", customTypeSerializer: typeof(PrototypeIdSerializer))] public string CombatToggleAction = "ActionCombatModeToggle"; - [DataField("combatToggleActionEntity")] + [DataField, AutoNetworkedField] public EntityUid? CombatToggleActionEntity; [ViewVariables(VVAccess.ReadWrite), DataField("isInCombatMode"), AutoNetworkedField] diff --git a/Content.Shared/Devour/SharedDevourSystem.cs b/Content.Shared/Devour/SharedDevourSystem.cs index cef8ad8de1..192fd20078 100644 --- a/Content.Shared/Devour/SharedDevourSystem.cs +++ b/Content.Shared/Devour/SharedDevourSystem.cs @@ -21,11 +21,11 @@ public abstract class SharedDevourSystem : EntitySystem { base.Initialize(); - SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnInit); SubscribeLocalEvent(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! diff --git a/Content.Shared/Ghost/GhostComponent.cs b/Content.Shared/Ghost/GhostComponent.cs index 63fc64c6ce..e58cb3a16f 100644 --- a/Content.Shared/Ghost/GhostComponent.cs +++ b/Content.Shared/Ghost/GhostComponent.cs @@ -17,19 +17,19 @@ public sealed partial class GhostComponent : Component [DataField("toggleLightingAction", customTypeSerializer: typeof(PrototypeIdSerializer))] public string ToggleLightingAction = "ActionToggleLighting"; - [DataField("toggleLightingActionEntity")] + [DataField, AutoNetworkedField] public EntityUid? ToggleLightingActionEntity; [DataField("toggleFovAction", customTypeSerializer: typeof(PrototypeIdSerializer))] public string ToggleFoVAction = "ActionToggleFov"; - [DataField("toggleFovActionEntity")] + [DataField, AutoNetworkedField] public EntityUid? ToggleFoVActionEntity; [DataField("toggleGhostsAction", customTypeSerializer: typeof(PrototypeIdSerializer))] 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))] - 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)] diff --git a/Content.Shared/Implants/Components/SubdermalImplantComponent.cs b/Content.Shared/Implants/Components/SubdermalImplantComponent.cs index 7f7484f51c..b2fdb14e4c 100644 --- a/Content.Shared/Implants/Components/SubdermalImplantComponent.cs +++ b/Content.Shared/Implants/Components/SubdermalImplantComponent.cs @@ -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 /// -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class SubdermalImplantComponent : Component { /// @@ -18,19 +17,22 @@ public sealed partial class SubdermalImplantComponent : Component /// [ViewVariables(VVAccess.ReadWrite)] [DataField("implantAction")] - public string? ImplantAction; + public EntProtoId? ImplantAction; + + [DataField, AutoNetworkedField] + public EntityUid? Action; /// /// The entity this implant is inside /// - [ViewVariables] + [ViewVariables, AutoNetworkedField] public EntityUid? ImplantedEntity; /// /// Should this implant be removeable? /// [ViewVariables(VVAccess.ReadWrite)] - [DataField("permanent")] + [DataField("permanent"), AutoNetworkedField] public bool Permanent = false; } diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs index 6d43c3dea1..55db027225 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs @@ -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 diff --git a/Content.Shared/Light/Components/UnpoweredFlashlightComponent.cs b/Content.Shared/Light/Components/UnpoweredFlashlightComponent.cs index e2146abe2f..1b0701edd2 100644 --- a/Content.Shared/Light/Components/UnpoweredFlashlightComponent.cs +++ b/Content.Shared/Light/Components/UnpoweredFlashlightComponent.cs @@ -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 . /// It doesn't consume any power and can be toggle only by verb. /// -[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))] - public string? ToggleAction = "ActionToggleLight"; + [DataField] + public EntProtoId ToggleAction = "ActionToggleLight"; - [DataField("toggleActionEntity")] public EntityUid? ToggleActionEntity; + [DataField, AutoNetworkedField] + public EntityUid? ToggleActionEntity; /// /// ID that determines the list /// of colors to select from when we get emagged /// - [DataField("emaggedColorsPrototype")] - [ViewVariables(VVAccess.ReadWrite)] - public string EmaggedColorsPrototype = "Emagged"; + [DataField, ViewVariables(VVAccess.ReadWrite)] + public ProtoId EmaggedColorsPrototype = "Emagged"; } diff --git a/Content.Shared/Mech/Components/MechComponent.cs b/Content.Shared/Mech/Components/MechComponent.cs index 2f889a8d49..ec8fc62dfb 100644 --- a/Content.Shared/Mech/Components/MechComponent.cs +++ b/Content.Shared/Mech/Components/MechComponent.cs @@ -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; } /// diff --git a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs b/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs index 026f731729..7111c67779 100644 --- a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs +++ b/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs @@ -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) diff --git a/Content.Shared/Mobs/Components/MobStateActionsComponent.cs b/Content.Shared/Mobs/Components/MobStateActionsComponent.cs index 8c72ee618d..2fae0b046a 100644 --- a/Content.Shared/Mobs/Components/MobStateActionsComponent.cs +++ b/Content.Shared/Mobs/Components/MobStateActionsComponent.cs @@ -24,4 +24,6 @@ public sealed partial class MobStateActionsComponent : Component /// [DataField("actions")] public Dictionary> Actions = new(); + + [DataField] public List GrantedActions = new(); } diff --git a/Content.Shared/Mobs/Systems/MobStateActionsSystem.cs b/Content.Shared/Mobs/Systems/MobStateActionsSystem.cs index 1e5695229e..735a3a6625 100644 --- a/Content.Shared/Mobs/Systems/MobStateActionsSystem.cs +++ b/Content.Shared/Mobs/Systems/MobStateActionsSystem.cs @@ -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; /// public sealed class MobStateActionsSystem : EntitySystem { - [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly SharedActionsSystem _actions = default!; /// @@ -20,33 +18,25 @@ public sealed class MobStateActionsSystem : EntitySystem private void OnMobStateChanged(EntityUid uid, MobStateActionsComponent component, MobStateChangedEvent args) { - if (_net.IsClient) + if (!TryComp(uid, out var action)) return; - if (!TryComp(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); } } diff --git a/Content.Shared/Movement/Components/JetpackComponent.cs b/Content.Shared/Movement/Components/JetpackComponent.cs index 73a8267500..0215e5a861 100644 --- a/Content.Shared/Movement/Components/JetpackComponent.cs +++ b/Content.Shared/Movement/Components/JetpackComponent.cs @@ -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))] - public string? ToggleAction = "ActionToggleJetpack"; + [DataField] public EntProtoId ToggleAction = "ActionToggleJetpack"; [DataField("toggleActionEntity")] public EntityUid? ToggleActionEntity; diff --git a/Content.Shared/Revenant/Components/RevenantComponent.cs b/Content.Shared/Revenant/Components/RevenantComponent.cs index 7b146e4236..9e92762b18 100644 --- a/Content.Shared/Revenant/Components/RevenantComponent.cs +++ b/Content.Shared/Revenant/Components/RevenantComponent.cs @@ -194,4 +194,6 @@ public sealed partial class RevenantComponent : Component [DataField("harvestingState")] public string HarvestingState = "harvesting"; #endregion + + [DataField] public EntityUid? Action; } diff --git a/Content.Shared/Speech/Components/MeleeSpeechComponent.cs b/Content.Shared/Speech/Components/MeleeSpeechComponent.cs index c457fec43f..e9bea22e2d 100644 --- a/Content.Shared/Speech/Components/MeleeSpeechComponent.cs +++ b/Content.Shared/Speech/Components/MeleeSpeechComponent.cs @@ -27,8 +27,7 @@ public sealed partial class MeleeSpeechComponent : Component [AutoNetworkedField] public int MaxBattlecryLength = 12; - [DataField("configureAction", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? ConfigureAction = "ActionConfigureMeleeSpeech"; + [DataField] public EntProtoId ConfigureAction = "ActionConfigureMeleeSpeech"; /// /// The action to open the battlecry UI diff --git a/Content.Shared/Spider/SharedSpiderSystem.cs b/Content.Shared/Spider/SharedSpiderSystem.cs index f6ee5711e7..2795d64b93 100644 --- a/Content.Shared/Spider/SharedSpiderSystem.cs +++ b/Content.Shared/Spider/SharedSpiderSystem.cs @@ -15,20 +15,18 @@ public abstract class SharedSpiderSystem : EntitySystem { base.Initialize(); - SubscribeLocalEvent(OnSpiderStartup); + SubscribeLocalEvent(OnInit); SubscribeLocalEvent(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)); } } diff --git a/Content.Shared/Spider/SpiderComponent.cs b/Content.Shared/Spider/SpiderComponent.cs index 8d1a4772e2..42213adcb1 100644 --- a/Content.Shared/Spider/SpiderComponent.cs +++ b/Content.Shared/Spider/SpiderComponent.cs @@ -16,6 +16,8 @@ public sealed partial class SpiderComponent : Component [ViewVariables(VVAccess.ReadWrite)] [DataField("webAction", customTypeSerializer: typeof(PrototypeIdSerializer))] public string WebAction = "ActionSpiderWeb"; + + [DataField] public EntityUid? Action; } public sealed partial class SpiderWebActionEvent : InstantActionEvent { } diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index ec02081c4e..295a8d8e02 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -209,6 +209,9 @@ noSpawn: true components: - type: InstantAction + clientExclusive: true + checkCanInteract: false + temporary: true icon: { sprite: Objects/Tools/multitool.rsi, state: icon } event: !type:ClearAllOverlaysEvent diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml index 4d647843c4..2b8f420273 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml @@ -12,7 +12,6 @@ - type: Clothing sprite: Clothing/Shoes/Boots/magboots.rsi - type: Magboots - toggleAction: ActionToggleMagboots - type: ClothingSpeedModifier walkModifier: 0.85 sprintModifier: 0.8