Refactor actions to be entities with components (#19900)

This commit is contained in:
DrSmugleaf
2023-09-08 18:16:05 -07:00
committed by GitHub
parent e18f731b91
commit c71f97e3a2
210 changed files with 10693 additions and 11714 deletions

View File

@@ -1,11 +1,9 @@
using Content.Shared.Actions.ActionTypes;
namespace Content.Client.Actions;
/// <summary>
/// This event is raised when a user clicks on an empty action slot. Enables other systems to fill this slow.
/// This event is raised when a user clicks on an empty action slot. Enables other systems to fill this slot.
/// </summary>
public sealed class FillActionSlotEvent : EntityEventArgs
{
public ActionType? Action;
public EntityUid? Action;
}

View File

@@ -1,10 +1,10 @@
using System.IO;
using System.Linq;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
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;
@@ -12,6 +12,7 @@ using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
@@ -20,14 +21,15 @@ namespace Content.Client.Actions
[UsedImplicitly]
public sealed class ActionsSystem : SharedActionsSystem
{
public delegate void OnActionReplaced(ActionType existing, ActionType action);
public delegate void OnActionReplaced(EntityUid actionId);
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
public event Action<ActionType>? ActionAdded;
public event Action<ActionType>? ActionRemoved;
public event Action<EntityUid>? ActionAdded;
public event Action<EntityUid>? ActionRemoved;
public event OnActionReplaced? ActionReplaced;
public event Action? ActionsUpdated;
public event Action<ActionsComponent>? LinkActions;
@@ -35,7 +37,11 @@ namespace Content.Client.Actions
public event Action? ClearAssignments;
public event Action<List<SlotAssignment>>? AssignSlot;
public ActionsComponent? PlayerActions { get; private set; }
/// <summary>
/// Queue of entities with <see cref="ActionsComponent"/> that needs to be updated after
/// handling a state.
/// </summary>
private readonly Queue<EntityUid> _actionHoldersQueue = new();
public override void Initialize()
{
@@ -45,110 +51,90 @@ namespace Content.Client.Actions
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleComponentState);
}
public override void Dirty(ActionType action)
public override void Dirty(EntityUid? actionId)
{
if (_playerManager.LocalPlayer?.ControlledEntity != action.AttachedEntity)
var action = GetActionData(actionId);
if (_playerManager.LocalPlayer?.ControlledEntity != action?.AttachedEntity)
return;
base.Dirty(action);
base.Dirty(actionId);
ActionsUpdated?.Invoke();
}
private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
{
if (args.Current is not ActionsComponentState state)
if (args.Current is not ActionsComponentState)
return;
state.SortedActions ??= new SortedSet<ActionType>(state.Actions);
var serverActions = state.SortedActions;
var removed = new List<ActionType>();
foreach (var act in component.Actions.ToList())
{
if (act.ClientExclusive)
continue;
if (!serverActions.TryGetValue(act, out var serverAct))
{
component.Actions.Remove(act);
if (act.AutoRemove)
removed.Add(act);
continue;
_actionHoldersQueue.Enqueue(uid);
}
act.CopyFrom(serverAct);
}
var added = new List<ActionType>();
// Anything that remains is a new action
foreach (var newAct in serverActions)
{
if (component.Actions.Contains(newAct))
continue;
// We create a new action, not just sorting a reference to the state's action.
var action = (ActionType) newAct.Clone();
component.Actions.Add(action);
added.Add(action);
}
if (_playerManager.LocalPlayer?.ControlledEntity != uid)
return;
foreach (var action in removed)
{
ActionRemoved?.Invoke(action);
}
foreach (var action in added)
{
ActionAdded?.Invoke(action);
}
ActionsUpdated?.Invoke();
}
protected override void AddActionInternal(ActionsComponent comp, ActionType action)
protected override void AddActionInternal(EntityUid actionId, IContainer container)
{
// Sometimes the client receives actions from the server, before predicting that newly added components will add
// their own shared actions. Just in case those systems ever decided to directly access action properties (e.g.,
// action.Toggled), we will remove duplicates:
if (comp.Actions.TryGetValue(action, out var existing))
if (container.Contains(actionId))
{
comp.Actions.Remove(existing);
ActionReplaced?.Invoke(existing, action);
ActionReplaced?.Invoke(actionId);
}
else
{
container.Insert(actionId);
}
}
comp.Actions.Add(action);
}
public override void AddAction(EntityUid uid, ActionType action, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
public override void AddAction(EntityUid holderId, EntityUid actionId, EntityUid? provider, ActionsComponent? holder = null, BaseActionComponent? action = null, bool dirty = true, IContainer? actionContainer = null)
{
if (!Resolve(uid, ref comp, false))
if (!Resolve(holderId, ref holder, false))
return;
action ??= GetActionData(actionId);
if (action == null)
{
Log.Warning($"No {nameof(BaseActionComponent)} found on entity {actionId}");
return;
}
dirty &= !action.ClientExclusive;
base.AddAction(uid, action, provider, comp, dirty);
base.AddAction(holderId, actionId, provider, holder, action, dirty, actionContainer);
if (uid == _playerManager.LocalPlayer?.ControlledEntity)
ActionAdded?.Invoke(action);
if (holderId == _playerManager.LocalPlayer?.ControlledEntity)
ActionAdded?.Invoke(actionId);
}
public override void RemoveAction(EntityUid uid, ActionType action, ActionsComponent? comp = null, bool dirty = true)
public override void RemoveAction(EntityUid holderId, EntityUid? actionId, ActionsComponent? comp = null, BaseActionComponent? action = null, bool dirty = true, ContainerManagerComponent? actionContainer = null)
{
if (GameTiming.ApplyingState && !action.ClientExclusive)
if (GameTiming.ApplyingState)
return;
if (!Resolve(uid, ref comp, false))
if (!Resolve(holderId, ref comp, false))
return;
dirty &= !action.ClientExclusive;
base.RemoveAction(uid, action, comp, dirty);
if (actionId == null)
return;
if (action.AutoRemove && uid == _playerManager.LocalPlayer?.ControlledEntity)
ActionRemoved?.Invoke(action);
action ??= GetActionData(actionId);
if (action is { ClientExclusive: false })
return;
dirty &= !action?.ClientExclusive ?? true;
base.RemoveAction(holderId, actionId, comp, action, dirty, actionContainer);
if (_playerManager.LocalPlayer?.ControlledEntity != holderId)
return;
if (action == null || action.AutoRemove)
ActionRemoved?.Invoke(actionId.Value);
}
public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()
{
if (_playerManager.LocalPlayer?.ControlledEntity is not { } user)
return Enumerable.Empty<(EntityUid, BaseActionComponent)>();
return GetActions(user);
}
private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args)
@@ -163,20 +149,18 @@ namespace Content.Client.Actions
public void UnlinkAllActions()
{
PlayerActions = null;
UnlinkActions?.Invoke();
}
public void LinkAllActions(ActionsComponent? actions = null)
{
var player = _playerManager.LocalPlayer?.ControlledEntity;
if (player == null || !Resolve(player.Value, ref actions, false))
if (_playerManager.LocalPlayer?.ControlledEntity is not { } user ||
!Resolve(user, ref actions, false))
{
return;
}
LinkActions?.Invoke(actions);
PlayerActions = actions;
}
public override void Shutdown()
@@ -185,29 +169,30 @@ namespace Content.Client.Actions
CommandBinds.Unregister<ActionsSystem>();
}
public void TriggerAction(ActionType? action)
public void TriggerAction(EntityUid actionId, BaseActionComponent action)
{
if (_playerManager.LocalPlayer?.ControlledEntity is not { } user ||
!TryComp(user, out ActionsComponent? actions))
{
if (PlayerActions == null || action == null || _playerManager.LocalPlayer?.ControlledEntity is not { Valid: true } user)
return;
}
if (action.Provider != null && Deleted(action.Provider))
return;
if (action is not InstantAction instantAction)
{
if (action is not InstantActionComponent instantAction)
return;
}
if (action.ClientExclusive)
{
if (instantAction.Event != null)
instantAction.Event.Performer = user;
PerformAction(user, PlayerActions, instantAction, instantAction.Event, GameTiming.CurTime);
PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime);
}
else
{
var request = new RequestPerformActionEvent(instantAction);
var request = new RequestPerformActionEvent(actionId);
EntityManager.RaisePredictiveEvent(request);
}
}
@@ -239,7 +224,7 @@ namespace Content.Client.Actions
/// </summary>
public void LoadActionAssignments(string path, bool userData)
{
if (PlayerActions == null)
if (_playerManager.LocalPlayer?.ControlledEntity is not { } user)
return;
var file = new ResPath(path).ToRootedPath();
@@ -265,17 +250,13 @@ namespace Content.Client.Actions
if (!map.TryGet("action", out var actionNode))
continue;
var action = _serialization.Read<ActionType>(actionNode, notNullableOverride: true);
var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
var actionId = Spawn(null);
AddComp<Component>(actionId, action);
AddAction(user, actionId, null);
if (PlayerActions.Actions.TryGetValue(action, out var existingAction))
{
existingAction.CopyFrom(action);
action = existingAction;
}
else
{
PlayerActions.Actions.Add(action);
}
if (map.TryGet<ValueDataNode>("name", out var nameNode))
_metaData.SetEntityName(actionId, nameNode.Value);
if (!map.TryGet("assignments", out var assignmentNode))
continue;
@@ -284,7 +265,7 @@ namespace Content.Client.Actions
foreach (var index in nodeAssignments)
{
var assignment = new SlotAssignment(index.Hotbar, index.Slot, action);
var assignment = new SlotAssignment(index.Hotbar, index.Slot, actionId);
assignments.Add(assignment);
}
}
@@ -292,6 +273,73 @@ namespace Content.Client.Actions
AssignSlot?.Invoke(assignments);
}
public record struct SlotAssignment(byte Hotbar, byte Slot, ActionType Action);
public override void Update(float frameTime)
{
base.Update(frameTime);
if (_actionHoldersQueue.Count == 0)
return;
var removed = new List<EntityUid>();
var added = new List<EntityUid>();
var query = GetEntityQuery<ActionsComponent>();
var queue = new Queue<EntityUid>(_actionHoldersQueue);
_actionHoldersQueue.Clear();
while (queue.TryDequeue(out var holderId))
{
if (!TryGetContainer(holderId, out var container) || container.ExpectedEntities.Count > 0)
{
_actionHoldersQueue.Enqueue(holderId);
continue;
}
if (!query.TryGetComponent(holderId, out var holder))
continue;
removed.Clear();
added.Clear();
foreach (var (act, data) in holder.OldClientActions.ToList())
{
if (data.ClientExclusive)
continue;
if (!container.Contains(act))
{
holder.OldClientActions.Remove(act);
if (data.AutoRemove)
removed.Add(act);
}
}
// Anything that remains is a new action
foreach (var newAct in container.ContainedEntities)
{
if (!holder.OldClientActions.ContainsKey(newAct))
added.Add(newAct);
if (TryGetActionData(newAct, out var serverData))
holder.OldClientActions[newAct] = new ActionMetaData(serverData.ClientExclusive, serverData.AutoRemove);
}
if (_playerManager.LocalPlayer?.ControlledEntity != holderId)
return;
foreach (var action in removed)
{
ActionRemoved?.Invoke(action);
}
foreach (var action in added)
{
ActionAdded?.Invoke(action);
}
ActionsUpdated?.Invoke();
}
}
public record struct SlotAssignment(byte Hotbar, byte Slot, EntityUid ActionId);
}
}

View File

@@ -1,9 +1,5 @@
using System;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Client.Stylesheets;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;

View File

@@ -2,7 +2,6 @@ using System.Numerics;
using Content.Client.Actions;
using Content.Client.Decals.Overlays;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Decals;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -10,7 +9,6 @@ using Robust.Client.Input;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Client.Decals;
@@ -22,6 +20,7 @@ public sealed class DecalPlacementSystem : EntitySystem
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly InputSystem _inputSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
@@ -153,9 +152,10 @@ public sealed class DecalPlacementSystem : EntitySystem
Cleanable = _cleanable,
};
ev.Action = new WorldTargetAction()
var actionId = Spawn(null);
AddComp(actionId, new WorldTargetActionComponent
{
DisplayName = $"{_decalId} ({_decalColor.ToHex()}, {(int) _decalAngle.Degrees})", // non-unique actions may be considered duplicates when saving/loading.
// non-unique actions may be considered duplicates when saving/loading.
Icon = decalProto.Sprite,
Repeat = true,
ClientExclusive = true,
@@ -164,7 +164,11 @@ public sealed class DecalPlacementSystem : EntitySystem
Range = -1,
Event = actionEvent,
IconColor = _decalColor,
};
});
_metaData.SetEntityName(actionId, $"{_decalId} ({_decalColor.ToHex()}, {(int) _decalAngle.Degrees})");
ev.Action = actionId;
}
public override void Shutdown()
@@ -194,24 +198,3 @@ public sealed class DecalPlacementSystem : EntitySystem
_inputSystem.SetEntityContextActive();
}
}
public sealed partial class PlaceDecalActionEvent : WorldTargetActionEvent
{
[DataField("decalId", customTypeSerializer:typeof(PrototypeIdSerializer<DecalPrototype>), required:true)]
public string DecalId = string.Empty;
[DataField("color")]
public Color Color;
[DataField("rotation")]
public double Rotation;
[DataField("snap")]
public bool Snap;
[DataField("zIndex")]
public int ZIndex;
[DataField("cleanable")]
public bool Cleanable;
}

View File

@@ -2,12 +2,10 @@ using Content.Client.Movement.Systems;
using Content.Shared.Actions;
using Content.Shared.Ghost;
using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Client.Console;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.GameStates;
namespace Content.Client.Ghost
{
@@ -81,9 +79,9 @@ namespace Content.Client.Ghost
sprite.Visible = GhostVisibility;
}
_actions.AddAction(uid, component.ToggleLightingAction, null);
_actions.AddAction(uid, component.ToggleFoVAction, null);
_actions.AddAction(uid, component.ToggleGhostsAction, null);
_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)
@@ -118,9 +116,9 @@ namespace Content.Client.Ghost
private void OnGhostRemove(EntityUid uid, GhostComponent component, ComponentRemove args)
{
_actions.RemoveAction(uid, component.ToggleLightingAction);
_actions.RemoveAction(uid, component.ToggleFoVAction);
_actions.RemoveAction(uid, component.ToggleGhostsAction);
_actions.RemoveAction(uid, component.ToggleLightingActionEntity);
_actions.RemoveAction(uid, component.ToggleFoVActionEntity);
_actions.RemoveAction(uid, component.ToggleGhostsActionEntity);
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
return;

View File

@@ -1,10 +1,11 @@
using Content.Client.Actions;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Mapping;
using Content.Shared.Maps;
using Robust.Client.Placement;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using static Robust.Shared.Utility.SpriteSpecifier;
namespace Content.Client.Mapping;
@@ -13,16 +14,17 @@ public sealed partial class MappingSystem : EntitySystem
[Dependency] private readonly IPlacementManager _placementMan = default!;
[Dependency] private readonly ITileDefinitionManager _tileMan = default!;
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
/// <summary>
/// The icon to use for space tiles.
/// </summary>
private readonly SpriteSpecifier _spaceIcon = new SpriteSpecifier.Texture(new ("Tiles/cropped_parallax.png"));
private readonly SpriteSpecifier _spaceIcon = new Texture(new ("Tiles/cropped_parallax.png"));
/// <summary>
/// The icon to use for entity-eraser.
/// </summary>
private readonly SpriteSpecifier _deleteIcon = new SpriteSpecifier.Texture(new ("Interface/VerbIcons/delete.svg.192dpi.png"));
private readonly SpriteSpecifier _deleteIcon = new Texture(new ("Interface/VerbIcons/delete.svg.192dpi.png"));
public string DefaultMappingActions = "/mapping_actions.yml";
@@ -73,6 +75,9 @@ public sealed partial class MappingSystem : EntitySystem
else
return;
InstantActionComponent action;
string name;
if (tileDef != null)
{
if (tileDef is not ContentTileDefinition contentTileDef)
@@ -80,45 +85,51 @@ public sealed partial class MappingSystem : EntitySystem
var tileIcon = contentTileDef.IsSpace
? _spaceIcon
: new SpriteSpecifier.Texture(contentTileDef.Sprite!.Value);
: new Texture(contentTileDef.Sprite!.Value);
ev.Action = new InstantAction()
action = new InstantActionComponent
{
ClientExclusive = true,
CheckCanInteract = false,
Event = actionEvent,
DisplayName = Loc.GetString(tileDef.Name),
Icon = tileIcon
};
return;
name = Loc.GetString(tileDef.Name);
}
if (actionEvent.Eraser)
else if (actionEvent.Eraser)
{
ev.Action = new InstantAction()
action = new InstantActionComponent
{
ClientExclusive = true,
CheckCanInteract = false,
Event = actionEvent,
DisplayName = "action-name-mapping-erase",
Icon = _deleteIcon,
};
return;
name = Loc.GetString("action-name-mapping-erase");
}
else
{
if (string.IsNullOrWhiteSpace(actionEvent.EntityType))
return;
ev.Action = new InstantAction()
action = new InstantActionComponent
{
ClientExclusive = true,
CheckCanInteract = false,
Event = actionEvent,
DisplayName = actionEvent.EntityType,
Icon = new SpriteSpecifier.EntityPrototype(actionEvent.EntityType),
Icon = new EntityPrototype(actionEvent.EntityType),
};
name = actionEvent.EntityType;
}
var actionId = Spawn(null);
AddComp<Component>(actionId, action);
_metaData.SetEntityName(actionId, name);
ev.Action = actionId;
}
private void OnStartPlacementAction(StartPlacementActionEvent args)
@@ -140,18 +151,3 @@ public sealed partial class MappingSystem : EntitySystem
_placementMan.ToggleEraser();
}
}
public sealed partial class StartPlacementActionEvent : InstantActionEvent
{
[DataField("entityType")]
public string? EntityType;
[DataField("tileId")]
public string? TileId;
[DataField("placementOption")]
public string? PlacementOption;
[DataField("eraser")]
public bool Eraser;
}

View File

@@ -3,7 +3,6 @@ using Content.Client.Actions;
using Content.Client.Items;
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.DeviceNetwork.Systems;
using Content.Shared.Input;
@@ -22,11 +21,11 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
private const string Action = "ClearNetworkLinkOverlays";
[ValidatePrototypeId<EntityPrototype>]
private const string Action = "ActionClearNetworkLinkOverlays";
public override void Initialize()
{
@@ -71,7 +70,7 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
if (!EntityQuery<NetworkConfiguratorActiveLinkOverlayComponent>().Any())
{
_overlay.RemoveOverlay<NetworkConfiguratorLinkOverlay>();
_actions.RemoveAction(_playerManager.LocalPlayer.ControlledEntity.Value, _prototypeManager.Index<InstantActionPrototype>(Action));
_actions.RemoveAction(_playerManager.LocalPlayer.ControlledEntity.Value, Action);
}
@@ -81,7 +80,7 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
if (!_overlay.HasOverlay<NetworkConfiguratorLinkOverlay>())
{
_overlay.AddOverlay(new NetworkConfiguratorLinkOverlay());
_actions.AddAction(_playerManager.LocalPlayer.ControlledEntity.Value, new InstantAction(_prototypeManager.Index<InstantActionPrototype>(Action)), null);
_actions.AddAction(_playerManager.LocalPlayer.ControlledEntity.Value, Spawn(Action), null);
}
EnsureComp<NetworkConfiguratorActiveLinkOverlayComponent>(component.ActiveDeviceList.Value);
@@ -103,7 +102,7 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
if (_playerManager.LocalPlayer?.ControlledEntity != null)
{
_actions.RemoveAction(_playerManager.LocalPlayer.ControlledEntity.Value, _prototypeManager.Index<InstantActionPrototype>(Action));
_actions.RemoveAction(_playerManager.LocalPlayer.ControlledEntity.Value, Action);
}
}

View File

@@ -1,15 +1,15 @@
using System.Linq;
using Content.Client.Actions;
using Content.Client.Message;
using Content.Shared.FixedPoint;
using Content.Shared.Store;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Client.Graphics;
using Content.Shared.Actions.ActionTypes;
using System.Linq;
using Content.Shared.FixedPoint;
namespace Content.Client.Store.Ui;
@@ -128,10 +128,13 @@ public sealed partial class StoreMenu : DefaultWindow
}
else if (listing.ProductAction != null)
{
var action = _prototypeManager.Index<InstantActionPrototype>(listing.ProductAction);
if (action.Icon != null)
var actionId = _entityManager.Spawn(listing.ProductAction);
if (_entityManager.System<ActionsSystem>().TryGetActionData(actionId, out var action) &&
action.Icon != null)
{
texture = spriteSys.Frame0(action.Icon);
}
}
var newListing = new StoreListingControl(listingName, listingDesc, GetListingPriceString(listing), canBuy, texture);
newListing.StoreItemBuyButton.OnButtonDown += args

View File

@@ -13,7 +13,6 @@ using Content.Client.UserInterface.Systems.Actions.Widgets;
using Content.Client.UserInterface.Systems.Actions.Windows;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Input;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -65,7 +64,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
/// <summary>
/// Action slot we are currently selecting a target for.
/// </summary>
public TargetedAction? SelectingTargetFor { get; private set; }
public EntityUid? SelectingTargetFor { get; private set; }
public ActionUIController()
{
@@ -175,7 +174,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
/// </summary>
private bool TargetingOnUse(in PointerInputCmdArgs args)
{
if (!_timing.IsFirstTimePredicted || _actionsSystem == null || SelectingTargetFor is not { } action)
if (!_timing.IsFirstTimePredicted || _actionsSystem == null || SelectingTargetFor is not { } actionId)
return false;
if (_playerManager.LocalPlayer?.ControlledEntity is not { } user)
@@ -184,6 +183,12 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
if (!EntityManager.TryGetComponent(user, out ActionsComponent? comp))
return false;
if (!_actionsSystem.TryGetActionData(actionId, out var baseAction) ||
baseAction is not BaseTargetActionComponent action)
{
return false;
}
// Is the action currently valid?
if (!action.Enabled
|| action.Charges is 0
@@ -196,19 +201,19 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
switch (action)
{
case WorldTargetAction mapTarget:
return TryTargetWorld(args, mapTarget, user, comp) || !action.InteractOnMiss;
case WorldTargetActionComponent mapTarget:
return TryTargetWorld(args, actionId, mapTarget, user, comp) || !mapTarget.InteractOnMiss;
case EntityTargetAction entTarget:
return TryTargetEntity(args, entTarget, user, comp) || !action.InteractOnMiss;
case EntityTargetActionComponent entTarget:
return TryTargetEntity(args, actionId, entTarget, user, comp) || !entTarget.InteractOnMiss;
default:
Logger.Error($"Unknown targeting action: {action.GetType()}");
Logger.Error($"Unknown targeting action: {actionId.GetType()}");
return false;
}
}
private bool TryTargetWorld(in PointerInputCmdArgs args, WorldTargetAction action, EntityUid user, ActionsComponent actionComp)
private bool TryTargetWorld(in PointerInputCmdArgs args, EntityUid actionId, WorldTargetActionComponent action, EntityUid user, ActionsComponent actionComp)
{
if (_actionsSystem == null)
return false;
@@ -232,10 +237,10 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
action.Event.Performer = user;
}
_actionsSystem.PerformAction(user, actionComp, action, action.Event, _timing.CurTime);
_actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
}
else
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, coords));
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(actionId, coords));
if (!action.Repeat)
StopTargeting();
@@ -243,7 +248,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
return true;
}
private bool TryTargetEntity(in PointerInputCmdArgs args, EntityTargetAction action, EntityUid user, ActionsComponent actionComp)
private bool TryTargetEntity(in PointerInputCmdArgs args, EntityUid actionId, EntityTargetActionComponent action, EntityUid user, ActionsComponent actionComp)
{
if (_actionsSystem == null)
return false;
@@ -264,10 +269,10 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
action.Event.Performer = user;
}
_actionsSystem.PerformAction(user, actionComp, action, action.Event, _timing.CurTime);
_actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
}
else
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, args.EntityUid));
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(actionId, args.EntityUid));
if (!action.Repeat)
StopTargeting();
@@ -322,13 +327,17 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void TriggerAction(int index)
{
if (CurrentPage[index] is not { } type)
if (_actionsSystem == null ||
CurrentPage[index] is not { } actionId ||
!_actionsSystem.TryGetActionData(actionId, out var baseAction))
{
return;
}
if (type is TargetedAction action)
ToggleTargeting(action);
if (baseAction is BaseTargetActionComponent action)
ToggleTargeting(actionId, action);
else
_actionsSystem?.TriggerAction(type);
_actionsSystem?.TriggerAction(actionId, baseAction);
}
private void ChangePage(int index)
@@ -360,14 +369,14 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
ChangePage(_currentPageIndex + 1);
}
private void AppendAction(ActionType action)
private void AppendAction(EntityUid action)
{
if (_container == null)
return;
foreach (var button in _container.GetButtons())
{
if (button.Action != null)
if (button.ActionId != null)
continue;
SetAction(button, action);
@@ -388,39 +397,45 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
}
}
private void OnActionAdded(ActionType action)
private void OnActionAdded(EntityUid actionId)
{
if (_actionsSystem == null ||
!_actionsSystem.TryGetActionData(actionId, out var action))
{
return;
}
// if the action is toggled when we add it, start targeting
if (action is TargetedAction targetAction && action.Toggled)
StartTargeting(targetAction);
if (action is BaseTargetActionComponent targetAction && action.Toggled)
StartTargeting(actionId, targetAction);
foreach (var page in _pages)
{
for (var i = 0; i < page.Size; i++)
{
if (page[i] == action)
if (page[i] == actionId)
{
return;
}
}
}
AppendAction(action);
AppendAction(actionId);
SearchAndDisplay();
}
private void OnActionRemoved(ActionType action)
private void OnActionRemoved(EntityUid actionId)
{
if (_container == null)
return;
// stop targeting if the action is removed
if (action == SelectingTargetFor)
if (actionId == SelectingTargetFor)
StopTargeting();
foreach (var button in _container.GetButtons())
{
if (button.Action == action)
if (button.ActionId == actionId)
{
SetAction(button, null);
}
@@ -430,7 +445,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
for (var i = 0; i < page.Size; i++)
{
if (page[i] == action)
if (page[i] == actionId)
{
page[i] = null;
}
@@ -440,15 +455,15 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
SearchAndDisplay();
}
private void OnActionReplaced(ActionType existing, ActionType action)
private void OnActionReplaced(EntityUid actionId)
{
if (_container == null)
return;
foreach (var button in _container.GetButtons())
{
if (button.Action == existing)
button.UpdateData(action);
if (button.ActionId == actionId)
button.UpdateData(actionId);
}
}
@@ -499,15 +514,15 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
}
}
private bool MatchesFilter(ActionType action, Filters filter)
private bool MatchesFilter(BaseActionComponent action, Filters filter)
{
return filter switch
{
Filters.Enabled => action.Enabled,
Filters.Item => action.Provider != null && action.Provider != _playerManager.LocalPlayer?.ControlledEntity,
Filters.Innate => action.Provider == null || action.Provider == _playerManager.LocalPlayer?.ControlledEntity,
Filters.Instant => action is InstantAction,
Filters.Targeted => action is TargetedAction,
Filters.Instant => action is InstantActionComponent,
Filters.Targeted => action is BaseTargetActionComponent,
_ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null)
};
}
@@ -518,7 +533,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
_window.ResultsGrid.RemoveAllChildren();
}
private void PopulateActions(IEnumerable<ActionType> actions)
private void PopulateActions(IEnumerable<(EntityUid Id, BaseActionComponent Comp)> actions)
{
if (_window == null)
return;
@@ -529,7 +544,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
var button = new ActionButton {Locked = true};
button.UpdateData(action);
button.UpdateData(action.Id);
button.ActionPressed += OnWindowActionPressed;
button.ActionUnpressed += OnWindowActionUnPressed;
button.ActionFocusExited += OnWindowActionFocusExisted;
@@ -538,16 +553,14 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
}
}
private void SearchAndDisplay(ActionsComponent? component = null)
private void SearchAndDisplay()
{
if (_window == null)
if (_window is not { Disposed: false } || _actionsSystem == null)
return;
var search = _window.SearchBar.Text;
var filters = _window.FilterButton.SelectedKeys;
IEnumerable<ActionType>? actions = (component ?? _actionsSystem?.PlayerActions)?.Actions;
actions ??= Array.Empty<ActionType>();
var actions = _actionsSystem.GetClientActions();
if (filters.Count == 0 && string.IsNullOrWhiteSpace(search))
{
@@ -557,45 +570,46 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
actions = actions.Where(action =>
{
if (filters.Count > 0 && filters.Any(filter => !MatchesFilter(action, filter)))
if (filters.Count > 0 && filters.Any(filter => !MatchesFilter(action.Comp, filter)))
return false;
if (action.Keywords.Any(keyword => search.Contains(keyword, StringComparison.OrdinalIgnoreCase)))
if (action.Comp.Keywords.Any(keyword => search.Contains(keyword, StringComparison.OrdinalIgnoreCase)))
return true;
if (action.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase))
var name = EntityManager.GetComponent<MetaDataComponent>(action.Id).EntityName;
if (name.Contains(search, StringComparison.OrdinalIgnoreCase))
return true;
if (action.Provider == null || action.Provider == _playerManager.LocalPlayer?.ControlledEntity)
if (action.Comp.Provider == null || action.Comp.Provider == _playerManager.LocalPlayer?.ControlledEntity)
return false;
var name = EntityManager.GetComponent<MetaDataComponent>(action.Provider.Value).EntityName;
return name.Contains(search, StringComparison.OrdinalIgnoreCase);
var providerName = EntityManager.GetComponent<MetaDataComponent>(action.Comp.Provider.Value).EntityName;
return providerName.Contains(search, StringComparison.OrdinalIgnoreCase);
});
PopulateActions(actions);
}
private void SetAction(ActionButton button, ActionType? type)
private void SetAction(ActionButton button, EntityUid? actionId)
{
int position;
if (type == null)
if (actionId == null)
{
button.ClearData();
if (_container?.TryGetButtonIndex(button, out position) ?? false)
{
CurrentPage[position] = type;
CurrentPage[position] = actionId;
}
return;
}
if (button.TryReplaceWith(type) &&
if (button.TryReplaceWith(actionId.Value) &&
_container != null &&
_container.TryGetButtonIndex(button, out position))
{
CurrentPage[position] = type;
CurrentPage[position] = actionId;
}
}
@@ -603,7 +617,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
if (UIManager.CurrentlyHovered is ActionButton button)
{
if (!_menuDragHelper.IsDragging || _menuDragHelper.Dragged?.Action is not { } type)
if (!_menuDragHelper.IsDragging || _menuDragHelper.Dragged?.ActionId is not { } type)
{
_menuDragHelper.EndDrag();
return;
@@ -669,7 +683,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
if (args.Function == EngineKeyFunctions.UIClick)
{
if (button.Action == null)
if (button.ActionId == null)
{
var ev = new FillActionSlotEvent();
EntityManager.EventBus.RaiseEvent(EventSource.Local, ev);
@@ -692,25 +706,28 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
{
if (args.Function != EngineKeyFunctions.UIClick)
if (args.Function != EngineKeyFunctions.UIClick || _actionsSystem == null)
return;
if (UIManager.CurrentlyHovered == button)
{
_menuDragHelper.EndDrag();
if (button.Action is TargetedAction action)
if (_actionsSystem.TryGetActionData(button.ActionId, out var baseAction))
{
if (baseAction is BaseTargetActionComponent action)
{
// for target actions, we go into "select target" mode, we don't
// message the server until we actually pick our target.
// if we're clicking the same thing we're already targeting for, then we simply cancel
// targeting
ToggleTargeting(action);
ToggleTargeting(button.ActionId.Value, action);
return;
}
_actionsSystem?.TriggerAction(button.Action);
_actionsSystem?.TriggerAction(button.ActionId.Value, baseAction);
}
}
else
{
@@ -722,7 +739,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private bool OnMenuBeginDrag()
{
if (_menuDragHelper.Dragged?.Action is { } action)
if (_actionsSystem != null && _actionsSystem.TryGetActionData(_menuDragHelper.Dragged?.ActionId, out var action))
{
if (action.EntityIcon != null)
{
@@ -828,7 +845,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
foreach (ref var assignment in CollectionsMarshal.AsSpan(assignments))
{
_pages[assignment.Hotbar][assignment.Slot] = assignment.Action;
_pages[assignment.Hotbar][assignment.Slot] = assignment.ActionId;
}
_container?.SetActionData(_pages[_currentPageIndex]);
@@ -864,7 +881,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
LoadDefaultActions(component);
_container?.SetActionData(_pages[DefaultPageIndex]);
SearchAndDisplay(component);
SearchAndDisplay();
}
private void OnComponentUnlinked()
@@ -876,7 +893,10 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void LoadDefaultActions(ActionsComponent component)
{
var actions = component.Actions.Where(actionType => actionType.AutoPopulate).ToList();
if (_actionsSystem == null)
return;
var actions = _actionsSystem.GetClientActions().Where(action => action.Comp.AutoPopulate).ToList();
var offset = 0;
var totalPages = _pages.Count;
@@ -892,7 +912,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
var actionIndex = slot + offset;
if (actionIndex < actions.Count)
{
page[slot] = actions[slot + offset];
page[slot] = actions[slot + offset].Id;
}
else
{
@@ -916,26 +936,26 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
/// If currently targeting with no slot or a different slot, switches to
/// targeting with the specified slot.
/// </summary>
private void ToggleTargeting(TargetedAction action)
private void ToggleTargeting(EntityUid actionId, BaseTargetActionComponent action)
{
if (SelectingTargetFor == action)
if (SelectingTargetFor == actionId)
{
StopTargeting();
return;
}
StartTargeting(action);
StartTargeting(actionId, action);
}
/// <summary>
/// Puts us in targeting mode, where we need to pick either a target point or entity
/// </summary>
private void StartTargeting(TargetedAction action)
private void StartTargeting(EntityUid actionId, BaseTargetActionComponent action)
{
// If we were targeting something else we should stop
StopTargeting();
SelectingTargetFor = action;
SelectingTargetFor = actionId;
// override "held-item" overlay
if (action.TargetingIndicator && _overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
@@ -955,7 +975,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
// - Add a yes/no checkmark where the HandItemOverlay usually is
// Highlight valid entity targets
if (action is not EntityTargetAction entityAction)
if (action is not EntityTargetActionComponent entityAction)
return;
Func<EntityUid, bool>? predicate = null;
@@ -992,20 +1012,20 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
//TODO: Serialize this shit
private sealed class ActionPage
{
private readonly ActionType?[] _data;
private readonly EntityUid?[] _data;
public ActionPage(int size)
{
_data = new ActionType?[size];
_data = new EntityUid?[size];
}
public ActionType? this[int index]
public EntityUid? this[int index]
{
get => _data[index];
set => _data[index] = value;
}
public static implicit operator ActionType?[](ActionPage p)
public static implicit operator EntityUid?[](ActionPage p)
{
return p._data.ToArray();
}

View File

@@ -1,9 +1,9 @@
using System.Numerics;
using Content.Client.Actions;
using Content.Client.Actions.UI;
using Content.Client.Cooldown;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@@ -19,11 +19,14 @@ namespace Content.Client.UserInterface.Systems.Actions.Controls;
public sealed class ActionButton : Control
{
private IEntityManager? _entities;
private ActionUIController Controller => UserInterfaceManager.GetUIController<ActionUIController>();
private IEntityManager Entities => _entities ??= IoCManager.Resolve<IEntityManager>();
private ActionsSystem Actions => Entities.System<ActionsSystem>();
private bool _beingHovered;
private bool _depressed;
private bool _toggled;
private bool _spriteViewDirty;
public BoundKeyFunction? KeyBind
{
@@ -48,7 +51,7 @@ public sealed class ActionButton : Control
private readonly SpriteView _smallItemSpriteView;
private readonly SpriteView _bigItemSpriteView;
public ActionType? Action { get; private set; }
public EntityUid? ActionId { get; private set; }
public bool Locked { get; set; }
public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionPressed;
@@ -176,11 +179,11 @@ public sealed class ActionButton : Control
private Control? SupplyTooltip(Control sender)
{
if (Action == null)
if (!Entities.TryGetComponent(ActionId, out MetaDataComponent? metadata))
return null;
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.DisplayName));
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description));
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
return new ActionAlertTooltip(name, decr);
}
@@ -192,18 +195,9 @@ public sealed class ActionButton : Control
private void UpdateItemIcon()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
if (Action?.EntityIcon != null && !entityManager.EntityExists(Action.EntityIcon))
{
// This is almost certainly because a player received/processed their own actions component state before
// being send the entity in their inventory that enabled this action.
// Defer updating icons to the next FrameUpdate().
_spriteViewDirty = true;
return;
}
if (Action?.EntityIcon is not { } entity || !entityManager.HasComponent<SpriteComponent>(entity))
if (!Actions.TryGetActionData(ActionId, out var action) ||
action is not { EntityIcon: { } entity } ||
!Entities.HasComponent<SpriteComponent>(entity))
{
_bigItemSpriteView.Visible = false;
_bigItemSpriteView.SetEntity(null);
@@ -212,7 +206,7 @@ public sealed class ActionButton : Control
}
else
{
switch (Action.ItemIconStyle)
switch (action.ItemIconStyle)
{
case ItemActionIconStyle.BigItem:
_bigItemSpriteView.Visible = true;
@@ -238,17 +232,17 @@ public sealed class ActionButton : Control
private void SetActionIcon(Texture? texture)
{
if (texture == null || Action == null)
if (!Actions.TryGetActionData(ActionId, out var action) || texture == null)
{
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
}
else if (Action.EntityIcon != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem)
else if (action.EntityIcon != null && action.ItemIconStyle == ItemActionIconStyle.BigItem)
{
_smallActionIcon.Texture = texture;
_smallActionIcon.Modulate = Action.IconColor;
_smallActionIcon.Modulate = action.IconColor;
_smallActionIcon.Visible = true;
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
@@ -256,7 +250,7 @@ public sealed class ActionButton : Control
else
{
_bigActionIcon.Texture = texture;
_bigActionIcon.Modulate = Action.IconColor;
_bigActionIcon.Modulate = action.IconColor;
_bigActionIcon.Visible = true;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
@@ -267,39 +261,39 @@ public sealed class ActionButton : Control
{
UpdateItemIcon();
if (Action == null)
if (!Actions.TryGetActionData(ActionId, out var action))
{
SetActionIcon(null);
return;
}
if ((Controller.SelectingTargetFor == Action || Action.Toggled) && Action.IconOn != null)
SetActionIcon(Action.IconOn.Frame0());
if ((Controller.SelectingTargetFor == ActionId || action.Toggled) && action.IconOn != null)
SetActionIcon(action.IconOn.Frame0());
else
SetActionIcon(Action.Icon?.Frame0());
SetActionIcon(action.Icon?.Frame0());
}
public bool TryReplaceWith(ActionType action)
public bool TryReplaceWith(EntityUid actionId)
{
if (Locked)
{
return false;
}
UpdateData(action);
UpdateData(actionId);
return true;
}
public void UpdateData(ActionType action)
public void UpdateData(EntityUid actionId)
{
Action = action;
ActionId = actionId;
Label.Visible = true;
UpdateIcons();
}
public void ClearData()
{
Action = null;
ActionId = null;
Cooldown.Visible = false;
Cooldown.Progress = 1;
Label.Visible = false;
@@ -310,20 +304,19 @@ public sealed class ActionButton : Control
{
base.FrameUpdate(args);
if (_spriteViewDirty)
if (!Actions.TryGetActionData(ActionId, out var action))
{
_spriteViewDirty = false;
UpdateIcons();
return;
}
if (Action?.Cooldown != null)
if (action.Cooldown != null)
{
Cooldown.FromTime(Action.Cooldown.Value.Start, Action.Cooldown.Value.End);
Cooldown.FromTime(action.Cooldown.Value.Start, action.Cooldown.Value.End);
}
if (Action != null && _toggled != Action.Toggled)
if (ActionId != null && _toggled != action.Toggled)
{
_toggled = Action.Toggled;
_toggled = action.Toggled;
}
}
@@ -350,7 +343,7 @@ public sealed class ActionButton : Control
public void Depress(GUIBoundKeyEventArgs args, bool depress)
{
// action can still be toggled if it's allowed to stay selected
if (Action is not {Enabled: true})
if (!Actions.TryGetActionData(ActionId, out var action) || action is not { Enabled: true })
return;
if (_depressed && !depress)
@@ -368,14 +361,14 @@ public sealed class ActionButton : Control
HighlightRect.Visible = _beingHovered;
// always show the normal empty button style if no action in this slot
if (Action == null)
if (!Actions.TryGetActionData(ActionId, out var action))
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
return;
}
// show a hover only if the action is usable or another action is being dragged on top of this
if (_beingHovered && (Controller.IsDragging || Action.Enabled))
if (_beingHovered && (Controller.IsDragging || action.Enabled))
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
}
@@ -390,16 +383,16 @@ public sealed class ActionButton : Control
}
// if it's toggled on, always show the toggled on style (currently same as depressed style)
if (Action.Toggled || Controller.SelectingTargetFor == Action)
if (action.Toggled || Controller.SelectingTargetFor == ActionId)
{
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
SetOnlyStylePseudoClass(Action.IconOn != null
SetOnlyStylePseudoClass(action.IconOn != null
? ContainerButton.StylePseudoClassNormal
: ContainerButton.StylePseudoClassPressed);
return;
}
if (!Action.Enabled)
if (!action.Enabled)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
return;

View File

@@ -1,4 +1,3 @@
using Content.Shared.Actions.ActionTypes;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -30,7 +29,7 @@ public class ActionButtonContainer : GridContainer
}
}
public void SetActionData(params ActionType?[] actionTypes)
public void SetActionData(params EntityUid?[] actionTypes)
{
ClearActionData();
@@ -40,7 +39,7 @@ public class ActionButtonContainer : GridContainer
if (action == null)
continue;
((ActionButton) GetChild(i)).UpdateData(action);
((ActionButton) GetChild(i)).UpdateData(action.Value);
}
}

View File

@@ -1,6 +1,5 @@
using System.Linq;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.CombatMode;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
@@ -25,6 +24,8 @@ public sealed class ActionsAddedTest
var sEntMan = server.ResolveDependency<IEntityManager>();
var cEntMan = client.ResolveDependency<IEntityManager>();
var session = server.ResolveDependency<IPlayerManager>().ServerSessions.Single();
var sActionSystem = server.System<SharedActionsSystem>();
var cActionSystem = client.System<SharedActionsSystem>();
// Dummy ticker is disabled - client should be in control of a normal mob.
Assert.NotNull(session.AttachedEntity);
@@ -43,21 +44,24 @@ public sealed class ActionsAddedTest
// This action should have a non-null event both on the server & client.
var evType = typeof(ToggleCombatActionEvent);
var sActions = sComp!.Actions.Where(
x => x is InstantAction act && act.Event?.GetType() == evType).ToArray();
var cActions = cComp!.Actions.Where(
x => x is InstantAction act && act.Event?.GetType() == evType).ToArray();
var sActions = sActionSystem.GetActions(ent).Where(
x => x.Comp is InstantActionComponent act && act.Event?.GetType() == evType).ToArray();
var cActions = cActionSystem.GetActions(ent).Where(
x => x.Comp is InstantActionComponent act && act.Event?.GetType() == evType).ToArray();
Assert.That(sActions.Length, Is.EqualTo(1));
Assert.That(cActions.Length, Is.EqualTo(1));
var sAct = (InstantAction) sActions[0];
var cAct = (InstantAction) cActions[0];
var sAct = sActions[0].Comp;
var cAct = cActions[0].Comp;
Assert.NotNull(sAct);
Assert.NotNull(cAct);
// Finally, these two actions are not the same object
// required, because integration tests do not respect the [NonSerialized] attribute and will simply events by reference.
Assert.That(ReferenceEquals(sAct, cAct), Is.False);
Assert.That(ReferenceEquals(sAct.Event, cAct.Event), Is.False);
Assert.That(ReferenceEquals(sAct.BaseEvent, cAct.BaseEvent), Is.False);
await pair.CleanReturnAsync();
}

View File

@@ -1,8 +1,6 @@
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
namespace Content.Server.Abilities.Mime
{
@@ -22,18 +20,12 @@ namespace Content.Server.Abilities.Mime
/// The wall prototype to use.
/// </summary>
[DataField("wallPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string WallPrototype = "WallInvisible";
public string WallPrototype = "ActionMimeInvisibleWall";
[DataField("invisibleWallAction")]
public InstantAction InvisibleWallAction = new()
{
UseDelay = TimeSpan.FromSeconds(30),
Icon = new SpriteSpecifier.Texture(new("Structures/Walls/solid.rsi/full.png")),
DisplayName = "mime-invisible-wall",
Description = "mime-invisible-wall-desc",
Priority = -1,
Event = new InvisibleWallActionEvent(),
};
[DataField("invisibleWallAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? InvisibleWallAction;
[DataField("invisibleWallActionEntity")] public EntityUid? InvisibleWallActionEntity;
// The vow zone lies below
public bool VowBroken = false;

View File

@@ -1,16 +1,15 @@
using Content.Server.Popups;
using Content.Server.Speech.Muting;
using Content.Shared.Actions;
using Content.Shared.Actions.Events;
using Content.Shared.Alert;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Physics;
using Content.Shared.Doors.Components;
using Content.Shared.Maps;
using Content.Shared.Mobs.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
using Content.Server.Speech.Muting;
using Content.Shared.Physics;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Timing;
namespace Content.Server.Abilities.Mime
{
@@ -29,8 +28,10 @@ namespace Content.Server.Abilities.Mime
{
base.Initialize();
SubscribeLocalEvent<MimePowersComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<MimePowersComponent, MapInitEvent>(OnComponentMapInit);
SubscribeLocalEvent<MimePowersComponent, InvisibleWallActionEvent>(OnInvisibleWall);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -53,10 +54,14 @@ namespace Content.Server.Abilities.Mime
private void OnComponentInit(EntityUid uid, MimePowersComponent component, ComponentInit args)
{
EnsureComp<MutedComponent>(uid);
_actionsSystem.AddAction(uid, component.InvisibleWallAction, uid);
_alertsSystem.ShowAlert(uid, AlertType.VowOfSilence);
}
private void OnComponentMapInit(EntityUid uid, MimePowersComponent component, MapInitEvent args)
{
_actionsSystem.AddAction(uid, ref component.InvisibleWallActionEntity, component.InvisibleWallAction, uid);
}
/// <summary>
/// Creates an invisible wall in a free space after some checks.
/// </summary>
@@ -116,7 +121,7 @@ namespace Content.Server.Abilities.Mime
RemComp<MutedComponent>(uid);
_alertsSystem.ClearAlert(uid, AlertType.VowOfSilence);
_alertsSystem.ShowAlert(uid, AlertType.VowBroken);
_actionsSystem.RemoveAction(uid, mimePowers.InvisibleWallAction);
_actionsSystem.RemoveAction(uid, mimePowers.InvisibleWallActionEntity);
}
/// <summary>
@@ -139,11 +144,7 @@ namespace Content.Server.Abilities.Mime
AddComp<MutedComponent>(uid);
_alertsSystem.ClearAlert(uid, AlertType.VowBroken);
_alertsSystem.ShowAlert(uid, AlertType.VowOfSilence);
_actionsSystem.AddAction(uid, mimePowers.InvisibleWallAction, uid);
_actionsSystem.AddAction(uid, ref mimePowers.InvisibleWallActionEntity, mimePowers.InvisibleWallAction, uid);
}
}
public sealed partial class InvisibleWallActionEvent : InstantActionEvent
{
}
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Interaction;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Actions;
@@ -19,12 +20,8 @@ namespace Content.Server.Actions;
[RegisterComponent]
public sealed partial class ActionOnInteractComponent : Component
{
[DataField("activateActions")]
public List<InstantAction>? ActivateActions;
[DataField("actions", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string>? Actions;
[DataField("entityActions")]
public List<EntityTargetAction>? EntityActions;
[DataField("worldActions")]
public List<WorldTargetAction>? WorldActions;
[DataField("actionEntities")] public List<EntityUid>? ActionEntities;
}

View File

@@ -1,5 +1,4 @@
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Interaction;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@@ -25,51 +24,41 @@ public sealed class ActionOnInteractSystem : EntitySystem
private void OnActivate(EntityUid uid, ActionOnInteractComponent component, ActivateInWorldEvent args)
{
if (args.Handled || component.ActivateActions == null)
if (args.Handled || component.ActionEntities == null)
return;
var options = new List<InstantAction>();
foreach (var action in component.ActivateActions)
{
if (ValidAction(action))
options.Add(action);
}
var options = GetValidActions<InstantActionComponent>(component.ActionEntities);
if (options.Count == 0)
return;
var act = _random.Pick(options);
var (actId, act) = _random.Pick(options);
if (act.Event != null)
act.Event.Performer = args.User;
act.Provider = uid;
_actions.PerformAction(args.User, null, act, act.Event, _timing.CurTime, false);
_actions.PerformAction(args.User, null, actId, act, act.Event, _timing.CurTime, false);
args.Handled = true;
}
private void OnAfterInteract(EntityUid uid, ActionOnInteractComponent component, AfterInteractEvent args)
{
if (args.Handled)
if (args.Handled || component.ActionEntities == null)
return;
// First, try entity target actions
if (args.Target != null && component.EntityActions != null)
if (args.Target != null)
{
var entOptions = new List<EntityTargetAction>();
foreach (var action in component.EntityActions)
var entOptions = GetValidActions<EntityTargetActionComponent>(component.ActionEntities, args.CanReach);
for (var i = entOptions.Count - 1; i >= 0; i--)
{
if (!ValidAction(action, args.CanReach))
continue;
var action = entOptions[i].Comp;
if (!_actions.ValidateEntityTarget(args.User, args.Target.Value, action))
continue;
entOptions.Add(action);
entOptions.RemoveAt(i);
}
if (entOptions.Count > 0)
{
var entAct = _random.Pick(entOptions);
var (entActId, entAct) = _random.Pick(entOptions);
if (entAct.Event != null)
{
entAct.Event.Performer = args.User;
@@ -77,32 +66,25 @@ public sealed class ActionOnInteractSystem : EntitySystem
}
entAct.Provider = uid;
_actions.PerformAction(args.User, null, entAct, entAct.Event, _timing.CurTime, false);
_actions.PerformAction(args.User, null, entActId, entAct, entAct.Event, _timing.CurTime, false);
args.Handled = true;
return;
}
}
// else: try world target actions
if (component.WorldActions == null)
return;
var options = new List<WorldTargetAction>();
foreach (var action in component.WorldActions)
var options = GetValidActions<WorldTargetActionComponent>(component.ActionEntities, args.CanReach);
for (var i = options.Count - 1; i >= 0; i--)
{
if (!ValidAction(action, args.CanReach))
continue;
var action = options[i].Comp;
if (!_actions.ValidateWorldTarget(args.User, args.ClickLocation, action))
continue;
options.Add(action);
options.RemoveAt(i);
}
if (options.Count == 0)
return;
var act = _random.Pick(options);
var (actId, act) = _random.Pick(options);
if (act.Event != null)
{
act.Event.Performer = args.User;
@@ -110,22 +92,44 @@ public sealed class ActionOnInteractSystem : EntitySystem
}
act.Provider = uid;
_actions.PerformAction(args.User, null, act, act.Event, _timing.CurTime, false);
_actions.PerformAction(args.User, null, actId, act, act.Event, _timing.CurTime, false);
args.Handled = true;
}
private bool ValidAction(ActionType act, bool canReach = true)
private bool ValidAction(BaseActionComponent action, bool canReach = true)
{
if (!act.Enabled)
if (!action.Enabled)
return false;
if (act.Charges.HasValue && act.Charges <= 0)
if (action.Charges.HasValue && action.Charges <= 0)
return false;
var curTime = _timing.CurTime;
if (act.Cooldown.HasValue && act.Cooldown.Value.End > curTime)
if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime)
return false;
return canReach || act is TargetedAction { CheckCanAccess: false };
return canReach || action is BaseTargetActionComponent { CheckCanAccess: false };
}
private List<(EntityUid Id, T Comp)> GetValidActions<T>(List<EntityUid>? actions, bool canReach = true) where T : BaseActionComponent
{
var valid = new List<(EntityUid Id, T Comp)>();
if (actions == null)
return valid;
foreach (var id in actions)
{
if (!_actions.TryGetActionData(id, out var baseAction) ||
baseAction as T is not { } action ||
!ValidAction(action, canReach))
{
continue;
}
valid.Add((id, action));
}
return valid;
}
}

View File

@@ -1,9 +1,5 @@
using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
namespace Content.Server.Actions
{

View File

@@ -1,8 +1,6 @@
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Storage;
using Robust.Shared.Audio;
using Robust.Shared.Serialization;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Animals.Components;
@@ -14,8 +12,8 @@ namespace Content.Server.Animals.Components;
[RegisterComponent]
public sealed partial class EggLayerComponent : Component
{
[DataField("eggLayAction", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string EggLayAction = "AnimalLayEgg";
[DataField("eggLayAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string EggLayAction = "ActionAnimalLayEgg";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("hungerUsage")]
@@ -51,5 +49,3 @@ public sealed partial class EggLayerComponent : Component
[DataField("accumulatedFrametime")]
public float AccumulatedFrametime;
}
public sealed partial class EggLayInstantActionEvent : InstantActionEvent {}

View File

@@ -1,20 +1,18 @@
using Content.Server.Actions;
using Content.Server.Animals.Components;
using Content.Server.Popups;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Actions.Events;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Storage;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Animals.Systems;
public sealed class EggLayerSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly AudioSystem _audio = default!;
@@ -54,10 +52,10 @@ public sealed class EggLayerSystem : EntitySystem
private void OnComponentInit(EntityUid uid, EggLayerComponent component, ComponentInit args)
{
if (!_prototype.TryIndex<InstantActionPrototype>(component.EggLayAction, out var action))
if (string.IsNullOrWhiteSpace(component.EggLayAction))
return;
_actions.AddAction(uid, new InstantAction(action), uid);
_actions.AddAction(uid, Spawn(component.EggLayAction), uid);
component.CurrentEggLayCooldown = _random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax);
}

View File

@@ -1,6 +1,7 @@
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Atmos;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Atmos.Components
{
@@ -93,8 +94,10 @@ namespace Content.Server.Atmos.Components
[DataField("tankFragmentScale"), ViewVariables(VVAccess.ReadWrite)]
public float TankFragmentScale = 2 * Atmospherics.OneAtmosphere;
[DataField("toggleAction", required: true)]
public InstantAction ToggleAction = new();
[DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ToggleAction = "ActionToggleInternals";
[DataField("toggleActionEntity")] public EntityUid? ToggleActionEntity;
/// <summary>
/// Valve to release gas from tank

View File

@@ -1,4 +1,3 @@
using System.Numerics;
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
@@ -8,17 +7,17 @@ using Content.Server.UserInterface;
using Content.Shared.Actions;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Toggleable;
using Content.Shared.Examine;
using Content.Shared.Toggleable;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Content.Shared.Verbs;
using Robust.Shared.Physics.Systems;
namespace Content.Server.Atmos.EntitySystems
{
@@ -105,7 +104,7 @@ namespace Content.Server.Atmos.EntitySystems
private void OnGetActions(EntityUid uid, GasTankComponent component, GetItemActionsEvent args)
{
args.Actions.Add(component.ToggleAction);
args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
}
private void OnExamined(EntityUid uid, GasTankComponent component, ExaminedEvent args)
@@ -233,7 +232,7 @@ namespace Content.Server.Atmos.EntitySystems
if (_internals.TryConnectTank(internals, component.Owner))
component.User = internals.Owner;
_actions.SetToggled(component.ToggleAction, component.IsConnected);
_actions.SetToggled(component.ToggleActionEntity, component.IsConnected);
// Couldn't toggle!
if (!component.IsConnected)
@@ -255,7 +254,7 @@ namespace Content.Server.Atmos.EntitySystems
var internals = GetInternalsComponent(component);
component.User = null;
_actions.SetToggled(component.ToggleAction, false);
_actions.SetToggled(component.ToggleActionEntity, false);
_internals.DisconnectTank(internals);
component.DisconnectStream?.Stop();

View File

@@ -2,9 +2,9 @@ using Content.Server.Actions;
using Content.Server.Bed.Components;
using Content.Server.Bed.Sleep;
using Content.Server.Body.Systems;
using Content.Server.Construction;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Bed;
using Content.Shared.Bed.Sleep;
using Content.Shared.Body.Components;
@@ -12,9 +12,7 @@ using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Server.Construction;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Bed
@@ -23,7 +21,6 @@ namespace Content.Server.Bed
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SleepingSystem _sleepingSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
@@ -42,18 +39,17 @@ namespace Content.Server.Bed
private void ManageUpdateList(EntityUid uid, HealOnBuckleComponent component, ref BuckleChangeEvent args)
{
_prototypeManager.TryIndex<InstantActionPrototype>("Sleep", out var sleepAction);
if (args.Buckling)
{
AddComp<HealOnBuckleHealingComponent>(uid);
component.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(component.HealTime);
if (sleepAction != null)
_actionsSystem.AddAction(args.BuckledEntity, new InstantAction(sleepAction), null);
component.SleepAction = Spawn(SleepingSystem.SleepActionId);
_actionsSystem.AddAction(args.BuckledEntity, component.SleepAction.Value, null);
return;
}
if (sleepAction != null)
_actionsSystem.RemoveAction(args.BuckledEntity, sleepAction);
if (component.SleepAction != null)
_actionsSystem.RemoveAction(args.BuckledEntity, component.SleepAction.Value);
_sleepingSystem.TryWaking(args.BuckledEntity);
RemComp<HealOnBuckleHealingComponent>(uid);

View File

@@ -17,5 +17,7 @@ namespace Content.Server.Bed.Components
public float SleepMultiplier = 3f;
public TimeSpan NextHealTime = TimeSpan.Zero; //Next heal
[DataField("sleepAction")] public EntityUid? SleepAction;
}
}

View File

@@ -1,7 +1,6 @@
using Content.Server.Actions;
using Content.Server.Popups;
using Content.Server.Sound.Components;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Audio;
using Content.Shared.Bed.Sleep;
using Content.Shared.Damage;
@@ -10,16 +9,14 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.StatusEffect;
using Content.Shared.Slippery;
using Content.Shared.StatusEffect;
using Content.Shared.Stunnable;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Content.Shared.Interaction.Components;
namespace Content.Server.Bed.Sleep
{
@@ -33,6 +30,8 @@ namespace Content.Server.Bed.Sleep
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
[ValidatePrototypeId<EntityPrototype>] public const string SleepActionId = "ActionSleep";
public override void Initialize()
{
base.Initialize();
@@ -53,7 +52,6 @@ namespace Content.Server.Bed.Sleep
/// </summary>
private void OnSleepStateChanged(EntityUid uid, MobStateComponent component, SleepStateChangedEvent args)
{
_prototypeManager.TryIndex<InstantActionPrototype>("Wake", out var wakeAction);
if (args.FellAsleep)
{
// Expiring status effects would remove the components needed for sleeping
@@ -72,16 +70,8 @@ namespace Content.Server.Bed.Sleep
emitSound.PopUp = sleepSound.PopUp;
}
if (wakeAction != null)
{
var wakeInstance = new InstantAction(wakeAction);
wakeInstance.Cooldown = (_gameTiming.CurTime, _gameTiming.CurTime + TimeSpan.FromSeconds(15));
_actionsSystem.AddAction(uid, wakeInstance, null);
}
return;
}
if (wakeAction != null)
_actionsSystem.RemoveAction(uid, wakeAction);
RemComp<StunnedComponent>(uid);
RemComp<KnockedDownComponent>(uid);
@@ -194,8 +184,7 @@ namespace Content.Server.Bed.Sleep
RaiseLocalEvent(uid, ref tryingToSleepEvent);
if (tryingToSleepEvent.Cancelled) return false;
if (_prototypeManager.TryIndex<InstantActionPrototype>("Sleep", out var sleepAction))
_actionsSystem.RemoveAction(uid, sleepAction);
_actionsSystem.RemoveAction(uid, SleepActionId);
EnsureComp<SleepingComponent>(uid);
return true;

View File

@@ -4,6 +4,7 @@ using Content.Server.Ghost.Roles.Events;
using Content.Server.Popups;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions;
using Content.Shared.Bible;
using Content.Shared.Damage;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
@@ -179,8 +180,9 @@ namespace Content.Server.Bible
if (component.AlreadySummoned)
return;
args.Actions.Add(component.SummonAction);
args.AddAction(ref component.SummonActionEntity, component.SummonAction);
}
private void OnSummon(EntityUid uid, SummonableComponent component, SummonActionEvent args)
{
AttemptSummon(component, args.Performer, Transform(args.Performer));
@@ -238,12 +240,7 @@ namespace Content.Server.Bible
Transform(familiar).AttachParent(component.Owner);
}
component.AlreadySummoned = true;
_actionsSystem.RemoveAction(user, component.SummonAction);
_actionsSystem.RemoveAction(user, component.SummonActionEntity);
}
}
public sealed partial class SummonActionEvent : InstantActionEvent
{
}
}

View File

@@ -1,7 +1,5 @@
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
namespace Content.Server.Bible.Components
{
@@ -18,7 +16,7 @@ namespace Content.Server.Bible.Components
public string? SpecialItemPrototype = null;
public bool AlreadySummoned = false;
[DataField("requriesBibleUser")]
[DataField("requiresBibleUser")]
public bool RequiresBibleUser = true;
/// <summary>
@@ -27,14 +25,11 @@ namespace Content.Server.Bible.Components
[ViewVariables]
public EntityUid? Summon = null;
[DataField("summonAction")]
public InstantAction SummonAction = new()
{
Icon = new SpriteSpecifier.Texture(new ("Clothing/Head/Hats/witch.rsi/icon.png")),
DisplayName = "bible-summon-verb",
Description = "bible-summon-verb-desc",
Event = new SummonActionEvent(),
};
[DataField("summonAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string SummonAction = "ActionBibleSummon";
[DataField("summonActionEntity")]
public EntityUid? SummonActionEntity;
/// Used for respawning
[DataField("accumulator")]

View File

@@ -1,5 +1,5 @@
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Clothing.Components
{
@@ -7,14 +7,15 @@ namespace Content.Server.Clothing.Components
[RegisterComponent]
public sealed partial class MaskComponent : Component
{
[DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ToggleAction = "ActionToggleMask";
/// <summary>
/// This mask can be toggled (pulled up/down)
/// </summary>
[DataField("toggleAction")]
public InstantAction? ToggleAction = null;
[DataField("toggleActionEntity")]
public EntityUid? ToggleActionEntity;
public bool IsToggled = false;
}
public sealed partial class ToggleMaskEvent : InstantActionEvent { }
}

View File

@@ -9,6 +9,7 @@ using Content.Server.Nutrition.EntitySystems;
using Content.Server.Popups;
using Content.Server.VoiceMask;
using Content.Shared.Actions;
using Content.Shared.Clothing;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.IdentityManagement.Components;
@@ -38,20 +39,20 @@ namespace Content.Server.Clothing
private void OnGetActions(EntityUid uid, MaskComponent component, GetItemActionsEvent args)
{
if (component.ToggleAction != null && !args.InHands)
args.Actions.Add(component.ToggleAction);
if (!args.InHands)
args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
}
private void OnToggleMask(EntityUid uid, MaskComponent mask, ToggleMaskEvent args)
{
if (mask.ToggleAction == null)
if (mask.ToggleActionEntity == null)
return;
if (!_inventorySystem.TryGetSlotEntity(args.Performer, "mask", out var existing) || !mask.Owner.Equals(existing))
return;
mask.IsToggled ^= true;
_actionSystem.SetToggled(mask.ToggleAction, mask.IsToggled);
_actionSystem.SetToggled(mask.ToggleActionEntity, mask.IsToggled);
// Pulling mask down can change identity, so we want to update that
_identity.QueueIdentityUpdate(args.Performer);
@@ -67,11 +68,11 @@ namespace Content.Server.Clothing
// set to untoggled when unequipped, so it isn't left in a 'pulled down' state
private void OnGotUnequipped(EntityUid uid, MaskComponent mask, GotUnequippedEvent args)
{
if (mask.ToggleAction == null)
if (mask.ToggleActionEntity == null)
return;
mask.IsToggled = false;
_actionSystem.SetToggled(mask.ToggleAction, mask.IsToggled);
_actionSystem.SetToggled(mask.ToggleActionEntity, mask.IsToggled);
ToggleMaskComponents(uid, mask, args.Equipee, true);
}

View File

@@ -1,10 +1,8 @@
using Content.Shared.Devour;
using Content.Server.Body.Systems;
using Content.Shared.Humanoid;
using Content.Shared.Chemistry.Components;
using Content.Server.Devour.Components;
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
using Content.Shared.Devour;
using Content.Shared.Devour.Components;
using Content.Shared.Humanoid;
namespace Content.Server.Devour;

View File

@@ -1,10 +1,4 @@
using System.Threading;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -42,11 +36,14 @@ namespace Content.Server.Dragon
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("maxAccumulator")] public float RiftMaxAccumulator = 300f;
[DataField("spawnRiftAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string SpawnRiftAction = "ActionSpawnRift";
/// <summary>
/// Spawns a rift which can summon more mobs.
/// </summary>
[DataField("spawnRiftAction")]
public InstantAction? SpawnRiftAction;
[DataField("spawnRiftActionEntity")]
public EntityUid? SpawnRiftActionEntity;
[ViewVariables(VVAccess.ReadWrite), DataField("riftPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string RiftPrototype = "CarpRift";
@@ -61,8 +58,4 @@ namespace Content.Server.Dragon
Params = AudioParams.Default.WithVolume(3f),
};
}
public sealed partial class DragonDevourActionEvent : EntityTargetActionEvent {}
public sealed partial class DragonSpawnRiftActionEvent : InstantActionEvent {}
}

View File

@@ -1,28 +1,20 @@
using Content.Server.Body.Systems;
using System.Numerics;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking;
using Content.Server.NPC;
using Content.Server.NPC.Systems;
using Content.Server.Popups;
using Content.Server.Station.Systems;
using Content.Shared.Actions;
using Content.Shared.Chemistry.Components;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Dragon;
using Content.Shared.Examine;
using Content.Shared.Humanoid;
using Content.Shared.Maps;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Content.Shared.Sprite;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Random;
using System.Numerics;
using Content.Shared.Sprite;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager;
namespace Content.Server.Dragon;
@@ -56,6 +48,7 @@ public sealed partial class DragonSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<DragonComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<DragonComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<DragonComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<DragonComponent, DragonSpawnRiftActionEvent>(OnDragonRift);
SubscribeLocalEvent<DragonComponent, RefreshMovementSpeedModifiersEvent>(OnDragonMove);
@@ -302,10 +295,12 @@ public sealed partial class DragonSystem : EntitySystem
private void OnStartup(EntityUid uid, DragonComponent component, ComponentStartup args)
{
if (component.SpawnRiftAction != null)
_actionsSystem.AddAction(uid, component.SpawnRiftAction, null);
Roar(component);
}
private void OnMapInit(EntityUid uid, DragonComponent component, MapInitEvent args)
{
_actionsSystem.AddAction(uid, ref component.SpawnRiftActionEntity, component.SpawnRiftAction);
}
}

View File

@@ -1,6 +1,6 @@
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -94,6 +94,6 @@ public sealed partial class ZombieRuleComponent : Component
[DataField("shuttleCalled")]
public bool ShuttleCalled;
[ValidatePrototypeId<InstantActionPrototype>]
public const string ZombifySelfActionPrototype = "TurnUndead";
[ValidatePrototypeId<EntityPrototype>]
public const string ZombifySelfActionPrototype = "ActionTurnUndead";
}

View File

@@ -11,7 +11,6 @@ using Content.Server.RoundEnd;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.Zombies;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.CCVar;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
@@ -195,9 +194,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
private void OnZombifySelf(EntityUid uid, ZombifyOnDeathComponent component, ZombifySelfActionEvent args)
{
_zombie.ZombifyEntity(uid);
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
_action.RemoveAction(uid, action);
_action.RemoveAction(uid, ZombieRuleComponent.ZombifySelfActionPrototype);
}
private float GetInfectedFraction(bool includeOffStation = true, bool includeDead = false)
@@ -325,7 +322,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
EnsureComp<ZombifyOnDeathComponent>(ownedEntity);
EnsureComp<IncurableZombieComponent>(ownedEntity);
var inCharacterName = MetaData(ownedEntity).EntityName;
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
var action = Spawn(ZombieRuleComponent.ZombifySelfActionPrototype);
_action.AddAction(mind.OwnedEntity.Value, action, null);
var message = Loc.GetString("zombie-patientzero-role-greeting");

View File

@@ -16,7 +16,6 @@ using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Events;
using Content.Shared.Roles.Jobs;
using Content.Shared.Storage.Components;
using Robust.Server.GameObjects;
using Robust.Server.Player;
@@ -48,6 +47,7 @@ namespace Content.Server.Ghost
SubscribeLocalEvent<GhostComponent, ComponentStartup>(OnGhostStartup);
SubscribeLocalEvent<GhostComponent, ComponentShutdown>(OnGhostShutdown);
SubscribeLocalEvent<GhostComponent, MapInitEvent>(OnGhostMapInit);
SubscribeLocalEvent<GhostComponent, ExaminedEvent>(OnGhostExamine);
@@ -121,13 +121,7 @@ namespace Content.Server.Ghost
eye.VisibilityMask |= (uint) VisibilityFlags.Ghost;
}
var time = _gameTiming.CurTime;
component.TimeOfDeath = time;
// TODO ghost: remove once ghosts are persistent and aren't deleted when returning to body
if (component.Action.UseDelay != null)
component.Action.Cooldown = (time, time + component.Action.UseDelay.Value);
_actions.AddAction(uid, component.Action, null);
component.TimeOfDeath = _gameTiming.CurTime;
}
private void OnGhostShutdown(EntityUid uid, GhostComponent component, ComponentShutdown args)
@@ -149,10 +143,26 @@ namespace Content.Server.Ghost
eye.VisibilityMask &= ~(uint) VisibilityFlags.Ghost;
}
_actions.RemoveAction(uid, component.Action);
_actions.RemoveAction(uid, component.ActionEntity);
}
}
private void OnGhostMapInit(EntityUid uid, GhostComponent component, MapInitEvent args)
{
// TODO ghost: remove once ghosts are persistent and aren't deleted when returning to body
var time = _gameTiming.CurTime;
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 OnGhostExamine(EntityUid uid, GhostComponent component, ExaminedEvent args)
{
var timeSinceDeath = _gameTiming.RealTime.Subtract(component.TimeOfDeath);

View File

@@ -1,7 +1,6 @@
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.Containers;
using Robust.Shared.Utility;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Guardian
{
@@ -24,17 +23,9 @@ namespace Content.Server.Guardian
/// </summary>
[ViewVariables] public ContainerSlot GuardianContainer = default!;
[DataField("action")]
public InstantAction Action = new()
{
DisplayName = "action-name-guardian",
Description = "action-description-guardian",
Icon = new SpriteSpecifier.Texture(new ("Interface/Actions/manifest.png")),
UseDelay = TimeSpan.FromSeconds(2),
CheckCanInteract = false, // allow use while stunned, etc. Gets removed on death anyways.
Event = new GuardianToggleActionEvent(),
};
}
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Action = "ActionToggleGuardian";
public sealed partial class GuardianToggleActionEvent : InstantActionEvent { };
[DataField("actionEntity")] public EntityUid? ActionEntity;
}
}

View File

@@ -1,12 +1,12 @@
using Content.Server.Inventory;
using Content.Server.Popups;
using Content.Server.Body.Systems;
using Content.Server.Popups;
using Content.Shared.Actions;
using Content.Shared.Audio;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Guardian;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
@@ -16,7 +16,6 @@ using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using Content.Shared.Hands.Components;
namespace Content.Server.Guardian
{
@@ -47,6 +46,7 @@ namespace Content.Server.Guardian
SubscribeLocalEvent<GuardianComponent, PlayerDetachedEvent>(OnGuardianUnplayer);
SubscribeLocalEvent<GuardianHostComponent, ComponentInit>(OnHostInit);
SubscribeLocalEvent<GuardianHostComponent, MapInitEvent>(OnHostMapInit);
SubscribeLocalEvent<GuardianHostComponent, MoveEvent>(OnHostMove);
SubscribeLocalEvent<GuardianHostComponent, MobStateChangedEvent>(OnHostStateChange);
SubscribeLocalEvent<GuardianHostComponent, ComponentShutdown>(OnHostShutdown);
@@ -90,7 +90,11 @@ namespace Content.Server.Guardian
private void OnHostInit(EntityUid uid, GuardianHostComponent component, ComponentInit args)
{
component.GuardianContainer = uid.EnsureContainer<ContainerSlot>("GuardianContainer");
_actionSystem.AddAction(uid, component.Action, null);
}
private void OnHostMapInit(EntityUid uid, GuardianHostComponent component, MapInitEvent args)
{
_actionSystem.AddAction(uid, ref component.ActionEntity, component.Action);
}
private void OnHostShutdown(EntityUid uid, GuardianHostComponent component, ComponentShutdown args)
@@ -102,7 +106,7 @@ namespace Content.Server.Guardian
_bodySystem.GibBody(component.HostedGuardian.Value);
EntityManager.QueueDeleteEntity(component.HostedGuardian.Value);
_actionSystem.RemoveAction(uid, component.Action);
_actionSystem.RemoveAction(uid, component.ActionEntity);
}
private void OnGuardianAttackAttempt(EntityUid uid, GuardianComponent component, AttackAttemptEvent args)

View File

@@ -2,7 +2,6 @@ using Content.Server.Actions;
using Content.Server.Popups;
using Content.Server.PowerCell;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Light;
@@ -12,11 +11,8 @@ using Content.Shared.Toggleable;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Light.EntitySystems
@@ -27,7 +23,6 @@ namespace Content.Server.Light.EntitySystems
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
@@ -77,14 +72,7 @@ namespace Content.Server.Light.EntitySystems
private void OnGetActions(EntityUid uid, HandheldLightComponent component, GetItemActionsEvent args)
{
if (component.ToggleAction == null
&& _proto.TryIndex(component.ToggleActionId, out InstantActionPrototype? act))
{
component.ToggleAction = new(act);
}
if (component.ToggleAction != null)
args.Actions.Add(component.ToggleAction);
args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
}
private void OnToggleAction(EntityUid uid, HandheldLightComponent component, ToggleActionEvent args)
@@ -107,20 +95,12 @@ namespace Content.Server.Light.EntitySystems
private void OnMapInit(EntityUid uid, HandheldLightComponent component, MapInitEvent args)
{
if (component.ToggleAction == null
&& _proto.TryIndex(component.ToggleActionId, out InstantActionPrototype? act))
{
component.ToggleAction = new(act);
}
if (component.ToggleAction != null)
_actions.AddAction(uid, component.ToggleAction, null);
_actions.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction);
}
private void OnShutdown(EntityUid uid, HandheldLightComponent component, ComponentShutdown args)
{
if (component.ToggleAction != null)
_actions.RemoveAction(uid, component.ToggleAction);
_actions.RemoveAction(uid, component.ToggleActionEntity);
}
private byte? GetLevel(EntityUid uid, HandheldLightComponent component)

View File

@@ -45,7 +45,7 @@ namespace Content.Server.Light.EntitySystems
private void OnGetActions(EntityUid uid, UnpoweredFlashlightComponent component, GetItemActionsEvent args)
{
args.Actions.Add(component.ToggleAction);
args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
}
private void AddToggleLightVerbs(EntityUid uid, UnpoweredFlashlightComponent component, GetVerbsEvent<ActivationVerb> args)
@@ -66,7 +66,7 @@ namespace Content.Server.Light.EntitySystems
private void OnMindAdded(EntityUid uid, UnpoweredFlashlightComponent component, MindAddedMessage args)
{
_actionsSystem.AddAction(uid, component.ToggleAction, null);
_actionsSystem.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction);
}
private void OnGotEmagged(EntityUid uid, UnpoweredFlashlightComponent component, ref GotEmaggedEvent args)
@@ -97,7 +97,7 @@ namespace Content.Server.Light.EntitySystems
_audioSystem.PlayPvs(flashlight.ToggleSound, uid);
RaiseLocalEvent(uid, new LightToggleEvent(flashlight.LightOn), true);
_actionsSystem.SetToggled(flashlight.ToggleAction, flashlight.LightOn);
_actionsSystem.SetToggled(flashlight.ToggleActionEntity, flashlight.LightOn);
}
}
}

View File

@@ -1,4 +1,4 @@
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Server.Magic.Components;
@@ -13,22 +13,14 @@ public sealed partial class SpellbookComponent : Component
/// List of spells that this book has. This is a combination of the WorldSpells, EntitySpells, and InstantSpells.
/// </summary>
[ViewVariables]
public readonly List<ActionType> Spells = new();
public readonly List<EntityUid> Spells = new();
/// <summary>
/// The three fields below is just used for initialization.
/// </summary>
[DataField("worldSpells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, WorldTargetActionPrototype>))]
[DataField("spells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, EntityPrototype>))]
[ViewVariables(VVAccess.ReadWrite)]
public Dictionary<string, int> WorldSpells = new();
[DataField("entitySpells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, EntityTargetActionPrototype>))]
[ViewVariables(VVAccess.ReadWrite)]
public Dictionary<string, int> EntitySpells = new();
[DataField("instantSpells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, InstantActionPrototype>))]
[ViewVariables(VVAccess.ReadWrite)]
public Dictionary<string, int> InstantSpells = new();
public Dictionary<string, int> SpellActions = new();
[DataField("learnTime")]
[ViewVariables(VVAccess.ReadWrite)]

View File

@@ -4,10 +4,8 @@ using Content.Server.Body.Systems;
using Content.Server.Chat.Systems;
using Content.Server.Doors.Systems;
using Content.Server.Magic.Components;
using Content.Server.Magic.Events;
using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Body.Components;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.DoAfter;
@@ -15,6 +13,7 @@ using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Interaction.Events;
using Content.Shared.Magic;
using Content.Shared.Magic.Events;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Spawners.Components;
@@ -22,11 +21,8 @@ using Content.Shared.Storage;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Exceptions;
namespace Content.Server.Magic;
@@ -38,7 +34,6 @@ public sealed class MagicSystem : EntitySystem
[Dependency] private readonly ISerializationManager _seriMan = default!;
[Dependency] private readonly IComponentFactory _compFact = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly DoorBoltSystem _boltsSystem = default!;
[Dependency] private readonly BodySystem _bodySystem = default!;
@@ -81,23 +76,9 @@ public sealed class MagicSystem : EntitySystem
private void OnInit(EntityUid uid, SpellbookComponent component, ComponentInit args)
{
//Negative charges means the spell can be used without it running out.
foreach (var (id, charges) in component.WorldSpells)
foreach (var (id, charges) in component.SpellActions)
{
var spell = new WorldTargetAction(_prototypeManager.Index<WorldTargetActionPrototype>(id));
_actionsSystem.SetCharges(spell, charges < 0 ? null : charges);
component.Spells.Add(spell);
}
foreach (var (id, charges) in component.InstantSpells)
{
var spell = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(id));
_actionsSystem.SetCharges(spell, charges < 0 ? null : charges);
component.Spells.Add(spell);
}
foreach (var (id, charges) in component.EntitySpells)
{
var spell = new EntityTargetAction(_prototypeManager.Index<EntityTargetActionPrototype>(id));
var spell = Spawn(id);
_actionsSystem.SetCharges(spell, charges < 0 ? null : charges);
component.Spells.Add(spell);
}

View File

@@ -1,8 +1,7 @@
using System.Threading;
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.Utility;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Medical.Components
namespace Content.Server.Medical.Stethoscope.Components
{
/// <summary>
/// Adds an innate verb when equipped to use a stethoscope.
@@ -15,12 +14,9 @@ namespace Content.Server.Medical.Components
[DataField("delay")]
public float Delay = 2.5f;
public EntityTargetAction Action = new()
{
Icon = new SpriteSpecifier.Texture(new ("Clothing/Neck/Misc/stethoscope.rsi/icon.png")),
DisplayName = "stethoscope-verb",
Priority = -1,
Event = new StethoscopeActionEvent(),
};
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Action = "ActionStethoscope";
[DataField("actionEntity")] public EntityUid? ActionEntity;
}
}

View File

@@ -1,19 +1,21 @@
using Content.Server.Body.Components;
using Content.Server.Medical.Components;
using Content.Server.Medical.Stethoscope.Components;
using Content.Server.Popups;
using Content.Shared.Actions;
using Content.Shared.Clothing.Components;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory.Events;
using Content.Shared.Verbs;
using Content.Shared.Medical;
using Content.Shared.Medical.Stethoscope;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Medical;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Server.Medical
namespace Content.Server.Medical.Stethoscope
{
public sealed class StethoscopeSystem : EntitySystem
{
@@ -97,7 +99,7 @@ namespace Content.Server.Medical
private void OnGetActions(EntityUid uid, StethoscopeComponent component, GetItemActionsEvent args)
{
args.Actions.Add(component.Action);
args.AddAction(ref component.ActionEntity, component.Action);
}
// construct the doafter and start it
@@ -156,6 +158,4 @@ namespace Content.Server.Medical
return msg;
}
}
public sealed partial class StethoscopeActionEvent : EntityTargetActionEvent {}
}

View File

@@ -2,7 +2,7 @@
using Content.Server.Chat.Systems;
using Content.Server.Popups;
using Content.Server.Speech.Muting;
using Content.Shared.Actions;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Robust.Server.Console;
@@ -82,24 +82,3 @@ public sealed class CritMobActionsSystem : EntitySystem
args.Handled = true;
}
}
/// <summary>
/// Only applies to mobs in crit capable of ghosting/succumbing
/// </summary>
public sealed partial class CritSuccumbEvent : InstantActionEvent
{
}
/// <summary>
/// Only applies/has functionality to mobs in crit that have <see cref="DeathgaspComponent"/>
/// </summary>
public sealed partial class CritFakeDeathEvent : InstantActionEvent
{
}
/// <summary>
/// Only applies to mobs capable of speaking, as a last resort in crit
/// </summary>
public sealed partial class CritLastWordsEvent : InstantActionEvent
{
}

View File

@@ -1,4 +1,3 @@
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Polymorph;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
@@ -11,7 +10,7 @@ namespace Content.Server.Polymorph.Components
/// A list of all the polymorphs that the entity has.
/// Used to manage them and remove them if needed.
/// </summary>
public Dictionary<string, InstantAction>? PolymorphActions = null;
public Dictionary<string, EntityUid>? PolymorphActions = null;
/// <summary>
/// The polymorphs that the entity starts out being able to do.

View File

@@ -5,7 +5,6 @@ using Content.Server.Mind.Commands;
using Content.Server.Nutrition;
using Content.Server.Polymorph.Components;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Buckle;
using Content.Shared.Damage;
using Content.Shared.Hands.EntitySystems;
@@ -46,6 +45,8 @@ namespace Content.Server.Polymorph.Systems
private ISawmill _sawmill = default!;
private const string RevertPolymorphId = "ActionRevertPolymorph";
public override void Initialize()
{
base.Initialize();
@@ -97,23 +98,21 @@ namespace Content.Server.Polymorph.Systems
if (proto.Forced)
return;
var act = new InstantAction
var actionId = Spawn(RevertPolymorphId);
if (_actions.TryGetActionData(actionId, out var action))
{
Event = new RevertPolymorphActionEvent(),
EntityIcon = component.Parent,
DisplayName = Loc.GetString("polymorph-revert-action-name"),
Description = Loc.GetString("polymorph-revert-action-description"),
UseDelay = TimeSpan.FromSeconds(proto.Delay),
};
action.EntityIcon = component.Parent;
action.UseDelay = TimeSpan.FromSeconds(proto.Delay);
}
_actions.AddAction(uid, act, null);
_actions.AddAction(uid, actionId, null, null, action);
}
private void OnBeforeFullyEaten(EntityUid uid, PolymorphedEntityComponent comp, BeforeFullyEatenEvent args)
{
if (!_proto.TryIndex<PolymorphPrototype>(comp.Prototype, out var proto))
{
_sawmill.Error("Invalid polymorph prototype {comp.Prototype}");
_sawmill.Error($"Invalid polymorph prototype {comp.Prototype}");
return;
}
@@ -334,23 +333,19 @@ namespace Content.Server.Polymorph.Systems
return;
var entproto = _proto.Index<EntityPrototype>(polyproto.Entity);
var act = new InstantAction
var actionId = Spawn(RevertPolymorphId);
if (_actions.TryGetActionData(actionId, out var baseAction) &&
baseAction is InstantActionComponent action)
{
Event = new PolymorphActionEvent
{
Prototype = polyproto,
},
DisplayName = Loc.GetString("polymorph-self-action-name", ("target", entproto.Name)),
Description = Loc.GetString("polymorph-self-action-description", ("target", entproto.Name)),
Icon = new SpriteSpecifier.EntityPrototype(polyproto.Entity),
ItemIconStyle = ItemActionIconStyle.NoItem,
};
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();
polycomp.PolymorphActions.Add(id, act);
_actions.AddAction(target, act, target);
polycomp.PolymorphActions ??= new Dictionary<string, EntityUid>();
polycomp.PolymorphActions.Add(id, actionId);
_actions.AddAction(target, actionId, target);
}
}
[PublicAPI]
@@ -399,18 +394,4 @@ namespace Content.Server.Polymorph.Systems
UpdateCollide();
}
}
public sealed partial class PolymorphActionEvent : InstantActionEvent
{
/// <summary>
/// The polymorph prototype containing all the information about
/// the specific polymorph.
/// </summary>
public PolymorphPrototype Prototype = default!;
}
public sealed partial class RevertPolymorphActionEvent : InstantActionEvent
{
}
}

View File

@@ -1,4 +1,3 @@
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -7,11 +6,13 @@ namespace Content.Server.RatKing
[RegisterComponent]
public sealed partial class RatKingComponent : Component
{
[DataField("actionRaiseArmy", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ActionRaiseArmy = "ActionRatKingRaiseArmy";
/// <summary>
/// The action for the Raise Army ability
/// </summary>
[DataField("actionRaiseArmy", required: true)]
public InstantAction ActionRaiseArmy = new();
[DataField("actionRaiseArmyEntity")] public EntityUid? ActionRaiseArmyEntity;
/// <summary>
/// The amount of hunger one use of Raise Army consumes
@@ -25,11 +26,14 @@ namespace Content.Server.RatKing
[ViewVariables(VVAccess.ReadWrite), DataField("armyMobSpawnId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ArmyMobSpawnId = "MobRatServant";
[DataField("actionDomain", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ActionDomain = "ActionRatKingDomain";
/// <summary>
/// The action for the Domain ability
/// </summary>
[DataField("actionDomain", required: true)]
public InstantAction ActionDomain = new();
[DataField("actionDomainEntity")]
public EntityUid? ActionDomainEntity;
/// <summary>
/// The amount of hunger one use of Domain consumes
@@ -43,4 +47,4 @@ namespace Content.Server.RatKing
[DataField("molesMiasmaPerDomain")]
public float MolesMiasmaPerDomain = 100f;
}
};
}

View File

@@ -1,13 +1,11 @@
using Content.Server.Actions;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Shared.Actions;
using Content.Shared.Atmos;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.RatKing;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
namespace Content.Server.RatKing
{
@@ -23,16 +21,16 @@ namespace Content.Server.RatKing
{
base.Initialize();
SubscribeLocalEvent<RatKingComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<RatKingComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RatKingComponent, RatKingRaiseArmyActionEvent>(OnRaiseArmy);
SubscribeLocalEvent<RatKingComponent, RatKingDomainActionEvent>(OnDomain);
}
private void OnStartup(EntityUid uid, RatKingComponent component, ComponentStartup args)
private void OnMapInit(EntityUid uid, RatKingComponent component, MapInitEvent args)
{
_action.AddAction(uid, component.ActionRaiseArmy, null);
_action.AddAction(uid, component.ActionDomain, null);
_action.AddAction(uid, ref component.ActionRaiseArmyEntity, component.ActionRaiseArmy);
_action.AddAction(uid, ref component.ActionDomainEntity, component.ActionDomain);
}
/// <summary>
@@ -86,14 +84,4 @@ namespace Content.Server.RatKing
tileMix?.AdjustMoles(Gas.Miasma, component.MolesMiasmaPerDomain);
}
}
public sealed partial class RatKingRaiseArmyActionEvent : InstantActionEvent
{
}
public sealed partial class RatKingDomainActionEvent : InstantActionEvent
{
}
};

View File

@@ -1,35 +1,33 @@
using System.Numerics;
using Content.Server.Actions;
using Content.Shared.Popups;
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.Interaction;
using Content.Server.GameTicking;
using Content.Shared.Stunnable;
using Content.Shared.Revenant;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
using Content.Shared.StatusEffect;
using Content.Server.Visible;
using Content.Shared.Examine;
using Robust.Shared.Prototypes;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Tag;
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
using Content.Server.Visible;
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Maps;
using Content.Shared.Mobs.Systems;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Revenant;
using Content.Shared.Revenant.Components;
using Content.Shared.StatusEffect;
using Content.Shared.Stunnable;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Revenant.EntitySystems;
public sealed partial class RevenantSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ActionsSystem _action = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly DamageableSystem _damage = default!;
@@ -46,11 +44,15 @@ public sealed partial class RevenantSystem : EntitySystem
[Dependency] private readonly VisibilitySystem _visibility = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string RevenantShopId = "ActionRevenantShop";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RevenantComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<RevenantComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RevenantComponent, RevenantShopActionEvent>(OnShop);
SubscribeLocalEvent<RevenantComponent, DamageChangedEvent>(OnDamage);
@@ -82,9 +84,11 @@ public sealed partial class RevenantSystem : EntitySystem
//ghost vision
if (TryComp(uid, out EyeComponent? eye))
eye.VisibilityMask |= (uint) (VisibilityFlags.Ghost);
}
var shopaction = new InstantAction(_proto.Index<InstantActionPrototype>("RevenantShop"));
_action.AddAction(uid, shopaction, null);
private void OnMapInit(EntityUid uid, RevenantComponent component, MapInitEvent args)
{
_action.AddAction(uid, Spawn(RevenantShopId), null);
}
private void OnStatusAdded(EntityUid uid, RevenantComponent component, StatusEffectAddedEvent args)

View File

@@ -1,9 +1,11 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Sericulture;
[RegisterComponent]
public sealed partial class SericultureComponent : Component
{
[DataField("popupText")]
public string PopupText = "sericulture-failure-hunger";
@@ -13,8 +15,10 @@ public sealed partial class SericultureComponent : Component
[DataField("entityProduced", required: true)]
public string EntityProduced = "";
[DataField("actionProto", required: true)]
public string ActionProto = "";
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Action = "ActionSericulture";
[DataField("actionEntity")] public EntityUid? ActionEntity;
/// <summary>
/// How long will it take to make.

View File

@@ -1,14 +1,11 @@
using Content.Server.Actions;
using Content.Server.DoAfter;
using Content.Server.Popups;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Server.Stack;
using Content.Shared.DoAfter;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.Prototypes;
using Content.Shared.Sericulture;
using Content.Server.Stack;
namespace Content.Server.Sericulture;
@@ -16,7 +13,6 @@ public sealed partial class SericultureSystem : EntitySystem
{
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly HungerSystem _hungerSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly StackSystem _stackSystem = default!;
@@ -25,26 +21,20 @@ public sealed partial class SericultureSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<SericultureComponent, ComponentInit>(OnCompInit);
SubscribeLocalEvent<SericultureComponent, MapInitEvent>(OnCompMapInit);
SubscribeLocalEvent<SericultureComponent, ComponentShutdown>(OnCompRemove);
SubscribeLocalEvent<SericultureComponent, SericultureActionEvent>(OnSericultureStart);
SubscribeLocalEvent<SericultureComponent, SericultureDoAfterEvent>(OnSericultureDoAfter);
}
private void OnCompInit(EntityUid uid, SericultureComponent comp, ComponentInit args)
private void OnCompMapInit(EntityUid uid, SericultureComponent comp, MapInitEvent args)
{
if (!_protoManager.TryIndex<InstantActionPrototype>(comp.ActionProto, out var actionProto))
return;
_actionsSystem.AddAction(uid, new InstantAction(actionProto), uid);
_actionsSystem.AddAction(uid, ref comp.ActionEntity, comp.Action, uid);
}
private void OnCompRemove(EntityUid uid, SericultureComponent comp, ComponentShutdown args)
{
if (!_protoManager.TryIndex<InstantActionPrototype>(comp.ActionProto, out var actionProto))
return;
_actionsSystem.RemoveAction(uid, new InstantAction(actionProto));
_actionsSystem.RemoveAction(uid, comp.ActionEntity);
}
private void OnSericultureStart(EntityUid uid, SericultureComponent comp, SericultureActionEvent args)
@@ -96,6 +86,4 @@ public sealed partial class SericultureSystem : EntitySystem
return false;
}
public sealed partial class SericultureActionEvent : InstantActionEvent { }
}

View File

@@ -57,8 +57,14 @@ public sealed partial class BorgSystem
private void OnSelectableInstalled(EntityUid uid, SelectableBorgModuleComponent component, ref BorgModuleInstalledEvent args)
{
var chassis = args.ChassisEnt;
component.ModuleSwapAction.EntityIcon = uid;
_actions.AddAction(chassis, component.ModuleSwapAction, uid);
var action = _actions.AddAction(chassis, ref component.ModuleSwapActionEntity, component.ModuleSwapActionId, uid);
if (action != null)
{
action.EntityIcon = uid;
Dirty(component.ModuleSwapActionEntity!.Value, action);
}
SelectModule(chassis, uid, moduleComp: component);
}

View File

@@ -5,7 +5,6 @@ using Content.Server.Radio.Components;
using Content.Server.Roles;
using Content.Server.Station.Systems;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Administration;
using Content.Shared.Chat;
using Content.Shared.Emag.Components;
@@ -43,7 +42,6 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
{
base.Initialize();
SubscribeLocalEvent<SiliconLawBoundComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<SiliconLawBoundComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<SiliconLawBoundComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SiliconLawBoundComponent, MindAddedMessage>(OnMindAdded);
@@ -58,20 +56,15 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
SubscribeLocalEvent<EmagSiliconLawComponent, ExaminedEvent>(OnExamined);
}
private void OnComponentStartup(EntityUid uid, SiliconLawBoundComponent component, ComponentStartup args)
{
component.ProvidedAction = new(_prototype.Index<InstantActionPrototype>(component.ViewLawsAction));
_actions.AddAction(uid, component.ProvidedAction, null);
}
private void OnComponentShutdown(EntityUid uid, SiliconLawBoundComponent component, ComponentShutdown args)
{
if (component.ProvidedAction != null)
_actions.RemoveAction(uid, component.ProvidedAction);
if (component.ViewLawsActionEntity != null)
_actions.RemoveAction(uid, component.ViewLawsActionEntity);
}
private void OnMapInit(EntityUid uid, SiliconLawBoundComponent component, MapInitEvent args)
{
_actions.AddAction(uid, ref component.ViewLawsActionEntity, component.ViewLawsAction);
GetLaws(uid, component);
}

View File

@@ -1,10 +1,8 @@
using Content.Server.Humanoid;
using Content.Server.Speech.EntitySystems;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Humanoid;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
@@ -33,11 +31,11 @@ public sealed partial class VocalComponent : Component
[DataField("wilhelmProbability")]
public float WilhelmProbability = 0.0002f;
[DataField("screamActionId", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string ScreamActionId = "Scream";
[DataField("screamAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ScreamAction = "ActionScream";
[DataField("screamAction")]
public InstantAction? ScreamAction;
[DataField("screamActionEntity")]
public EntityUid? ScreamActionEntity;
/// <summary>
/// Currently loaded emote sounds prototype, based on entity sex.
@@ -46,8 +44,3 @@ public sealed partial class VocalComponent : Component
[ViewVariables]
public EmoteSoundsPrototype? EmoteSounds = null;
}
public sealed partial class ScreamActionEvent : InstantActionEvent
{
}

View File

@@ -1,8 +1,8 @@
using Content.Server.Administration.Logs;
using Content.Shared.Actions;
using Content.Shared.Database;
using Content.Shared.Speech.Components;
using Content.Shared.Speech.EntitySystems;
using Content.Shared.Database;
using Robust.Server.GameObjects;
namespace Content.Server.Speech.EntitySystems;
@@ -18,17 +18,15 @@ public sealed class MeleeSpeechSystem : SharedMeleeSpeechSystem
SubscribeLocalEvent<MeleeSpeechComponent, MeleeSpeechBattlecryChangedMessage>(OnBattlecryChanged);
SubscribeLocalEvent<MeleeSpeechComponent, MeleeSpeechConfigureActionEvent>(OnConfigureAction);
SubscribeLocalEvent<MeleeSpeechComponent, GetItemActionsEvent>(OnGetActions);
SubscribeLocalEvent<MeleeSpeechComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<MeleeSpeechComponent, MapInitEvent>(OnComponentMapInit);
}
private void OnComponentInit(EntityUid uid, MeleeSpeechComponent component, ComponentInit args)
private void OnComponentMapInit(EntityUid uid, MeleeSpeechComponent component, MapInitEvent args)
{
if (component.ConfigureAction != null)
_actionSystem.AddAction(uid, component.ConfigureAction, uid);
_actionSystem.AddAction(uid, ref component.ConfigureActionEntity, component.ConfigureAction, uid);
}
private void OnGetActions(EntityUid uid, MeleeSpeechComponent component, GetItemActionsEvent args)
{
if (component.ConfigureAction != null)
args.Actions.Add(component.ConfigureAction);
args.AddAction(ref component.ConfigureActionEntity, component.ConfigureAction);
}
private void OnBattlecryChanged(EntityUid uid, MeleeSpeechComponent comp, MeleeSpeechBattlecryChangedMessage args)
{

View File

@@ -1,10 +1,9 @@
using Content.Server.Actions;
using Content.Server.Chat.Systems;
using Content.Server.Humanoid;
using Content.Server.Speech.Components;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Humanoid;
using Content.Shared.Speech;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -32,21 +31,16 @@ public sealed class VocalSystem : EntitySystem
private void OnMapInit(EntityUid uid, VocalComponent component, MapInitEvent args)
{
// try to add scream action when vocal comp added
if (_proto.TryIndex(component.ScreamActionId, out InstantActionPrototype? proto))
{
component.ScreamAction = new InstantAction(proto);
_actions.AddAction(uid, component.ScreamAction, null);
}
_actions.AddAction(uid, ref component.ScreamActionEntity, component.ScreamAction);
LoadSounds(uid, component);
}
private void OnShutdown(EntityUid uid, VocalComponent component, ComponentShutdown args)
{
// remove scream action when component removed
if (component.ScreamAction != null)
if (component.ScreamActionEntity != null)
{
_actions.RemoveAction(uid, component.ScreamAction);
_actions.RemoveAction(uid, component.ScreamActionEntity);
}
}
@@ -76,7 +70,7 @@ public sealed class VocalSystem : EntitySystem
if (args.Handled)
return;
_chat.TryEmoteWithChat(uid, component.ScreamActionId);
_chat.TryEmoteWithChat(uid, component.ScreamId);
args.Handled = true;
}

View File

@@ -1,16 +1,15 @@
using System.Linq;
using Content.Server.Actions;
using Content.Server.Administration.Logs;
using Content.Server.PDA.Ringer;
using Content.Server.Stack;
using Content.Server.Store.Components;
using Content.Server.UserInterface;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Store;
using Content.Shared.Database;
using Robust.Server.GameObjects;
using System.Linq;
namespace Content.Server.Store.Systems;
@@ -162,10 +161,9 @@ public sealed partial class StoreSystem
}
//give action
if (listing.ProductAction != null)
if (!string.IsNullOrWhiteSpace(listing.ProductAction))
{
var action = new InstantAction(_proto.Index<InstantActionPrototype>(listing.ProductAction));
_actions.AddAction(buyer, action, null);
_actions.AddAction(buyer, Spawn(listing.ProductAction), null);
}
//broadcast event

View File

@@ -5,6 +5,7 @@ using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.UserInterface;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Server.Player;

View File

@@ -1,57 +1,32 @@
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.UserInterface;
[RegisterComponent]
public sealed partial class IntrinsicUIComponent : Component, ISerializationHooks
public sealed partial class IntrinsicUIComponent : Component
{
/// <summary>
/// List of UIs and their actions that this entity has.
/// </summary>
[DataField("uis", required: true)]
public List<IntrinsicUIEntry> UIs = new();
void ISerializationHooks.AfterDeserialization()
{
for (var i = 0; i < UIs.Count; i++)
{
var ui = UIs[i];
ui.AfterDeserialization();
UIs[i] = ui;
}
}
[DataField("uis", required: true)] public List<IntrinsicUIEntry> UIs = new();
}
[DataDefinition]
public partial struct IntrinsicUIEntry
public partial class IntrinsicUIEntry
{
[ViewVariables] public Enum? Key { get; private set; } = null;
/// <summary>
/// The BUI key that this intrinsic UI should open.
/// </summary>
[DataField("key", required: true)]
private string _keyRaw = default!;
public Enum? Key { get; private set; }
[DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>), required: true)]
public string? ToggleAction;
/// <summary>
/// The action used for this BUI.
/// </summary>
[DataField("toggleAction", required: true)]
public InstantAction ToggleAction = new();
public void AfterDeserialization()
{
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
if (reflectionManager.TryParseEnumReference(_keyRaw, out var key))
Key = key;
if (ToggleAction.Event is ToggleIntrinsicUIEvent ev)
{
ev.Key = Key;
}
}
public IntrinsicUIEntry() {}
[DataField("toggleActionEntity")]
public EntityUid? ToggleActionEntity = new();
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Actions;
using Content.Shared.Actions;
using JetBrains.Annotations;
using Content.Shared.UserInterface;
using Robust.Server.GameObjects;
namespace Content.Server.UserInterface;
@@ -28,7 +28,7 @@ public sealed class IntrinsicUISystem : EntitySystem
foreach (var entry in component.UIs)
{
_actionsSystem.AddAction(uid, entry.ToggleAction, null, actions);
_actionsSystem.AddAction(uid, ref entry.ToggleActionEntity, entry.ToggleAction, null, actions);
}
}
@@ -68,13 +68,6 @@ public sealed class IntrinsicUISystem : EntitySystem
}
}
[UsedImplicitly]
public sealed partial class ToggleIntrinsicUIEvent : InstantActionEvent
{
[ViewVariables]
public Enum? Key { get; set; }
}
// Competing with ActivatableUI for horrible event names.
public sealed class IntrinsicUIOpenAttemptEvent : CancellableEntityEventArgs
{

View File

@@ -8,7 +8,6 @@ using Content.Server.UserInterface;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Damage;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
@@ -44,6 +43,7 @@ namespace Content.Server.VendingMachines
base.Initialize();
_sawmill = Logger.GetSawmill("vending");
SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnComponentMapInit);
SubscribeLocalEvent<VendingMachineComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<VendingMachineComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
@@ -62,6 +62,12 @@ namespace Content.Server.VendingMachines
SubscribeLocalEvent<VendingMachineRestockComponent, PriceCalculationEvent>(OnPriceCalculation);
}
private void OnComponentMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args)
{
_action.AddAction(uid, ref component.ActionEntity, component.Action, uid);
Dirty(uid, component);
}
private void OnVendingPrice(EntityUid uid, VendingMachineComponent component, ref PriceCalculationEvent args)
{
var price = 0.0;
@@ -88,12 +94,6 @@ namespace Content.Server.VendingMachines
{
TryUpdateVisualState(uid, component);
}
if (component.Action != null)
{
var action = new InstantAction(PrototypeManager.Index<InstantActionPrototype>(component.Action));
_action.AddAction(uid, action, uid);
}
}
private void OnActivatableUIOpenAttempt(EntityUid uid, VendingMachineComponent component, ActivatableUIOpenAttemptEvent args)

View File

@@ -1,9 +1,6 @@
using Content.Server.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Speech;
using Robust.Shared.Prototypes;
namespace Content.Server.VoiceMask;
@@ -12,7 +9,6 @@ public sealed partial class VoiceMaskSystem
{
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private const string MaskSlot = "mask";
@@ -26,12 +22,7 @@ public sealed partial class VoiceMaskSystem
var comp = EnsureComp<VoiceMaskComponent>(user);
comp.VoiceName = component.LastSetName;
if (!_prototypeManager.TryIndex<InstantActionPrototype>(component.Action, out var action))
{
throw new ArgumentException("Could not get voice masking prototype.");
}
_actions.AddAction(user, (InstantAction) action.Clone(), uid);
_actions.AddAction(user, ref component.ActionEntity, component.Action, uid);
}
private void OnUnequip(EntityUid uid, VoiceMaskerComponent compnent, GotUnequippedEvent args)

View File

@@ -1,7 +1,6 @@
using Content.Server.Administration.Logs;
using Content.Server.Chat.Systems;
using Content.Server.Popups;
using Content.Shared.Actions;
using Content.Shared.Database;
using Content.Shared.Inventory.Events;
using Content.Shared.Preferences;
@@ -88,7 +87,3 @@ public sealed partial class VoiceMaskSystem : EntitySystem
UserInterfaceSystem.SetUiState(bui, new VoiceMaskBuiState(component.VoiceName));
}
}
public sealed partial class VoiceMaskSetNameEvent : InstantActionEvent
{
}

View File

@@ -1,4 +1,4 @@
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.VoiceMask;
@@ -8,6 +8,8 @@ public sealed partial class VoiceMaskerComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)] public string LastSetName = "Unknown";
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string Action = "ChangeVoiceMask";
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Action = "ActionChangeVoiceMask";
[DataField("actionEntity")] public EntityUid? ActionEntity;
}

View File

@@ -1,7 +1,5 @@
using Content.Server.Actions;
using Content.Server.Popups;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Shared.Prototypes;
@@ -12,32 +10,29 @@ public partial class ArtifactSystem
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[ValidatePrototypeId<EntityPrototype>] private const string ArtifactActivateActionId = "ActionArtifactActivate";
/// <summary>
/// Used to add the artifact activation action (hehe), which lets sentient artifacts activate themselves,
/// either through admemery or the sentience effect.
/// </summary>
public void InitializeActions()
{
SubscribeLocalEvent<ArtifactComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ArtifactComponent, MapInitEvent>(OnStartup);
SubscribeLocalEvent<ArtifactComponent, ComponentRemove>(OnRemove);
SubscribeLocalEvent<ArtifactComponent, ArtifactSelfActivateEvent>(OnSelfActivate);
}
private void OnStartup(EntityUid uid, ArtifactComponent component, ComponentStartup args)
private void OnStartup(EntityUid uid, ArtifactComponent component, MapInitEvent args)
{
if (_prototype.TryIndex<InstantActionPrototype>("ArtifactActivate", out var proto))
{
_actions.AddAction(uid, new InstantAction(proto), null);
}
RandomizeArtifact(uid, component);
_actions.AddAction(uid, Spawn(ArtifactActivateActionId), null);
}
private void OnRemove(EntityUid uid, ArtifactComponent component, ComponentRemove args)
{
if (_prototype.TryIndex<InstantActionPrototype>("ArtifactActivate", out var proto))
{
_actions.RemoveAction(uid, new InstantAction(proto));
}
_actions.RemoveAction(uid, ArtifactActivateActionId);
}
private void OnSelfActivate(EntityUid uid, ArtifactComponent component, ArtifactSelfActivateEvent args)
@@ -52,4 +47,3 @@ public partial class ArtifactSystem
args.Handled = true;
}
}

View File

@@ -6,12 +6,12 @@ using Content.Server.Power.EntitySystems;
using Content.Server.Xenoarchaeology.Equipment.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.CCVar;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Configuration;
using Content.Shared.CCVar;
namespace Content.Server.Xenoarchaeology.XenoArtifacts;
@@ -30,7 +30,6 @@ public sealed partial class ArtifactSystem : EntitySystem
_sawmill = Logger.GetSawmill("artifact");
SubscribeLocalEvent<ArtifactComponent, MapInitEvent>(OnInit);
SubscribeLocalEvent<ArtifactComponent, PriceCalculationEvent>(GetPrice);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEnd);
@@ -38,11 +37,6 @@ public sealed partial class ArtifactSystem : EntitySystem
InitializeActions();
}
private void OnInit(EntityUid uid, ArtifactComponent component, MapInitEvent args)
{
RandomizeArtifact(uid, component);
}
/// <summary>
/// Calculates the price of an artifact based on
/// how many nodes have been unlocked/triggered

View File

@@ -1,6 +1,6 @@
using Content.Server.Magic.Events;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Magic.Events;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;

View File

@@ -1,8 +1,8 @@
using Content.Shared.Actions.ActionTypes;
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,7 +18,9 @@ namespace Content.Shared.Actions;
/// </remarks>
public sealed class GetItemActionsEvent : EntityEventArgs
{
public SortedSet<ActionType> Actions = new();
private readonly IEntityManager _entities;
private readonly INetManager _net;
public readonly SortedSet<EntityUid> Actions = new();
/// <summary>
/// User equipping the item.
@@ -35,11 +37,26 @@ public sealed class GetItemActionsEvent : EntityEventArgs
/// </summary>
public bool InHands => SlotFlags == null;
public GetItemActionsEvent(EntityUid user, SlotFlags? slotFlags = null)
public GetItemActionsEvent(IEntityManager entities, INetManager net, EntityUid user, SlotFlags? slotFlags = null)
{
_entities = entities;
_net = net;
User = user;
SlotFlags = slotFlags;
}
public void AddAction(ref EntityUid? actionId, string? prototypeId)
{
if (_entities.Deleted(actionId))
{
if (string.IsNullOrWhiteSpace(prototypeId) || _net.IsClient)
return;
actionId = _entities.Spawn(prototypeId);
}
Actions.Add(actionId.Value);
}
}
/// <summary>
@@ -48,22 +65,22 @@ public sealed class GetItemActionsEvent : EntityEventArgs
[Serializable, NetSerializable]
public sealed class RequestPerformActionEvent : EntityEventArgs
{
public readonly ActionType Action;
public readonly EntityUid Action;
public readonly EntityUid? EntityTarget;
public readonly EntityCoordinates? EntityCoordinatesTarget;
public RequestPerformActionEvent(InstantAction action)
public RequestPerformActionEvent(EntityUid action)
{
Action = action;
}
public RequestPerformActionEvent(EntityTargetAction action, EntityUid entityTarget)
public RequestPerformActionEvent(EntityUid action, EntityUid entityTarget)
{
Action = action;
EntityTarget = entityTarget;
}
public RequestPerformActionEvent(WorldTargetAction action, EntityCoordinates entityCoordinatesTarget)
public RequestPerformActionEvent(EntityUid action, EntityCoordinates entityCoordinatesTarget)
{
Action = action;
EntityCoordinatesTarget = entityCoordinatesTarget;

View File

@@ -1,56 +0,0 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Actions.ActionTypes;
// These are just prototype definitions for actions. Allows actions to be defined once in yaml and re-used elsewhere.
// Note that you still need to create a new instance of each action to properly track the state (cooldown, toggled,
// enabled, etc). The prototypes should not be modified directly.
//
// If ever action states data is separated from the rest of the data, this might not be required
// anymore.
[Prototype("worldTargetAction")]
public sealed partial class WorldTargetActionPrototype : WorldTargetAction, IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
// This is a shitty hack to get around the fact that action-prototypes should not in general be sever-exclusive
// prototypes, but some actions may need to use server-exclusive events, and there is no way to specify on a
// per-prototype basis whether the client should ignore it when validating yaml.
[DataField("serverEvent", serverOnly: true)]
public WorldTargetActionEvent? ServerEvent
{
get => Event;
set => Event = value;
}
}
[Prototype("entityTargetAction")]
public sealed partial class EntityTargetActionPrototype : EntityTargetAction, IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
[DataField("serverEvent", serverOnly: true)]
public EntityTargetActionEvent? ServerEvent
{
get => Event;
set => Event = value;
}
}
[Prototype("instantAction")]
public sealed partial class InstantActionPrototype : InstantAction, IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
[DataField("serverEvent", serverOnly: true)]
public InstantActionEvent? ServerEvent
{
get => Event;
set => Event = value;
}
}

View File

@@ -1,275 +0,0 @@
using Robust.Shared.Audio;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Actions.ActionTypes;
[ImplicitDataDefinitionForInheritors]
[Serializable, NetSerializable]
public abstract partial class ActionType : IEquatable<ActionType>, IComparable, ICloneable
{
/// <summary>
/// Icon representing this action in the UI.
/// </summary>
[DataField("icon")]
public SpriteSpecifier? Icon;
/// <summary>
/// For toggle actions only, icon to show when toggled on. If omitted, the action will simply be highlighted
/// when turned on.
/// </summary>
[DataField("iconOn")]
public SpriteSpecifier? IconOn;
/// <summary>
/// If not null, this color will modulate the action icon color.
/// </summary>
/// <remarks>
/// This currently only exists for decal-placement actions, so that the action icons correspond to the color of
/// the decal. But this is probably useful for other actions, including maybe changing color on toggle.
/// </remarks>
[DataField("iconColor")]
public Color IconColor = Color.White;
/// <summary>
/// Name to show in UI.
/// </summary>
[DataField("name")]
public string DisplayName = string.Empty;
/// <summary>
/// This is just <see cref="DisplayName"/> with localized strings resolved and markup removed. If null, will be
/// inferred from <see cref="DisplayName"/>. This is cached to speed up game state handling.
/// </summary>
[NonSerialized]
public string? RawName;
/// <summary>
/// Description to show in UI. Accepts formatting.
/// </summary>
[DataField("description")]
public string Description = string.Empty;
/// <summary>
/// Keywords that can be used to search for this action in the action menu.
/// </summary>
[DataField("keywords")]
public HashSet<string> Keywords = new();
/// <summary>
/// Whether this action is currently enabled. If not enabled, this action cannot be performed.
/// </summary>
[DataField("enabled")]
public bool Enabled = true;
/// <summary>
/// The toggle state of this action. Toggling switches the currently displayed icon, see <see cref="Icon"/> and <see cref="IconOn"/>.
/// </summary>
/// <remarks>
/// The toggle can set directly via <see cref="SharedActionsSystem.SetToggled()"/>, but it will also be
/// automatically toggled for targeted-actions while selecting a target.
/// </remarks>
public bool Toggled;
/// <summary>
/// The current cooldown on the action.
/// </summary>
public (TimeSpan Start, TimeSpan End)? Cooldown;
/// <summary>
/// Time interval between action uses.
/// </summary>
[DataField("useDelay")]
public TimeSpan? UseDelay;
/// <summary>
/// Convenience tool for actions with limited number of charges. Automatically decremented on use, and the
/// action is disabled when it reaches zero. Does NOT automatically remove the action from the action bar.
/// </summary>
[DataField("charges")]
public int? Charges;
/// <summary>
/// The entity that enables / provides this action. If the action is innate, this may be the user themselves. If
/// this action has no provider (e.g., mapping tools), the this will result in broadcast events.
/// </summary>
public EntityUid? Provider;
/// <summary>
/// Entity to use for the action icon. Defaults to using <see cref="Provider"/>.
/// </summary>
public EntityUid? EntityIcon
{
get => _entityIcon ?? Provider;
set => _entityIcon = value;
}
private EntityUid? _entityIcon;
/// <summary>
/// Whether the action system should block this action if the user cannot currently interact. Some spells or
/// abilities may want to disable this and implement their own checks.
/// </summary>
[DataField("checkCanInteract")]
public bool CheckCanInteract = true;
/// <summary>
/// If true, will simply execute the action locally without sending to the server.
/// </summary>
[DataField("clientExclusive")]
public bool ClientExclusive = false;
/// <summary>
/// Determines the order in which actions are automatically added the action bar.
/// </summary>
[DataField("priority")]
public int Priority = 0;
/// <summary>
/// What entity, if any, currently has this action in the actions component?
/// </summary>
[ViewVariables]
public EntityUid? AttachedEntity;
/// <summary>
/// Whether or not to automatically add this action to the action bar when it becomes available.
/// </summary>
[DataField("autoPopulate")]
public bool AutoPopulate = true;
/// <summary>
/// Whether or not to automatically remove this action to the action bar when it becomes unavailable.
/// </summary>
[DataField("autoRemove")]
public bool AutoRemove = true;
/// <summary>
/// Temporary actions are removed from the action component when removed from the action-bar/GUI. Currently,
/// should only be used for client-exclusive actions (server is not notified).
/// </summary>
/// <remarks>
/// Currently there is no way for a player to just voluntarily remove actions. They can hide them from the
/// toolbar, but not actually remove them. This is undesirable for things like dynamically added mapping
/// entity-selection actions, as the # of actions would just keep increasing.
/// </remarks>
[DataField("temporary")]
public bool Temporary;
// TODO re-add support for this
// UI refactor seems to have just broken it.
/// <summary>
/// Determines the appearance of the entity-icon for actions that are enabled via some entity.
/// </summary>
[DataField("itemIconStyle")]
public ItemActionIconStyle ItemIconStyle;
/// <summary>
/// If not null, this sound will be played when performing this action.
/// </summary>
[DataField("sound")]
public SoundSpecifier? Sound;
/// <summary>
/// Compares two actions based on their properties. This is used to determine equality when the client requests the
/// server to perform some action. Also determines the order in which actions are automatically added to the action bar.
/// </summary>
/// <remarks>
/// Basically: if an action has the same priority, name, and is enabled by the same entity, then the actions are considered equal.
/// The entity-check is required to avoid toggling all flashlights simultaneously whenever a flashlight-hoarder uses an action.
/// </remarks>
public virtual int CompareTo(object? obj)
{
if (obj is not ActionType otherAction)
return -1;
if (Priority != otherAction.Priority)
return otherAction.Priority - Priority;
RawName ??= FormattedMessage.RemoveMarkup(Loc.GetString(DisplayName));
otherAction.RawName ??= FormattedMessage.RemoveMarkup(Loc.GetString(otherAction.DisplayName));
var cmp = string.Compare(RawName, otherAction.RawName, StringComparison.CurrentCulture);
if (cmp != 0)
return cmp;
if (Provider != otherAction.Provider)
{
if (Provider == null)
return -1;
if (otherAction.Provider == null)
return 1;
// uid to int casting... it says "Do NOT use this in content". You can't tell me what to do.
return (int) Provider - (int) otherAction.Provider;
}
return 0;
}
/// <summary>
/// Proper client-side state handling requires the ability to clone an action from the component state.
/// Otherwise modifying the action can lead to modifying the stored server state.
/// </summary>
public abstract object Clone();
public virtual void CopyFrom(object objectToClone)
{
if (objectToClone is not ActionType toClone)
return;
// This is pretty Ugly to look at. But actions are sent to the client in a component state, so they have to be
// cloneable. Would be easy if this were a struct of only value-types, but I don't want to restrict actions like
// that.
Priority = toClone.Priority;
Icon = toClone.Icon;
IconOn = toClone.IconOn;
DisplayName = toClone.DisplayName;
RawName = null;
Description = toClone.Description;
Provider = toClone.Provider;
AttachedEntity = toClone.AttachedEntity;
Enabled = toClone.Enabled;
Toggled = toClone.Toggled;
Cooldown = toClone.Cooldown;
Charges = toClone.Charges;
Keywords = new(toClone.Keywords);
AutoPopulate = toClone.AutoPopulate;
AutoRemove = toClone.AutoRemove;
ItemIconStyle = toClone.ItemIconStyle;
CheckCanInteract = toClone.CheckCanInteract;
UseDelay = toClone.UseDelay;
Sound = toClone.Sound;
ItemIconStyle = toClone.ItemIconStyle;
_entityIcon = toClone._entityIcon;
}
public bool Equals(ActionType? other)
{
return CompareTo(other) == 0;
}
public static bool operator ==(ActionType? left, ActionType? right)
{
if (left is null)
return right is null;
return left.Equals(right);
}
public static bool operator !=(ActionType? left, ActionType? right)
{
return !(left == right);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = Priority.GetHashCode();
hashCode = (hashCode * 397) ^ DisplayName.GetHashCode();
hashCode = (hashCode * 397) ^ Provider.GetHashCode();
return hashCode;
}
}
}

View File

@@ -1,43 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Actions.ActionTypes;
/// <summary>
/// Instantaneous action with no extra targeting information. Will result in <see cref="InstantActionEvent"/> being raised.
/// </summary>
[Serializable, NetSerializable]
[Virtual]
public partial class InstantAction : ActionType
{
/// <summary>
/// The local-event to raise when this action is performed.
/// </summary>
[DataField("event")]
[NonSerialized]
public InstantActionEvent? Event;
public InstantAction() { }
public InstantAction(InstantAction toClone)
{
CopyFrom(toClone);
}
public override void CopyFrom(object objectToClone)
{
base.CopyFrom(objectToClone);
// Server doesn't serialize events to us.
// As such we don't want them to bulldoze any events we may have gotten locally.
if (objectToClone is not InstantAction toClone)
return;
// Events should be re-usable, and shouldn't be modified during prediction.
if (toClone.Event != null)
Event = toClone.Event;
}
public override object Clone()
{
return new InstantAction(this);
}
}

View File

@@ -1,153 +0,0 @@
using Content.Shared.Interaction;
using Content.Shared.Whitelist;
using Robust.Shared.Serialization;
namespace Content.Shared.Actions.ActionTypes;
[Serializable, NetSerializable]
public abstract partial class TargetedAction : ActionType
{
/// <summary>
/// For entity- or map-targeting actions, if this is true the action will remain selected after it is used, so
/// it can be continuously re-used. If this is false, the action will be deselected after one use.
/// </summary>
[DataField("repeat")]
public bool Repeat;
/// <summary>
/// For entity- or map-targeting action, determines whether the action is deselected if the user doesn't click a valid target.
/// </summary>
[DataField("deselectOnMiss")]
public bool DeselectOnMiss;
/// <summary>
/// Whether the action system should block this action if the user cannot actually access the target
/// (unobstructed, in inventory, in backpack, etc). Some spells or abilities may want to disable this and
/// implement their own checks.
/// </summary>
/// <remarks>
/// Even if this is false, the <see cref="Range"/> will still be checked.
/// </remarks>
[DataField("checkCanAccess")]
public bool CheckCanAccess = true;
[DataField("range")]
public float Range = SharedInteractionSystem.InteractionRange;
/// <summary>
/// If the target is invalid, this bool determines whether the left-click will default to performing a standard-interaction
/// </summary>
/// <remarks>
/// Interactions will still be blocked if the target-validation generates a pop-up
/// </remarks>
[DataField("interactOnMiss")]
public bool InteractOnMiss = false;
/// <summary>
/// If true, and if <see cref="ShowHandItemOverlay"/> is enabled, then this action's icon will be drawn by that
/// over lay in place of the currently held item "held item".
/// </summary>
[DataField("targetingIndicator")]
public bool TargetingIndicator = true;
public override void CopyFrom(object objectToClone)
{
base.CopyFrom(objectToClone);
if (objectToClone is not TargetedAction toClone)
return;
Range = toClone.Range;
CheckCanAccess = toClone.CheckCanAccess;
DeselectOnMiss = toClone.DeselectOnMiss;
Repeat = toClone.Repeat;
InteractOnMiss = toClone.InteractOnMiss;
TargetingIndicator = toClone.TargetingIndicator;
}
}
/// <summary>
/// Action that targets some entity. Will result in <see cref="EntityTargetActionEvent"/> being raised.
/// </summary>
[Serializable, NetSerializable]
[Virtual]
public partial class EntityTargetAction : TargetedAction
{
/// <summary>
/// The local-event to raise when this action is performed.
/// </summary>
[NonSerialized]
[DataField("event")]
public EntityTargetActionEvent? Event;
[DataField("whitelist")]
public EntityWhitelist? Whitelist;
[DataField("canTargetSelf")]
public bool CanTargetSelf = true;
public EntityTargetAction() { }
public EntityTargetAction(EntityTargetAction toClone)
{
CopyFrom(toClone);
}
public override void CopyFrom(object objectToClone)
{
base.CopyFrom(objectToClone);
if (objectToClone is not EntityTargetAction toClone)
return;
CanTargetSelf = toClone.CanTargetSelf;
// This isn't a deep copy, but I don't expect white-lists to ever be edited during prediction. So good enough?
Whitelist = toClone.Whitelist;
// Events should be re-usable, and shouldn't be modified during prediction.
if (toClone.Event != null)
Event = toClone.Event;
}
public override object Clone()
{
return new EntityTargetAction(this);
}
}
/// <summary>
/// Action that targets some map coordinates. Will result in <see cref="WorldTargetActionEvent"/> being raised.
/// </summary>
[Serializable, NetSerializable]
[Virtual]
public partial class WorldTargetAction : TargetedAction
{
/// <summary>
/// The local-event to raise when this action is performed.
/// </summary>
[DataField("event")]
[NonSerialized]
public WorldTargetActionEvent? Event;
public WorldTargetAction() { }
public WorldTargetAction(WorldTargetAction toClone)
{
CopyFrom(toClone);
}
public override void CopyFrom(object objectToClone)
{
base.CopyFrom(objectToClone);
if (objectToClone is not WorldTargetAction toClone)
return;
// Events should be re-usable, and shouldn't be modified during prediction.
if (toClone.Event != null)
Event = toClone.Event;
}
public override object Clone()
{
return new WorldTargetAction(this);
}
}

View File

@@ -1,4 +1,3 @@
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
@@ -9,10 +8,10 @@ namespace Content.Shared.Actions;
[Access(typeof(SharedActionsSystem))]
public sealed partial class ActionsComponent : Component
{
[ViewVariables]
[Access(typeof(SharedActionsSystem), Other = AccessPermissions.ReadExecute)]
// FIXME Friends
public SortedSet<ActionType> Actions = new();
/// <summary>
/// Handled on the client to track added and removed actions.
/// </summary>
[ViewVariables] public readonly Dictionary<EntityUid, ActionMetaData> OldClientActions = new();
public override bool SendOnlyToOwner => true;
}
@@ -20,17 +19,16 @@ public sealed partial class ActionsComponent : Component
[Serializable, NetSerializable]
public sealed class ActionsComponentState : ComponentState
{
public readonly List<ActionType> Actions;
public readonly List<EntityUid> Actions;
[NonSerialized]
public SortedSet<ActionType>? SortedActions;
public ActionsComponentState(List<ActionType> actions)
public ActionsComponentState(List<EntityUid> actions)
{
Actions = actions;
}
}
public readonly record struct ActionMetaData(bool ClientExclusive, bool AutoRemove);
/// <summary>
/// Determines how the action icon appears in the hotbar for item actions.
/// </summary>

View File

@@ -0,0 +1,187 @@
using Robust.Shared.Audio;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Actions;
// TODO this should be an IncludeDataFields of each action component type, not use inheritance
public abstract partial class BaseActionComponent : Component
{
public abstract BaseActionEvent? BaseEvent { get; }
/// <summary>
/// Icon representing this action in the UI.
/// </summary>
[DataField("icon")] public SpriteSpecifier? Icon;
/// <summary>
/// For toggle actions only, icon to show when toggled on. If omitted, the action will simply be highlighted
/// when turned on.
/// </summary>
[DataField("iconOn")] public SpriteSpecifier? IconOn;
/// <summary>
/// If not null, this color will modulate the action icon color.
/// </summary>
/// <remarks>
/// This currently only exists for decal-placement actions, so that the action icons correspond to the color of
/// the decal. But this is probably useful for other actions, including maybe changing color on toggle.
/// </remarks>
[DataField("iconColor")] public Color IconColor = Color.White;
/// <summary>
/// Keywords that can be used to search for this action in the action menu.
/// </summary>
[DataField("keywords")] public HashSet<string> Keywords = new();
/// <summary>
/// Whether this action is currently enabled. If not enabled, this action cannot be performed.
/// </summary>
[DataField("enabled")] public bool Enabled = true;
/// <summary>
/// The toggle state of this action. Toggling switches the currently displayed icon, see <see cref="Icon"/> and <see cref="IconOn"/>.
/// </summary>
/// <remarks>
/// The toggle can set directly via <see cref="SharedActionsSystem.SetToggled"/>, but it will also be
/// automatically toggled for targeted-actions while selecting a target.
/// </remarks>
public bool Toggled;
/// <summary>
/// The current cooldown on the action.
/// </summary>
public (TimeSpan Start, TimeSpan End)? Cooldown;
/// <summary>
/// Time interval between action uses.
/// </summary>
[DataField("useDelay")] public TimeSpan? UseDelay;
/// <summary>
/// Convenience tool for actions with limited number of charges. Automatically decremented on use, and the
/// action is disabled when it reaches zero. Does NOT automatically remove the action from the action bar.
/// </summary>
[DataField("charges")] public int? Charges;
/// <summary>
/// The entity that enables / provides this action. If the action is innate, this may be the user themselves. If
/// this action has no provider (e.g., mapping tools), the this will result in broadcast events.
/// </summary>
public EntityUid? Provider;
/// <summary>
/// Entity to use for the action icon. Defaults to using <see cref="Provider"/>.
/// </summary>
public EntityUid? EntityIcon
{
get => _entityIcon ?? Provider;
set => _entityIcon = value;
}
private EntityUid? _entityIcon;
/// <summary>
/// Whether the action system should block this action if the user cannot currently interact. Some spells or
/// abilities may want to disable this and implement their own checks.
/// </summary>
[DataField("checkCanInteract")] public bool CheckCanInteract = true;
/// <summary>
/// If true, will simply execute the action locally without sending to the server.
/// </summary>
[DataField("clientExclusive")] public bool ClientExclusive = false;
/// <summary>
/// Determines the order in which actions are automatically added the action bar.
/// </summary>
[DataField("priority")] public int Priority = 0;
/// <summary>
/// What entity, if any, currently has this action in the actions component?
/// </summary>
[ViewVariables] public EntityUid? AttachedEntity;
/// <summary>
/// Whether or not to automatically add this action to the action bar when it becomes available.
/// </summary>
[DataField("autoPopulate")] public bool AutoPopulate = true;
/// <summary>
/// Whether or not to automatically remove this action to the action bar when it becomes unavailable.
/// </summary>
[DataField("autoRemove")] public bool AutoRemove = true;
/// <summary>
/// Temporary actions are removed from the action component when removed from the action-bar/GUI. Currently,
/// should only be used for client-exclusive actions (server is not notified).
/// </summary>
/// <remarks>
/// Currently there is no way for a player to just voluntarily remove actions. They can hide them from the
/// toolbar, but not actually remove them. This is undesirable for things like dynamically added mapping
/// entity-selection actions, as the # of actions would just keep increasing.
/// </remarks>
[DataField("temporary")] public bool Temporary;
// TODO re-add support for this
// UI refactor seems to have just broken it.
/// <summary>
/// Determines the appearance of the entity-icon for actions that are enabled via some entity.
/// </summary>
[DataField("itemIconStyle")] public ItemActionIconStyle ItemIconStyle;
/// <summary>
/// If not null, this sound will be played when performing this action.
/// </summary>
[DataField("sound")] public SoundSpecifier? Sound;
}
[Serializable, NetSerializable]
public abstract class BaseActionComponentState : ComponentState
{
public SpriteSpecifier? Icon;
public SpriteSpecifier? IconOn;
public Color IconColor;
public HashSet<string> Keywords;
public bool Enabled;
public bool Toggled;
public (TimeSpan Start, TimeSpan End)? Cooldown;
public TimeSpan? UseDelay;
public int? Charges;
public EntityUid? Provider;
public EntityUid? EntityIcon;
public bool CheckCanInteract;
public bool ClientExclusive;
public int Priority;
public EntityUid? AttachedEntity;
public bool AutoPopulate;
public bool AutoRemove;
public bool Temporary;
public ItemActionIconStyle ItemIconStyle;
public SoundSpecifier? Sound;
protected BaseActionComponentState(BaseActionComponent component)
{
Icon = component.Icon;
IconOn = component.IconOn;
IconColor = component.IconColor;
Keywords = component.Keywords;
Enabled = component.Enabled;
Toggled = component.Toggled;
Cooldown = component.Cooldown;
UseDelay = component.UseDelay;
Charges = component.Charges;
Provider = component.Provider;
EntityIcon = component.EntityIcon;
CheckCanInteract = component.CheckCanInteract;
ClientExclusive = component.ClientExclusive;
Priority = component.Priority;
AttachedEntity = component.AttachedEntity;
AutoPopulate = component.AutoPopulate;
AutoRemove = component.AutoRemove;
Temporary = component.Temporary;
ItemIconStyle = component.ItemIconStyle;
Sound = component.Sound;
}
}

View File

@@ -0,0 +1,43 @@
using Content.Shared.Interaction;
namespace Content.Shared.Actions;
public abstract partial class BaseTargetActionComponent : BaseActionComponent
{
/// <summary>
/// For entity- or map-targeting actions, if this is true the action will remain selected after it is used, so
/// it can be continuously re-used. If this is false, the action will be deselected after one use.
/// </summary>
[DataField("repeat")] public bool Repeat;
/// <summary>
/// For entity- or map-targeting action, determines whether the action is deselected if the user doesn't click a valid target.
/// </summary>
[DataField("deselectOnMiss")] public bool DeselectOnMiss;
/// <summary>
/// Whether the action system should block this action if the user cannot actually access the target
/// (unobstructed, in inventory, in backpack, etc). Some spells or abilities may want to disable this and
/// implement their own checks.
/// </summary>
/// <remarks>
/// Even if this is false, the <see cref="Range"/> will still be checked.
/// </remarks>
[DataField("checkCanAccess")] public bool CheckCanAccess = true;
[DataField("range")] public float Range = SharedInteractionSystem.InteractionRange;
/// <summary>
/// If the target is invalid, this bool determines whether the left-click will default to performing a standard-interaction
/// </summary>
/// <remarks>
/// Interactions will still be blocked if the target-validation generates a pop-up
/// </remarks>
[DataField("interactOnMiss")] public bool InteractOnMiss = false;
/// <summary>
/// If true, and if <see cref="ShowHandItemOverlay"/> is enabled, then this action's icon will be drawn by that
/// over lay in place of the currently held item "held item".
/// </summary>
[DataField("targetingIndicator")] public bool TargetingIndicator = true;
}

View File

@@ -0,0 +1,35 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Actions;
[RegisterComponent, NetworkedComponent]
public sealed partial class EntityTargetActionComponent : BaseTargetActionComponent
{
public override BaseActionEvent? BaseEvent => Event;
/// <summary>
/// The local-event to raise when this action is performed.
/// </summary>
[DataField("event")]
[NonSerialized]
public EntityTargetActionEvent? Event;
[DataField("whitelist")] public EntityWhitelist? Whitelist;
[DataField("canTargetSelf")] public bool CanTargetSelf = true;
}
[Serializable, NetSerializable]
public sealed class EntityTargetActionComponentState : BaseActionComponentState
{
public EntityWhitelist? Whitelist;
public bool CanTargetSelf;
public EntityTargetActionComponentState(EntityTargetActionComponent component) : base(component)
{
Whitelist = component.Whitelist;
CanTargetSelf = component.CanTargetSelf;
}
}

View File

@@ -0,0 +1,3 @@
namespace Content.Shared.Actions.Events;
public sealed partial class EggLayInstantActionEvent : InstantActionEvent {}

View File

@@ -0,0 +1,4 @@
namespace Content.Shared.Actions.Events;
[ByRefEvent]
public record struct GetActionDataEvent(BaseActionComponent? Action);

View File

@@ -0,0 +1,5 @@
namespace Content.Shared.Actions.Events;
public sealed partial class InvisibleWallActionEvent : InstantActionEvent
{
}

View File

@@ -0,0 +1,25 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Actions;
[RegisterComponent, NetworkedComponent]
public sealed partial class InstantActionComponent : BaseActionComponent
{
public override BaseActionEvent? BaseEvent => Event;
/// <summary>
/// The local-event to raise when this action is performed.
/// </summary>
[DataField("event")]
[NonSerialized]
public InstantActionEvent? Event;
}
[Serializable, NetSerializable]
public sealed class InstantActionComponentState : BaseActionComponentState
{
public InstantActionComponentState(InstantActionComponent component) : base(component)
{
}
}

View File

@@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Actions.Events;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Hands;
@@ -8,16 +10,18 @@ using Content.Shared.Inventory.Events;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using System.Linq;
namespace Content.Shared.Actions;
public abstract class SharedActionsSystem : EntitySystem
{
private const string ActionContainerId = "ActionContainer";
[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!;
@@ -34,14 +38,149 @@ public abstract class SharedActionsSystem : EntitySystem
SubscribeLocalEvent<ActionsComponent, DidUnequipEvent>(OnDidUnequip);
SubscribeLocalEvent<ActionsComponent, DidUnequipHandEvent>(OnHandUnequipped);
SubscribeLocalEvent<ActionsComponent, ComponentGetState>(GetState);
SubscribeLocalEvent<ActionsComponent, MapInitEvent>(OnActionsMapInit);
SubscribeLocalEvent<ActionsComponent, ComponentGetState>(OnActionsGetState);
SubscribeLocalEvent<ActionsComponent, ComponentShutdown>(OnActionsShutdown);
SubscribeLocalEvent<InstantActionComponent, ComponentGetState>(OnInstantGetState);
SubscribeLocalEvent<EntityTargetActionComponent, ComponentGetState>(OnEntityTargetGetState);
SubscribeLocalEvent<WorldTargetActionComponent, ComponentGetState>(OnWorldTargetGetState);
SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState);
SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState);
SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState);
SubscribeLocalEvent<InstantActionComponent, GetActionDataEvent>(OnGetActionData);
SubscribeLocalEvent<EntityTargetActionComponent, GetActionDataEvent>(OnGetActionData);
SubscribeLocalEvent<WorldTargetActionComponent, GetActionDataEvent>(OnGetActionData);
SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest);
}
#region ComponentStateManagement
public virtual void Dirty(ActionType action)
private void OnInstantGetState(EntityUid uid, InstantActionComponent component, ref ComponentGetState args)
{
args.State = new InstantActionComponentState(component);
}
private void OnEntityTargetGetState(EntityUid uid, EntityTargetActionComponent component, ref ComponentGetState args)
{
args.State = new EntityTargetActionComponentState(component);
}
private void OnWorldTargetGetState(EntityUid uid, WorldTargetActionComponent component, ref ComponentGetState args)
{
args.State = new WorldTargetActionComponentState(component);
}
private void BaseHandleState(BaseActionComponent component, BaseActionComponentState state)
{
component.Icon = state.Icon;
component.IconOn = state.IconOn;
component.IconColor = state.IconColor;
component.Keywords = new HashSet<string>(state.Keywords);
component.Enabled = state.Enabled;
component.Toggled = state.Toggled;
component.Cooldown = state.Cooldown;
component.UseDelay = state.UseDelay;
component.Charges = state.Charges;
component.Provider = state.Provider;
component.EntityIcon = state.EntityIcon;
component.CheckCanInteract = state.CheckCanInteract;
component.ClientExclusive = state.ClientExclusive;
component.Priority = state.Priority;
component.AttachedEntity = state.AttachedEntity;
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(component, state);
}
private void OnEntityTargetHandleState(EntityUid uid, EntityTargetActionComponent component, ref ComponentHandleState args)
{
if (args.Current is not EntityTargetActionComponentState state)
return;
BaseHandleState(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(component, state);
}
private void OnGetActionData<T>(EntityUid uid, T component, ref GetActionDataEvent args) where T : BaseActionComponent
{
args.Action = component;
}
public BaseActionComponent? GetActionData(EntityUid? actionId)
{
if (actionId == null)
return null;
// 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;
}
public bool TryGetActionData(
[NotNullWhen(true)] EntityUid? actionId,
[NotNullWhen(true)] out BaseActionComponent? action)
{
action = null;
return actionId != null && (action = GetActionData(actionId)) != null;
}
protected Container EnsureContainer(EntityUid holderId)
{
return _containerSystem.EnsureContainer<Container>(holderId, ActionContainerId);
}
protected bool TryGetContainer(
EntityUid holderId,
[NotNullWhen(true)] out IContainer? container,
ContainerManagerComponent? containerManager = null)
{
return _containerSystem.TryGetContainer(holderId, ActionContainerId, out container, containerManager);
}
public void SetCooldown(EntityUid? actionId, TimeSpan start, TimeSpan end)
{
if (actionId == null)
return;
var action = GetActionData(actionId);
if (action == null)
return;
action.Cooldown = (start, end);
Dirty(actionId.Value, action);
}
#region ComponentStateManagement
public virtual void Dirty(EntityUid? actionId)
{
if (!TryGetActionData(actionId, out var action))
return;
Dirty(actionId.Value, action);
if (action.AttachedEntity == null)
return;
@@ -51,39 +190,63 @@ public abstract class SharedActionsSystem : EntitySystem
return;
}
Dirty(comp);
Dirty(action.AttachedEntity.Value, comp);
}
public void SetToggled(ActionType action, bool toggled)
public void SetToggled(EntityUid? actionId, bool toggled)
{
if (!TryGetActionData(actionId, out var action) ||
action.Toggled == toggled)
{
if (action.Toggled == toggled)
return;
}
action.Toggled = toggled;
Dirty(action);
Dirty(actionId.Value, action);
}
public void SetEnabled(ActionType action, bool enabled)
public void SetEnabled(EntityUid? actionId, bool enabled)
{
if (!TryGetActionData(actionId, out var action) ||
action.Enabled == enabled)
{
if (action.Enabled == enabled)
return;
}
action.Enabled = enabled;
Dirty(action);
Dirty(actionId.Value, action);
}
public void SetCharges(ActionType action, int? charges)
public void SetCharges(EntityUid? actionId, int? charges)
{
if (!TryGetActionData(actionId, out var action) ||
action.Charges == charges)
{
if (action.Charges == charges)
return;
}
action.Charges = charges;
Dirty(action);
Dirty(actionId.Value, action);
}
private void GetState(EntityUid uid, ActionsComponent component, ref ComponentGetState args)
private void OnActionsMapInit(EntityUid uid, ActionsComponent component, MapInitEvent args)
{
args.State = new ActionsComponentState(component.Actions.ToList());
EnsureContainer(uid);
}
private void OnActionsGetState(EntityUid uid, ActionsComponent component, ref ComponentGetState args)
{
var actions = new List<EntityUid>();
if (TryGetContainer(uid, out var container))
actions.AddRange(container.ContainedEntities);
args.State = new ActionsComponentState(actions);
}
private void OnActionsShutdown(EntityUid uid, ActionsComponent component, ComponentShutdown args)
{
if (TryGetContainer(uid, out var container))
container.Shutdown(EntityManager);
}
#endregion
@@ -101,33 +264,36 @@ public abstract class SharedActionsSystem : EntitySystem
if (!TryComp(user, out ActionsComponent? component))
return;
if (!TryComp(ev.Action, out MetaDataComponent? metaData))
return;
var name = Name(ev.Action, metaData);
// Does the user actually have the requested action?
if (!component.Actions.TryGetValue(ev.Action, out var act))
if (!TryGetContainer(user, out var container) || !container.Contains(ev.Action))
{
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} attempted to perform an action that they do not have: {ev.Action.DisplayName}.");
$"{ToPrettyString(user):user} attempted to perform an action that they do not have: {name}.");
return;
}
if (!act.Enabled)
var action = GetActionData(ev.Action);
if (action == null || !action.Enabled)
return;
var curTime = GameTiming.CurTime;
if (act.Cooldown.HasValue && act.Cooldown.Value.End > curTime)
if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime)
return;
BaseActionEvent? performEvent = null;
// Validate request by checking action blockers and the like:
var name = Loc.GetString(act.DisplayName);
switch (act)
switch (action)
{
case EntityTargetAction entityAction:
case EntityTargetActionComponent entityAction:
if (ev.EntityTarget is not { Valid: true } entityTarget)
{
Log.Error($"Attempted to perform an entity-targeted action without a target! Action: {entityAction.DisplayName}");
Log.Error($"Attempted to perform an entity-targeted action without a target! Action: {name}");
return;
}
@@ -137,7 +303,7 @@ public abstract class SharedActionsSystem : EntitySystem
if (!ValidateEntityTarget(user, entityTarget, entityAction))
return;
if (act.Provider == null)
if (action.Provider == null)
{
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} is performing the {name:action} action targeted at {ToPrettyString(entityTarget):target}.");
@@ -145,22 +311,21 @@ public abstract class SharedActionsSystem : EntitySystem
else
{
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(act.Provider.Value):provider}) targeted at {ToPrettyString(entityTarget):target}.");
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(action.Provider.Value):provider}) targeted at {ToPrettyString(entityTarget):target}.");
}
if (entityAction.Event != null)
{
entityAction.Event.Target = entityTarget;
Dirty(ev.Action, entityAction);
performEvent = entityAction.Event;
}
break;
case WorldTargetAction worldAction:
case WorldTargetActionComponent worldAction:
if (ev.EntityCoordinatesTarget is not { } entityCoordinatesTarget)
{
Log.Error($"Attempted to perform a world-targeted action without a target! Action: {worldAction.DisplayName}");
Log.Error($"Attempted to perform a world-targeted action without a target! Action: {name}");
return;
}
@@ -169,7 +334,7 @@ public abstract class SharedActionsSystem : EntitySystem
if (!ValidateWorldTarget(user, entityCoordinatesTarget, worldAction))
return;
if (act.Provider == null)
if (action.Provider == null)
{
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} is performing the {name:action} action targeted at {entityCoordinatesTarget:target}.");
@@ -177,23 +342,22 @@ public abstract class SharedActionsSystem : EntitySystem
else
{
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(act.Provider.Value):provider}) targeted at {entityCoordinatesTarget:target}.");
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(action.Provider.Value):provider}) targeted at {entityCoordinatesTarget:target}.");
}
if (worldAction.Event != null)
{
worldAction.Event.Target = entityCoordinatesTarget;
Dirty(ev.Action, worldAction);
performEvent = worldAction.Event;
}
break;
case InstantAction instantAction:
if (act.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
case InstantActionComponent instantAction:
if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
return;
if (act.Provider == null)
if (action.Provider == null)
{
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} is performing the {name:action} action.");
@@ -201,7 +365,7 @@ public abstract class SharedActionsSystem : EntitySystem
else
{
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} is performing the {name:action} action provided by {ToPrettyString(act.Provider.Value):provider}.");
$"{ToPrettyString(user):user} is performing the {name:action} action provided by {ToPrettyString(action.Provider.Value):provider}.");
}
performEvent = instantAction.Event;
@@ -212,10 +376,10 @@ public abstract class SharedActionsSystem : EntitySystem
performEvent.Performer = user;
// All checks passed. Perform the action!
PerformAction(user, component, act, performEvent, curTime);
PerformAction(user, component, ev.Action, action, performEvent, curTime);
}
public bool ValidateEntityTarget(EntityUid user, EntityUid target, EntityTargetAction action)
public bool ValidateEntityTarget(EntityUid user, EntityUid target, EntityTargetActionComponent action)
{
if (!target.IsValid() || Deleted(target))
return false;
@@ -254,7 +418,7 @@ public abstract class SharedActionsSystem : EntitySystem
return _interactionSystem.CanAccessViaStorage(user, target);
}
public bool ValidateWorldTarget(EntityUid user, EntityCoordinates coords, WorldTargetAction action)
public bool ValidateWorldTarget(EntityUid user, EntityCoordinates coords, WorldTargetActionComponent action)
{
if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
return false;
@@ -276,7 +440,7 @@ public abstract class SharedActionsSystem : EntitySystem
return _interactionSystem.InRangeUnobstructed(user, coords, range: action.Range);
}
public void PerformAction(EntityUid performer, ActionsComponent? component, ActionType action, BaseActionEvent? actionEvent, TimeSpan curTime, bool predicted = true)
public void PerformAction(EntityUid performer, ActionsComponent? component, EntityUid actionId, BaseActionComponent action, BaseActionEvent? actionEvent, TimeSpan curTime, bool predicted = true)
{
var handled = false;
@@ -320,89 +484,180 @@ public abstract class SharedActionsSystem : EntitySystem
action.Cooldown = (curTime, curTime + action.UseDelay.Value);
}
Dirty(actionId, action);
if (dirty && component != null)
Dirty(component);
Dirty(performer, component);
}
#endregion
#region AddRemoveActions
/// <summary>
/// Add an action to an action component. If the entity has no action component, this will give them one.
/// Add an action to an action holder.
/// If the holder has no actions component, this will give them one.
/// </summary>
/// <param name="uid">Entity to receive the actions</param>
/// <param name="action">The action to add</param>
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);
}
AddAction(holderId, actionId.Value, provider, holderComp);
return GetActionData(actionId);
}
/// <summary>
/// Add an action to an action holder.
/// If the holder has no actions component, this will give them one.
/// </summary>
/// <param name="holderId">Entity to receive the actions</param>
/// <param name="actionId">Action entity to add</param>
/// <param name="provider">The entity that enables these actions (e.g., flashlight). May be null (innate actions).</param>
public virtual void AddAction(EntityUid uid, ActionType action, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
/// <param name="holder">Component of <see cref="holderId"/></param>
/// <param name="action">Component of <see cref="actionId"/></param>
/// <param name="actionContainer">Action container of <see cref="holderId"/></param>
public virtual void AddAction(EntityUid holderId, EntityUid actionId, EntityUid? provider, ActionsComponent? holder = null, BaseActionComponent? action = null, bool dirty = true, IContainer? actionContainer = null)
{
// Because action classes have state data, e.g. cooldowns and uses-remaining, people should not be adding prototypes directly
if (action is IPrototype)
action ??= GetActionData(actionId);
// TODO remove when action subscriptions are split up
if (action == null)
{
Log.Error("Attempted to directly add a prototype action. You need to clone a prototype in order to use it.");
Log.Warning($"No {nameof(BaseActionComponent)} found on entity {actionId}");
return;
}
comp ??= EnsureComp<ActionsComponent>(uid);
holder ??= EnsureComp<ActionsComponent>(holderId);
action.Provider = provider;
action.AttachedEntity = uid;
AddActionInternal(comp, action);
action.AttachedEntity = holderId;
Dirty(actionId, action);
actionContainer ??= EnsureContainer(holderId);
AddActionInternal(actionId, actionContainer);
if (dirty)
Dirty(comp);
Dirty(holderId, holder);
}
protected virtual void AddActionInternal(ActionsComponent comp, ActionType action)
protected virtual void AddActionInternal(EntityUid actionId, IContainer container)
{
comp.Actions.Add(action);
container.Insert(actionId);
}
/// <summary>
/// Add actions to an action component. If the entity has no action component, this will give them one.
/// </summary>
/// <param name="uid">Entity to receive the actions</param>
/// <param name="holderId">Entity to receive the actions</param>
/// <param name="actions">The actions to add</param>
/// <param name="provider">The entity that enables these actions (e.g., flashlight). May be null (innate actions).</param>
public void AddActions(EntityUid uid, IEnumerable<ActionType> actions, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
public void AddActions(EntityUid holderId, IEnumerable<EntityUid> actions, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
{
comp ??= EnsureComp<ActionsComponent>(uid);
comp ??= EnsureComp<ActionsComponent>(holderId);
var allClientExclusive = true;
var container = EnsureContainer(holderId);
foreach (var action in actions)
foreach (var actionId in actions)
{
AddAction(uid, action, provider, comp, false);
var action = GetActionData(actionId);
if (action == null)
continue;
AddAction(holderId, actionId, provider, comp, action, false, container);
allClientExclusive = allClientExclusive && action.ClientExclusive;
}
if (dirty && !allClientExclusive)
Dirty(comp);
Dirty(holderId, comp);
}
public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetActions(EntityUid holderId, IContainer? container = null)
{
if (container == null &&
!TryGetContainer(holderId, out container))
{
yield break;
}
foreach (var actionId in container.ContainedEntities)
{
if (!TryGetActionData(actionId, out var action))
continue;
yield return (actionId, action);
}
}
/// <summary>
/// Remove any actions that were enabled by some other entity. Useful when unequiping items that grant actions.
/// </summary>
public void RemoveProvidedActions(EntityUid uid, EntityUid provider, ActionsComponent? comp = null)
public void RemoveProvidedActions(EntityUid holderId, EntityUid provider, ActionsComponent? comp = null, ContainerManagerComponent? actionContainer = null)
{
if (!Resolve(uid, ref comp, false))
if (!Resolve(holderId, ref comp, ref actionContainer, false))
return;
foreach (var act in comp.Actions.ToArray())
{
if (act.Provider == provider)
RemoveAction(uid, act, comp, dirty: false);
}
Dirty(comp);
}
public virtual void RemoveAction(EntityUid uid, ActionType action, ActionsComponent? comp = null, bool dirty = true)
{
if (!Resolve(uid, ref comp, false))
if (!TryGetContainer(holderId, out var container, actionContainer))
return;
comp.Actions.Remove(action);
foreach (var actionId in container.ContainedEntities.ToArray())
{
var action = GetActionData(actionId);
if (action?.Provider == provider)
RemoveAction(holderId, actionId, comp, dirty: false, actionContainer: actionContainer);
}
Dirty(holderId, comp);
}
public virtual void RemoveAction(EntityUid holderId, EntityUid? actionId, ActionsComponent? comp = null, BaseActionComponent? action = null, bool dirty = true, ContainerManagerComponent? actionContainer = null)
{
if (actionId == null ||
!Resolve(holderId, ref comp, ref actionContainer, false) ||
!TryGetContainer(holderId, out var container, actionContainer) ||
!container.Contains(actionId.Value) ||
TerminatingOrDeleted(actionId.Value))
{
return;
}
action ??= GetActionData(actionId);
container.Remove(actionId.Value);
if (action != null)
{
action.AttachedEntity = null;
Dirty(actionId.Value, action);
}
if (dirty)
Dirty(comp);
Dirty(holderId, comp);
}
/// <summary>
/// Removes all actions with the given prototype id.
/// </summary>
public void RemoveAction(EntityUid holderId, string actionPrototypeId, ActionsComponent? holderComp = null, ContainerManagerComponent? actionContainer = null)
{
if (!Resolve(holderId, ref holderComp, ref actionContainer, false))
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));
}
foreach (var action in actions)
{
RemoveAction(holderId, action.Id, holderComp, action.Comp, actionContainer: actionContainer);
}
}
#endregion
@@ -410,7 +665,7 @@ public abstract class SharedActionsSystem : EntitySystem
#region EquipHandlers
private void OnDidEquip(EntityUid uid, ActionsComponent component, DidEquipEvent args)
{
var ev = new GetItemActionsEvent(args.Equipee, args.SlotFlags);
var ev = new GetItemActionsEvent(EntityManager, _net, args.Equipee, args.SlotFlags);
RaiseLocalEvent(args.Equipment, ev);
if (ev.Actions.Count == 0)
@@ -421,7 +676,7 @@ public abstract class SharedActionsSystem : EntitySystem
private void OnHandEquipped(EntityUid uid, ActionsComponent component, DidEquipHandEvent args)
{
var ev = new GetItemActionsEvent(args.User);
var ev = new GetItemActionsEvent(EntityManager, _net, args.User);
RaiseLocalEvent(args.Equipped, ev);
if (ev.Actions.Count == 0)

View File

@@ -0,0 +1,25 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Actions;
[RegisterComponent, NetworkedComponent]
public sealed partial class WorldTargetActionComponent : BaseTargetActionComponent
{
public override BaseActionEvent? BaseEvent => Event;
/// <summary>
/// The local-event to raise when this action is performed.
/// </summary>
[DataField("event")]
[NonSerialized]
public WorldTargetActionEvent? Event;
}
[Serializable, NetSerializable]
public sealed class WorldTargetActionComponentState : BaseActionComponentState
{
public WorldTargetActionComponentState(WorldTargetActionComponent component) : base(component)
{
}
}

View File

@@ -1,18 +1,25 @@
using Content.Shared.Speech;
using Content.Shared.Actions;
using Content.Shared.Bed.Sleep;
using Content.Shared.Eye.Blinding.Systems;
using Content.Shared.Speech;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Bed.Sleep
{
public abstract class SharedSleepingSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly BlindableSystem _blindableSystem = default!;
[ValidatePrototypeId<EntityPrototype>] private const string WakeActionId = "ActionWake";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SleepingComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<SleepingComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SleepingComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<SleepingComponent, SpeakAttemptEvent>(OnSpeakAttempt);
SubscribeLocalEvent<SleepingComponent, CanSeeAttemptEvent>(OnSeeAttempt);
@@ -22,7 +29,7 @@ namespace Content.Server.Bed.Sleep
private void OnSleepUnpaused(EntityUid uid, SleepingComponent component, ref EntityUnpausedEvent args)
{
component.CoolDownEnd += args.PausedTime;
Dirty(component);
Dirty(uid, component);
}
private void OnStartup(EntityUid uid, SleepingComponent component, ComponentStartup args)
@@ -32,8 +39,17 @@ namespace Content.Server.Bed.Sleep
_blindableSystem.UpdateIsBlind(uid);
}
private void OnMapInit(EntityUid uid, SleepingComponent component, MapInitEvent args)
{
component.WakeAction = Spawn(WakeActionId);
_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);

View File

@@ -25,4 +25,6 @@ public sealed partial class SleepingComponent : Component
[DataField("cooldownEnd", customTypeSerializer:typeof(TimeOffsetSerializer))]
public TimeSpan CoolDownEnd;
[DataField("wakeAction")] public EntityUid? WakeAction;
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.Actions;
namespace Content.Shared.Bible;
public sealed partial class SummonActionEvent : InstantActionEvent
{
}

View File

@@ -1,8 +1,6 @@
using System.Linq;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Examine;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
@@ -80,11 +78,7 @@ public sealed partial class BlockingSystem : EntitySystem
private void OnGetActions(EntityUid uid, BlockingComponent component, GetItemActionsEvent args)
{
if (component.BlockingToggleAction == null && _proto.TryIndex(component.BlockingToggleActionId, out InstantActionPrototype? act))
component.BlockingToggleAction = new(act);
if (component.BlockingToggleAction != null)
args.Actions.Add(component.BlockingToggleAction);
args.AddAction(ref component.BlockingToggleActionEntity, component.BlockingToggleAction);
}
private void OnToggleAction(EntityUid uid, BlockingComponent component, ToggleActionEvent args)
@@ -191,7 +185,7 @@ public sealed partial class BlockingSystem : EntitySystem
CantBlockError(user);
return false;
}
_actionsSystem.SetToggled(component.BlockingToggleAction, true);
_actionsSystem.SetToggled(component.BlockingToggleActionEntity, true);
_popupSystem.PopupEntity(msgUser, user, user);
_popupSystem.PopupEntity(msgOther, user, Filter.PvsExcept(user), true);
}
@@ -252,7 +246,7 @@ public sealed partial class BlockingSystem : EntitySystem
if (xform.Anchored)
_transformSystem.Unanchor(user, xform);
_actionsSystem.SetToggled(component.BlockingToggleAction, false);
_actionsSystem.SetToggled(component.BlockingToggleActionEntity, false);
_fixtureSystem.DestroyFixture(user, BlockingComponent.BlockFixtureID, body: physicsComponent);
_physics.SetBodyType(user, blockingUserComponent.OriginalBodyType, body: physicsComponent);
_popupSystem.PopupEntity(msgUser, user, user);

View File

@@ -1,7 +1,7 @@
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Damage;
using Robust.Shared.Audio;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Blocking;
@@ -47,11 +47,11 @@ public sealed partial class BlockingComponent : Component
[DataField("activeBlockModifier", required: true)]
public DamageModifierSet ActiveBlockDamageModifier = default!;
[DataField("blockingToggleActionId", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string BlockingToggleActionId = "ToggleBlock";
[DataField("blockingToggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string BlockingToggleAction = "ActionToggleBlock";
[DataField("blockingToggleAction")]
public InstantAction? BlockingToggleAction;
[DataField("blockingToggleActionEntity")]
public EntityUid? BlockingToggleActionEntity;
/// <summary>
/// The sound to be played when you get hit while actively blocking

View File

@@ -1,6 +1,5 @@
using Content.Shared.Containers.ItemSlots;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
namespace Content.Shared.CartridgeLoader;
@@ -42,6 +41,6 @@ public sealed partial class CartridgeLoaderComponent : Component
[DataField("diskSpace")]
public int DiskSpace = 5;
[DataField("uiKey", required: true, customTypeSerializer: typeof(EnumSerializer))]
[DataField("uiKey", required: true)]
public Enum UiKey = default!;
}

View File

@@ -1,4 +1,6 @@
using Content.Shared.Actions;
namespace Content.Shared.Clothing;
/// <summary>
@@ -55,3 +57,5 @@ public sealed class EquipmentVisualsUpdatedEvent : EntityEventArgs
RevealedLayers = revealedLayers;
}
}
public sealed partial class ToggleMaskEvent : InstantActionEvent { }

View File

@@ -1,7 +1,8 @@
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Clothing.EntitySystems;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Clothing.Components;
@@ -23,14 +24,13 @@ public sealed partial class StealthClothingComponent : Component
[DataField("visibility"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public float Visibility;
[DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ToggleAction = "ActionTogglePhaseCloak";
/// <summary>
/// The action for enabling and disabling stealth.
/// </summary>
[DataField("toggleAction")]
public InstantAction ToggleAction = new()
{
Event = new ToggleStealthEvent()
};
[DataField("toggleActionEntity")] public EntityUid? ToggleActionEntity;
}
/// <summary>

View File

@@ -1,4 +1,3 @@
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Inventory;
using Robust.Shared.Containers;
@@ -20,9 +19,11 @@ public sealed partial class ToggleableClothingComponent : Component
/// <summary>
/// Action used to toggle the clothing on or off.
/// </summary>
[DataField("actionId", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string ActionId = "ToggleSuitPiece";
public InstantAction? ToggleAction = null;
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Action = "ActionToggleSuitPiece";
[DataField("actionEntity")]
public EntityUid? ActionEntity;
/// <summary>
/// Default clothing entity prototype to spawn into the clothing container.
@@ -66,7 +67,7 @@ public sealed partial class ToggleableClothingComponent : Component
public TimeSpan? StripDelay = TimeSpan.FromSeconds(3);
/// <summary>
/// Text shown in the toggle-clothing verb. Defaults to using the name of the <see cref="ToggleAction"/> action.
/// Text shown in the toggle-clothing verb. Defaults to using the name of the <see cref="ActionEntity"/> action.
/// </summary>
[DataField("verbText")]
public string? VerbText;

View File

@@ -3,7 +3,6 @@ using Content.Shared.Clothing.Components;
using Content.Shared.Inventory.Events;
using Content.Shared.Stealth;
using Content.Shared.Stealth.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Clothing.EntitySystems;
@@ -59,7 +58,7 @@ public sealed class StealthClothingSystem : EntitySystem
if (ev.Cancelled)
return;
args.Actions.Add(comp.ToggleAction);
args.AddAction(ref comp.ToggleActionEntity, comp.ToggleAction);
}
/// <summary>

View File

@@ -1,5 +1,4 @@
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Clothing.Components;
using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement;
@@ -10,7 +9,6 @@ using Content.Shared.Popups;
using Content.Shared.Strip;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -60,7 +58,7 @@ public sealed class ToggleableClothingSystem : EntitySystem
if (!args.CanAccess || !args.CanInteract || component.ClothingUid == null || component.Container == null)
return;
var text = component.VerbText ?? component.ToggleAction?.DisplayName;
var text = component.VerbText ?? (component.ActionEntity == null ? null : Name(component.ActionEntity.Value));
if (text == null)
return;
@@ -179,8 +177,11 @@ public sealed class ToggleableClothingSystem : EntitySystem
// automatically be deleted.
// remove action.
if (component.ToggleAction?.AttachedEntity != null)
_actionsSystem.RemoveAction(component.ToggleAction.AttachedEntity.Value, component.ToggleAction);
if (_actionsSystem.TryGetActionData(component.ActionEntity, out var action) &&
action.AttachedEntity != null)
{
_actionsSystem.RemoveAction(action.AttachedEntity.Value, component.ActionEntity);
}
if (component.ClothingUid != null)
QueueDel(component.ClothingUid.Value);
@@ -200,8 +201,11 @@ public sealed class ToggleableClothingSystem : EntitySystem
return;
// remove action.
if (toggleComp.ToggleAction?.AttachedEntity != null)
_actionsSystem.RemoveAction(toggleComp.ToggleAction.AttachedEntity.Value, toggleComp.ToggleAction);
if (_actionsSystem.TryGetActionData(toggleComp.ActionEntity, out var action) &&
action.AttachedEntity != null)
{
_actionsSystem.RemoveAction(action.AttachedEntity.Value, toggleComp.ActionEntity);
}
RemComp(component.AttachedUid, toggleComp);
}
@@ -259,8 +263,7 @@ public sealed class ToggleableClothingSystem : EntitySystem
if (component.ClothingUid == null || (args.SlotFlags & component.RequiredFlags) != component.RequiredFlags)
return;
if (component.ToggleAction != null)
args.Actions.Add(component.ToggleAction);
args.AddAction(ref component.ActionEntity, component.Action);
}
private void OnInit(EntityUid uid, ToggleableClothingComponent component, ComponentInit args)
@@ -280,13 +283,9 @@ public sealed class ToggleableClothingSystem : EntitySystem
return;
}
if (component.ToggleAction == null
&& _proto.TryIndex(component.ActionId, out InstantActionPrototype? act))
{
component.ToggleAction = new(act);
}
component.ActionEntity ??= Spawn(component.Action);
if (component.ClothingUid != null && component.ToggleAction != null)
if (component.ClothingUid != null && component.ActionEntity != null)
{
DebugTools.Assert(Exists(component.ClothingUid), "Toggleable clothing is missing expected entity.");
DebugTools.Assert(TryComp(component.ClothingUid, out AttachedClothingComponent? comp), "Toggleable clothing is missing an attached component");
@@ -300,10 +299,10 @@ public sealed class ToggleableClothingSystem : EntitySystem
component.Container.Insert(component.ClothingUid.Value, EntityManager, ownerTransform: xform);
}
if (component.ToggleAction != null)
if (_actionsSystem.TryGetActionData(component.ActionEntity, out var action))
{
component.ToggleAction.EntityIcon = component.ClothingUid;
_actionsSystem.Dirty(component.ToggleAction);
action.EntityIcon = component.ClothingUid;
_actionsSystem.Dirty(component.ActionEntity);
}
}
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Clothing;
@@ -7,8 +8,11 @@ namespace Content.Shared.Clothing;
[Access(typeof(SharedMagbootsSystem))]
public sealed partial class MagbootsComponent : Component
{
[DataField("toggleAction", required: true)]
public InstantAction ToggleAction = new();
[DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>), required: true)]
public string? ToggleAction;
[DataField("toggleActionEntity")]
public EntityUid? ToggleActionEntity;
[DataField("on"), AutoNetworkedField]
public bool On;

View File

@@ -62,7 +62,7 @@ public abstract class SharedMagbootsSystem : EntitySystem
protected void OnChanged(EntityUid uid, MagbootsComponent component)
{
_sharedActions.SetToggled(component.ToggleAction, component.On);
_sharedActions.SetToggled(component.ToggleActionEntity, component.On);
_clothingSpeedModifier.SetClothingSpeedModifierEnabled(uid, component.On);
}
@@ -86,7 +86,7 @@ public abstract class SharedMagbootsSystem : EntitySystem
private void OnGetActions(EntityUid uid, MagbootsComponent component, GetItemActionsEvent args)
{
args.Actions.Add(component.ToggleAction);
args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
}
}

View File

@@ -1,7 +1,7 @@
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Targeting;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.CombatMode
@@ -32,11 +32,11 @@ namespace Content.Shared.CombatMode
#endregion
[DataField("combatToggleActionId", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string CombatToggleActionId = "CombatModeToggle";
[DataField("combatToggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string CombatToggleAction = "ActionCombatModeToggle";
[DataField("combatToggleAction")]
public InstantAction? CombatToggleAction;
[DataField("combatToggleActionEntity")]
public EntityUid? CombatToggleActionEntity;
[ViewVariables(VVAccess.ReadWrite), DataField("isInCombatMode"), AutoNetworkedField]
public bool IsInCombatMode;

View File

@@ -1,7 +1,6 @@
using Content.Shared.Actions;
using Content.Shared.Alert;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
namespace Content.Shared.CombatMode.Pacification;
@@ -33,10 +32,7 @@ public sealed class PacificationSystem : EntitySystem
_combatSystem.SetCanDisarm(uid, false, combatMode);
_combatSystem.SetInCombatMode(uid, false, combatMode);
if (combatMode.CombatToggleAction != null)
_actionsSystem.SetEnabled(combatMode.CombatToggleAction, false);
_actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, false);
_alertsSystem.ShowAlert(uid, AlertType.Pacified);
}
@@ -48,9 +44,7 @@ public sealed class PacificationSystem : EntitySystem
if (combatMode.CanDisarm != null)
_combatSystem.SetCanDisarm(uid, true, combatMode);
if (combatMode.CombatToggleAction != null)
_actionsSystem.SetEnabled(combatMode.CombatToggleAction, true);
_actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, true);
_alertsSystem.ClearAlert(uid, AlertType.Pacified);
}
}

View File

@@ -1,10 +1,7 @@
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Popups;
using Content.Shared.Targeting;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Content.Shared.CombatMode;
@@ -13,7 +10,6 @@ public abstract class SharedCombatModeSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -21,27 +17,19 @@ public abstract class SharedCombatModeSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<CombatModeComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<CombatModeComponent, MapInitEvent>(OnStartup);
SubscribeLocalEvent<CombatModeComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<CombatModeComponent, ToggleCombatActionEvent>(OnActionPerform);
}
private void OnStartup(EntityUid uid, CombatModeComponent component, ComponentStartup args)
private void OnStartup(EntityUid uid, CombatModeComponent component, MapInitEvent args)
{
if (component.CombatToggleAction == null
&& _protoMan.TryIndex(component.CombatToggleActionId, out InstantActionPrototype? toggleProto))
{
component.CombatToggleAction = new(toggleProto);
}
if (component.CombatToggleAction != null)
_actionsSystem.AddAction(uid, component.CombatToggleAction, null);
_actionsSystem.AddAction(uid, ref component.CombatToggleActionEntity, component.CombatToggleAction);
}
private void OnShutdown(EntityUid uid, CombatModeComponent component, ComponentShutdown args)
{
if (component.CombatToggleAction != null)
_actionsSystem.RemoveAction(uid, component.CombatToggleAction);
_actionsSystem.RemoveAction(uid, component.CombatToggleActionEntity);
}
private void OnActionPerform(EntityUid uid, CombatModeComponent component, ToggleCombatActionEvent args)
@@ -86,8 +74,8 @@ public abstract class SharedCombatModeSystem : EntitySystem
component.IsInCombatMode = value;
Dirty(entity, component);
if (component.CombatToggleAction != null)
_actionsSystem.SetToggled(component.CombatToggleAction, component.IsInCombatMode);
if (component.CombatToggleActionEntity != null)
_actionsSystem.SetToggled(component.CombatToggleActionEntity, component.IsInCombatMode);
}
public virtual void SetActiveZone(EntityUid entity, TargetingZone zone,

View File

@@ -141,7 +141,8 @@ namespace Content.Shared.Cuffs
private void OnCuffsRemovedFromContainer(EntityUid uid, CuffableComponent component, EntRemovedFromContainerMessage args)
{
if (args.Container.ID != component.Container.ID)
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
if (args.Container.ID != component.Container?.ID)
return;
_handVirtualItem.DeleteInHandsMatching(uid, args.Entity);

View File

@@ -0,0 +1,25 @@
using Content.Shared.Actions;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Decals;
public sealed partial class PlaceDecalActionEvent : WorldTargetActionEvent
{
[DataField("decalId", customTypeSerializer:typeof(PrototypeIdSerializer<DecalPrototype>), required:true)]
public string DecalId = string.Empty;
[DataField("color")]
public Color Color;
[DataField("rotation")]
public double Rotation;
[DataField("snap")]
public bool Snap;
[DataField("zIndex")]
public int ZIndex;
[DataField("cleanable")]
public bool Cleanable;
}

View File

@@ -1,21 +1,22 @@
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Devour;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Devour.Components;
namespace Content.Shared.Devour.Components;
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedDevourSystem))]
public sealed partial class DevourerComponent : Component
{
[DataField("devourAction")]
public EntityTargetAction? DevourAction;
[DataField("devourAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? DevourAction = "ActionDevour";
[DataField("devourActionEntity")]
public EntityUid? DevourActionEntity;
[ViewVariables(VVAccess.ReadWrite), DataField("soundDevour")]
public SoundSpecifier? SoundDevour = new SoundPathSpecifier("/Audio/Effects/demon_consume.ogg")

Some files were not shown because too many files have changed in this diff Show More