Patched Actions Rework (#6899)
* Rejig Actions * fix merge errors * lambda-b-gon * fix PAI, add innate actions * Revert "fix PAI, add innate actions" This reverts commit 4b501ac083e979e31ebd98d7b98077e0dbdd344b. * Just fix by making nullable. if only require: true actually did something somehow. * Make AddActions() ensure an actions component and misc comments * misc cleanup * Limit range even when not checking for obstructions * remove old guardian code * rename function and make EntityUid nullable * fix magboot bug * fix action search menu * make targeting toggle all equivalent actions * fix combat popups (enabling <-> disabling) * fix networking * Allow action locking * prevent telepathy
This commit is contained in:
61
Content.Shared/Actions/ActionEvents.cs
Normal file
61
Content.Shared/Actions/ActionEvents.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions;
|
||||
|
||||
public sealed class GetActionsEvent : EntityEventArgs
|
||||
{
|
||||
public SortedSet<ActionType> Actions = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event used to communicate with the client that the user wishes to perform some action.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Basically a wrapper for <see cref="PerformActionEvent"/> that the action system will validate before performing
|
||||
/// (check cooldown, target, enabling-entity)
|
||||
/// </remarks>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RequestPerformActionEvent : EntityEventArgs
|
||||
{
|
||||
public readonly ActionType Action;
|
||||
public readonly EntityUid? EntityTarget;
|
||||
public readonly MapCoordinates? MapTarget;
|
||||
|
||||
public RequestPerformActionEvent(InstantAction action)
|
||||
{
|
||||
Action = action;
|
||||
}
|
||||
|
||||
public RequestPerformActionEvent(EntityTargetAction action, EntityUid entityTarget)
|
||||
{
|
||||
Action = action;
|
||||
EntityTarget = entityTarget;
|
||||
}
|
||||
|
||||
public RequestPerformActionEvent(WorldTargetAction action, MapCoordinates mapTarget)
|
||||
{
|
||||
Action = action;
|
||||
MapTarget = mapTarget;
|
||||
}
|
||||
}
|
||||
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract class PerformActionEvent : HandledEntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The user performing the action
|
||||
/// </summary>
|
||||
public EntityUid Performer;
|
||||
}
|
||||
|
||||
public abstract class PerformEntityTargetActionEvent : PerformActionEvent
|
||||
{
|
||||
public EntityUid Target;
|
||||
}
|
||||
|
||||
public abstract class PerformWorldTargetActionEvent : PerformActionEvent
|
||||
{
|
||||
public MapCoordinates Target;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to all configured actions by action type.
|
||||
/// </summary>
|
||||
public sealed class ActionManager
|
||||
{
|
||||
[Dependency]
|
||||
private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private readonly Dictionary<ActionType, ActionPrototype> _typeToAction = new();
|
||||
private readonly Dictionary<ItemActionType, ItemActionPrototype> _typeToItemAction = new();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
foreach (var action in _prototypeManager.EnumeratePrototypes<ActionPrototype>())
|
||||
{
|
||||
if (!_typeToAction.TryAdd(action.ActionType, action))
|
||||
{
|
||||
Logger.ErrorS("action",
|
||||
"Found action with duplicate actionType {0} - all actions must have" +
|
||||
" a unique actionType, this one will be skipped", action.ActionType);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var action in _prototypeManager.EnumeratePrototypes<ItemActionPrototype>())
|
||||
{
|
||||
if (!_typeToItemAction.TryAdd(action.ActionType, action))
|
||||
{
|
||||
Logger.ErrorS("action",
|
||||
"Found itemAction with duplicate actionType {0} - all actions must have" +
|
||||
" a unique actionType, this one will be skipped", action.ActionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <returns>all action prototypes of all types</returns>
|
||||
public IEnumerable<BaseActionPrototype> EnumerateActions()
|
||||
{
|
||||
return _typeToAction.Values.Concat<BaseActionPrototype>(_typeToItemAction.Values);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the action of the indicated type
|
||||
/// </summary>
|
||||
/// <returns>true if found</returns>
|
||||
public bool TryGet(ActionType actionType, [NotNullWhen(true)] out ActionPrototype? action)
|
||||
{
|
||||
return _typeToAction.TryGetValue(actionType, out action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the item action of the indicated type
|
||||
/// </summary>
|
||||
/// <returns>true if found</returns>
|
||||
public bool TryGet(ItemActionType actionType, [NotNullWhen(true)] out ItemActionPrototype? action)
|
||||
{
|
||||
return _typeToItemAction.TryGetValue(actionType, out action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Every possible action. Corresponds to actionType in action prototypes.
|
||||
/// </summary>
|
||||
public enum ActionType : byte
|
||||
{
|
||||
Error,
|
||||
HumanScream,
|
||||
VoxScream,
|
||||
CombatMode,
|
||||
Disarm,
|
||||
GhostBoo,
|
||||
DebugInstant,
|
||||
DebugToggle,
|
||||
DebugTargetPoint,
|
||||
DebugTargetPointRepeat,
|
||||
DebugTargetEntity,
|
||||
DebugTargetEntityRepeat,
|
||||
SpellPie,
|
||||
ManifestGuardian,
|
||||
PAIMidi
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every possible item action. Corresponds to actionType in itemAction prototypes.
|
||||
/// </summary>
|
||||
public enum ItemActionType : byte
|
||||
{
|
||||
Error,
|
||||
ToggleInternals,
|
||||
ToggleLight,
|
||||
ToggleMagboots,
|
||||
DebugInstant,
|
||||
DebugToggle,
|
||||
DebugTargetPoint,
|
||||
DebugTargetPointRepeat,
|
||||
DebugTargetEntity,
|
||||
DebugTargetEntityRepeat
|
||||
}
|
||||
}
|
||||
276
Content.Shared/Actions/ActionTypes/ActionType.cs
Normal file
276
Content.Shared/Actions/ActionTypes/ActionType.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Actions.ActionTypes;
|
||||
|
||||
[DataDefinition]
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
[Serializable, NetSerializable]
|
||||
public abstract 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", required: true)]
|
||||
public string Name = string.Empty;
|
||||
|
||||
/// <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>
|
||||
/// 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;
|
||||
|
||||
/// <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, the user will speak these words when performing the action. Convenient feature to have for some
|
||||
/// actions. Gets passed through localization.
|
||||
/// </summary>
|
||||
[DataField("speech")]
|
||||
public string? Speech;
|
||||
|
||||
/// <summary>
|
||||
/// If not null, this sound will be played when performing this action.
|
||||
/// </summary>
|
||||
[DataField("sound")]
|
||||
public SoundSpecifier? Sound;
|
||||
|
||||
[DataField("audioParams")]
|
||||
public AudioParams? AudioParams;
|
||||
|
||||
/// <summary>
|
||||
/// A pop-up to show the user when performing this action. Gets passed through localization.
|
||||
/// </summary>
|
||||
[DataField("userPopup")]
|
||||
public string? UserPopup;
|
||||
|
||||
/// <summary>
|
||||
/// A pop-up to show to all players when performing this action. Gets passed through localization.
|
||||
/// </summary>
|
||||
[DataField("popup")]
|
||||
public string? Popup;
|
||||
|
||||
/// <summary>
|
||||
/// If not null, this string will be appended to the pop-up localization strings when the action was toggled on
|
||||
/// after execution. Exists to make it easy to have a different pop-up for turning the action on or off (e.g.,
|
||||
/// combat mode toggle).
|
||||
/// </summary>
|
||||
[DataField("popupToggleSuffix")]
|
||||
public string? PopupToggleSuffix = null;
|
||||
|
||||
/// <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;
|
||||
|
||||
var name = FormattedMessage.RemoveMarkup(Loc.GetString(Name));
|
||||
var otherName = FormattedMessage.RemoveMarkup(Loc.GetString(otherAction.Name));
|
||||
if (name != otherName)
|
||||
return string.Compare(name, otherName, StringComparison.CurrentCulture);
|
||||
|
||||
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;
|
||||
Name = toClone.Name;
|
||||
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;
|
||||
Speech = toClone.Speech;
|
||||
UseDelay = toClone.UseDelay;
|
||||
Sound = toClone.Sound;
|
||||
AudioParams = toClone.AudioParams;
|
||||
UserPopup = toClone.UserPopup;
|
||||
Popup = toClone.Popup;
|
||||
PopupToggleSuffix = toClone.PopupToggleSuffix;
|
||||
ItemIconStyle = toClone.ItemIconStyle;
|
||||
}
|
||||
|
||||
public bool Equals(ActionType? other)
|
||||
{
|
||||
return CompareTo(other) == 0;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hashCode = Priority.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ Name.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ Provider.GetHashCode();
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Content.Shared/Actions/ActionTypes/InstantAction.cs
Normal file
41
Content.Shared/Actions/ActionTypes/InstantAction.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions.ActionTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Instantaneous action with no extra targeting information. Will result in <see cref="PerformActionEvent"/> being raised.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[Friend(typeof(SharedActionsSystem))]
|
||||
[Virtual]
|
||||
public class InstantAction : ActionType
|
||||
{
|
||||
/// <summary>
|
||||
/// The local-event to raise when this action is performed.
|
||||
/// </summary>
|
||||
[DataField("event")]
|
||||
[NonSerialized]
|
||||
public PerformActionEvent? Event;
|
||||
|
||||
public InstantAction() { }
|
||||
public InstantAction(InstantAction toClone)
|
||||
{
|
||||
CopyFrom(toClone);
|
||||
}
|
||||
|
||||
public override void CopyFrom(object objectToClone)
|
||||
{
|
||||
base.CopyFrom(objectToClone);
|
||||
|
||||
if (objectToClone is not InstantAction toClone)
|
||||
return;
|
||||
|
||||
// Events should be re-usable, and shouldn't be modified during prediction.
|
||||
Event = toClone.Event;
|
||||
}
|
||||
|
||||
public override object Clone()
|
||||
{
|
||||
return new InstantAction(this);
|
||||
}
|
||||
}
|
||||
153
Content.Shared/Actions/ActionTypes/TargetedAction.cs
Normal file
153
Content.Shared/Actions/ActionTypes/TargetedAction.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions.ActionTypes;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public abstract 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="PerformEntityTargetActionEvent"/> being raised.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[Friend(typeof(SharedActionsSystem))]
|
||||
[Virtual]
|
||||
public class EntityTargetAction : TargetedAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The local-event to raise when this action is performed.
|
||||
/// </summary>
|
||||
[NonSerialized]
|
||||
[DataField("event")]
|
||||
public PerformEntityTargetActionEvent? 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.
|
||||
Event = toClone.Event;
|
||||
}
|
||||
|
||||
public override object Clone()
|
||||
{
|
||||
return new EntityTargetAction(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action that targets some map coordinates. Will result in <see cref="PerformWorldTargetActionEvent"/> being raised.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[Friend(typeof(SharedActionsSystem))]
|
||||
[Virtual]
|
||||
public class WorldTargetAction : TargetedAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The local-event to raise when this action is performed.
|
||||
/// </summary>
|
||||
[DataField("event")]
|
||||
[NonSerialized]
|
||||
public PerformWorldTargetActionEvent? 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.
|
||||
Event = toClone.Event;
|
||||
}
|
||||
|
||||
public override object Clone()
|
||||
{
|
||||
return new WorldTargetAction(this);
|
||||
}
|
||||
}
|
||||
46
Content.Shared/Actions/ActionsComponent.cs
Normal file
46
Content.Shared/Actions/ActionsComponent.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions;
|
||||
|
||||
[NetworkedComponent]
|
||||
[RegisterComponent]
|
||||
[Friend(typeof(SharedActionsSystem))]
|
||||
public sealed class ActionsComponent : Component
|
||||
{
|
||||
[ViewVariables]
|
||||
public SortedSet<ActionType> Actions = new();
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ActionsComponentState : ComponentState
|
||||
{
|
||||
public readonly List<ActionType> Actions;
|
||||
|
||||
public ActionsComponentState(List<ActionType> actions)
|
||||
{
|
||||
Actions = actions;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines how the action icon appears in the hotbar for item actions.
|
||||
/// </summary>
|
||||
public enum ItemActionIconStyle : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// The default - The item icon will be big with a small action icon in the corner
|
||||
/// </summary>
|
||||
BigItem,
|
||||
|
||||
/// <summary>
|
||||
/// The action icon will be big with a small item icon in the corner
|
||||
/// </summary>
|
||||
BigAction,
|
||||
|
||||
/// <summary>
|
||||
/// BigAction but no item icon will be shown in the corner.
|
||||
/// </summary>
|
||||
NoItem
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using System;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Currently just a marker interface delineating the different possible
|
||||
/// types of action behaviors.
|
||||
/// </summary>
|
||||
public interface IActionBehavior
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all action event args
|
||||
/// </summary>
|
||||
public abstract class ActionEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity performing the action.
|
||||
/// </summary>
|
||||
public readonly EntityUid Performer;
|
||||
/// <summary>
|
||||
/// Action being performed
|
||||
/// </summary>
|
||||
public readonly ActionType ActionType;
|
||||
/// <summary>
|
||||
/// Actions component of the performer.
|
||||
/// </summary>
|
||||
public readonly SharedActionsComponent? PerformerActions;
|
||||
|
||||
public ActionEventArgs(EntityUid performer, ActionType actionType)
|
||||
{
|
||||
Performer = performer;
|
||||
ActionType = actionType;
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(Performer, out PerformerActions))
|
||||
{
|
||||
throw new InvalidOperationException($"performer {IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(performer).EntityName} tried to perform action {actionType} " +
|
||||
$" but the performer had no actions component," +
|
||||
" which should never occur");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which does something immediately when used and has
|
||||
/// no target.
|
||||
/// </summary>
|
||||
public interface IInstantAction : IActionBehavior
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the instant action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoInstantAction(InstantActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class InstantActionEventArgs : ActionEventArgs
|
||||
{
|
||||
public InstantActionEventArgs(EntityUid performer, ActionType actionType) : base(performer, actionType)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which is used on a targeted entity.
|
||||
/// </summary>
|
||||
public interface ITargetEntityAction : IActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target entity action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetEntityAction(TargetEntityActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class TargetEntityActionEventArgs : ActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity being targeted
|
||||
/// </summary>
|
||||
public readonly EntityUid Target;
|
||||
|
||||
public TargetEntityActionEventArgs(EntityUid performer, ActionType actionType, EntityUid target) :
|
||||
base(performer, actionType)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which is used on a targeted entity.
|
||||
/// </summary>
|
||||
public interface ITargetEntityItemAction : IItemActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target entity action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetEntityAction(TargetEntityItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class TargetEntityItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity being targeted
|
||||
/// </summary>
|
||||
public readonly EntityUid Target;
|
||||
|
||||
public TargetEntityItemActionEventArgs(EntityUid performer, EntityUid target, EntityUid item,
|
||||
ItemActionType actionType) : base(performer, item, actionType)
|
||||
{
|
||||
Target = target;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which requires the user to select a target point, which
|
||||
/// does not necessarily have an entity on it.
|
||||
/// </summary>
|
||||
public interface ITargetPointAction : IActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target point action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetPointAction(TargetPointActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class TargetPointActionEventArgs : ActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Local coordinates of the targeted position.
|
||||
/// </summary>
|
||||
public readonly EntityCoordinates Target;
|
||||
|
||||
public TargetPointActionEventArgs(EntityUid performer, EntityCoordinates target, ActionType actionType)
|
||||
: base(performer, actionType)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which requires the user to select a target point, which
|
||||
/// does not necessarily have an entity on it.
|
||||
/// </summary>
|
||||
public interface ITargetPointItemAction : IItemActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target point action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetPointAction(TargetPointItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class TargetPointItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Local coordinates of the targeted position.
|
||||
/// </summary>
|
||||
public readonly EntityCoordinates Target;
|
||||
|
||||
public TargetPointItemActionEventArgs(EntityUid performer, EntityCoordinates target, EntityUid item,
|
||||
ItemActionType actionType) : base(performer, item, actionType)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which can be toggled on and off
|
||||
/// </summary>
|
||||
public interface IToggleAction : IActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the action will be toggled on/off.
|
||||
/// Implementation should perform the server side logic of whatever
|
||||
/// happens when it is toggled on / off.
|
||||
/// </summary>
|
||||
/// <returns>true if the attempt to toggle was successful, meaning the state should be toggled to the desired value.
|
||||
/// False to leave toggle status unchanged. This is NOT returning the new toggle status, it is only returning
|
||||
/// whether the attempt to toggle to the indicated status was successful.
|
||||
///
|
||||
/// Note that it's still okay if the implementation directly modifies toggle status via SharedActionsComponent,
|
||||
/// this is just an additional level of safety to ensure implementations will always
|
||||
/// explicitly indicate if the toggle status should be changed.</returns>
|
||||
bool DoToggleAction(ToggleActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class ToggleActionEventArgs : ActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// True if the toggle is attempting to be toggled on, false if attempting to toggle off
|
||||
/// </summary>
|
||||
public readonly bool ToggledOn;
|
||||
/// <summary>
|
||||
/// Opposite of ToggledOn
|
||||
/// </summary>
|
||||
public bool ToggledOff => !ToggledOn;
|
||||
|
||||
public ToggleActionEventArgs(EntityUid performer, ActionType actionType, bool toggledOn) : base(performer, actionType)
|
||||
{
|
||||
ToggledOn = toggledOn;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors.Item
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which does something immediately when used and has
|
||||
/// no target.
|
||||
/// </summary>
|
||||
public interface IInstantItemAction : IItemActionBehavior
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the instant action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoInstantAction(InstantItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class InstantItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
public InstantItemActionEventArgs(EntityUid performer, EntityUid item, ItemActionType actionType) :
|
||||
base(performer, item, actionType)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors.Item
|
||||
{
|
||||
/// <summary>
|
||||
/// Currently just a marker interface delineating the different possible
|
||||
/// types of item action behaviors.
|
||||
/// </summary>
|
||||
public interface IItemActionBehavior
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all item action event args
|
||||
/// </summary>
|
||||
public abstract class ItemActionEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity performing the action.
|
||||
/// </summary>
|
||||
public readonly EntityUid Performer;
|
||||
/// <summary>
|
||||
/// Item being used to perform the action
|
||||
/// </summary>
|
||||
public readonly EntityUid Item;
|
||||
/// <summary>
|
||||
/// Action being performed
|
||||
/// </summary>
|
||||
public readonly ItemActionType ActionType;
|
||||
/// <summary>
|
||||
/// Item actions component of the item.
|
||||
/// </summary>
|
||||
public readonly ItemActionsComponent? ItemActions;
|
||||
|
||||
public ItemActionEventArgs(EntityUid performer, EntityUid item, ItemActionType actionType)
|
||||
{
|
||||
Performer = performer;
|
||||
ActionType = actionType;
|
||||
Item = item;
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
if (!entMan.TryGetComponent(Item, out ItemActions))
|
||||
{
|
||||
throw new InvalidOperationException($"performer {entMan.GetComponent<MetaDataComponent>(performer).EntityName} tried to perform item action {actionType} " +
|
||||
$" for item {entMan.GetComponent<MetaDataComponent>(Item).EntityName} but the item had no ItemActionsComponent," +
|
||||
" which should never occur");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors.Item
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which can be toggled on and off
|
||||
/// </summary>
|
||||
public interface IToggleItemAction : IItemActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the action will be toggled on/off.
|
||||
/// Implementation should perform the server side logic of whatever
|
||||
/// happens when it is toggled on / off.
|
||||
/// </summary>
|
||||
/// <returns>true if the attempt to toggle was successful, meaning the state should be toggled to the desired value.
|
||||
/// False to leave toggle status unchanged. This is NOT returning the new toggle status, it is only returning
|
||||
/// whether the attempt to toggle to the indicated status was successful.
|
||||
///
|
||||
/// Note that it's still okay if the implementation directly modifies toggle status via ItemActionsComponent,
|
||||
/// this is just an additional level of safety to ensure implementations will always
|
||||
/// explicitly indicate if the toggle status should be changed.</returns>
|
||||
bool DoToggleAction(ToggleItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class ToggleItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// True if the toggle was toggled on, false if it was toggled off
|
||||
/// </summary>
|
||||
public readonly bool ToggledOn;
|
||||
/// <summary>
|
||||
/// Opposite of ToggledOn
|
||||
/// </summary>
|
||||
public bool ToggledOff => !ToggledOn;
|
||||
|
||||
public ToggleItemActionEventArgs(EntityUid performer, bool toggledOn, EntityUid item,
|
||||
ItemActionType actionType) : base(performer, item, actionType)
|
||||
{
|
||||
ToggledOn = toggledOn;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Shared.Actions.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// This should be used on items which provide actions. Defines which actions the item provides
|
||||
/// and allows modifying the states of those actions. Item components should use this rather than
|
||||
/// SharedActionsComponent on the player to handle granting / revoking / modifying the states of the
|
||||
/// actions provided by this item.
|
||||
///
|
||||
/// When a player equips this item, all the actions defined in this component will be granted to the
|
||||
/// player in their current states. This means the states will persist between players.
|
||||
///
|
||||
/// Currently only maintained server side and not synced to client, as are all the equip/unequip events.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class ItemActionsComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for the item actions initially provided by this item. Actions defined here
|
||||
/// will be automatically granted unless their state is modified using the methods
|
||||
/// on this component. Additional actions can be granted by this item via GrantOrUpdate
|
||||
/// </summary>
|
||||
public IEnumerable<ItemActionConfig> ActionConfigs => _actionConfigs;
|
||||
|
||||
public bool IsEquipped;
|
||||
|
||||
/// <summary>
|
||||
/// hand it's currently in, null if not in a hand.
|
||||
/// </summary>
|
||||
public Hand? InHand;
|
||||
|
||||
/// <summary>
|
||||
/// Entity currently holding this in hand or equip slot. Null if not held.
|
||||
/// </summary>
|
||||
public EntityUid? Holder;
|
||||
// cached actions component of the holder, since we'll need to access it frequently
|
||||
public SharedActionsComponent? HolderActionsComponent;
|
||||
|
||||
[DataField("actions")]
|
||||
private List<ItemActionConfig> _actionConfigs
|
||||
{
|
||||
get => internalActionConfigs;
|
||||
set
|
||||
{
|
||||
internalActionConfigs = value;
|
||||
foreach (var actionConfig in value)
|
||||
{
|
||||
GrantOrUpdate(actionConfig.ActionType, actionConfig.Enabled, false, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// State of all actions provided by this item.
|
||||
private readonly Dictionary<ItemActionType, ActionState> _actions = new();
|
||||
private List<ItemActionConfig> internalActionConfigs = new ();
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
GrantOrUpdateAllToHolder();
|
||||
}
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
RevokeAllFromHolder();
|
||||
}
|
||||
|
||||
public void GrantOrUpdateAllToHolder()
|
||||
{
|
||||
if (HolderActionsComponent == null) return;
|
||||
foreach (var (actionType, state) in _actions)
|
||||
{
|
||||
HolderActionsComponent.GrantOrUpdateItemAction(actionType, Owner, state);
|
||||
}
|
||||
}
|
||||
|
||||
public void RevokeAllFromHolder()
|
||||
{
|
||||
if (HolderActionsComponent == null) return;
|
||||
foreach (var (actionType, state) in _actions)
|
||||
{
|
||||
HolderActionsComponent.RevokeItemAction(actionType, Owner);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the state of the action, granting it if it isn't already granted.
|
||||
/// If the action had any existing state, those specific fields will be overwritten by any
|
||||
/// corresponding non-null arguments.
|
||||
/// </summary>
|
||||
/// <param name="actionType">action being granted / updated</param>
|
||||
/// <param name="enabled">When null, preserves the current enable status of the action, defaulting
|
||||
/// to true if action has no current state.
|
||||
/// When non-null, indicates whether the entity is able to perform the action (if disabled,
|
||||
/// the player will see they have the action but it will appear greyed out)</param>
|
||||
/// <param name="toggleOn">When null, preserves the current toggle status of the action, defaulting
|
||||
/// to false if action has no current state.
|
||||
/// When non-null, action will be shown toggled to this value</param>
|
||||
/// <param name="cooldown"> When null (unless clearCooldown is true), preserves the current cooldown status of the action, defaulting
|
||||
/// to no cooldown if action has no current state.
|
||||
/// When non-null or clearCooldown is true, action cooldown will be set to this value. Note that this cooldown
|
||||
/// is tied to this item.</param>
|
||||
/// <param name="clearCooldown"> If true, setting cooldown to null will clear the current cooldown
|
||||
/// of this action rather than preserving it.</param>
|
||||
public void GrantOrUpdate(ItemActionType actionType, bool? enabled = null,
|
||||
bool? toggleOn = null,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null, bool clearCooldown = false)
|
||||
{
|
||||
var dirty = false;
|
||||
// this will be overwritten if we find the value in our dict, otherwise
|
||||
// we will use this as our new action state.
|
||||
if (!_actions.TryGetValue(actionType, out var actionState))
|
||||
{
|
||||
dirty = true;
|
||||
actionState = new ActionState(enabled ?? true, toggleOn ?? false);
|
||||
}
|
||||
|
||||
if (enabled.HasValue && enabled != actionState.Enabled)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Enabled = true;
|
||||
}
|
||||
|
||||
if ((cooldown.HasValue || clearCooldown) && actionState.Cooldown != cooldown)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Cooldown = cooldown;
|
||||
}
|
||||
|
||||
if (toggleOn.HasValue && actionState.ToggledOn != toggleOn.Value)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.ToggledOn = toggleOn.Value;
|
||||
}
|
||||
|
||||
if (!dirty) return;
|
||||
|
||||
_actions[actionType] = actionState;
|
||||
HolderActionsComponent?.GrantOrUpdateItemAction(actionType, Owner, actionState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the cooldown of a particular action. Actions on cooldown cannot be used.
|
||||
/// Setting the cooldown to null clears it.
|
||||
/// </summary>
|
||||
public void Cooldown(ItemActionType actionType, (TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
GrantOrUpdate(actionType, cooldown: cooldown, clearCooldown: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable / disable this action. Disabled actions are still shown to the player, but
|
||||
/// shown as not usable.
|
||||
/// </summary>
|
||||
public void SetEnabled(ItemActionType actionType, bool enabled)
|
||||
{
|
||||
GrantOrUpdate(actionType, enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the action on / off
|
||||
/// </summary>
|
||||
public void Toggle(ItemActionType actionType, bool toggleOn)
|
||||
{
|
||||
GrantOrUpdate(actionType, toggleOn: toggleOn);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for an item action provided by an item.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public sealed class ItemActionConfig : ISerializationHooks
|
||||
{
|
||||
[DataField("actionType", required: true)]
|
||||
public ItemActionType ActionType { get; private set; } = ItemActionType.Error;
|
||||
|
||||
/// <summary>
|
||||
/// Whether action is initially enabled on this item. Defaults to true.
|
||||
/// </summary>
|
||||
public bool Enabled { get; private set; } = true;
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
if (ActionType == ItemActionType.Error)
|
||||
{
|
||||
Logger.ErrorS("action", "invalid or missing actionType");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,641 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Actions.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the actions available to an entity.
|
||||
/// Should only be used for player-controlled entities.
|
||||
///
|
||||
/// Actions are granted directly to the owner entity. Item actions are granted via a particular item which
|
||||
/// must be in the owner's inventory (the action is revoked when item leaves the owner's inventory). This
|
||||
/// should almost always be done via ItemActionsComponent on the item entity (which also tracks the
|
||||
/// cooldowns associated with the actions on that item).
|
||||
///
|
||||
/// Actions can still have an associated state even when revoked. For example, a flashlight toggle action
|
||||
/// may be unusable while the player is stunned, but this component will still have an entry for the action
|
||||
/// so the user can see whether it's currently toggled on or off.
|
||||
/// </summary>
|
||||
[NetworkedComponent()]
|
||||
public abstract class SharedActionsComponent : Component
|
||||
{
|
||||
private static readonly TimeSpan CooldownExpiryThreshold = TimeSpan.FromSeconds(10);
|
||||
|
||||
[Dependency]
|
||||
protected readonly ActionManager ActionManager = default!;
|
||||
[Dependency]
|
||||
protected readonly IGameTiming GameTiming = default!;
|
||||
[Dependency]
|
||||
protected readonly IEntityManager EntityManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Actions granted to this entity as soon as they spawn, regardless
|
||||
/// of the status of the entity.
|
||||
/// </summary>
|
||||
public IEnumerable<ActionType> InnateActions => _innateActions ?? Enumerable.Empty<ActionType>();
|
||||
[DataField("innateActions")]
|
||||
private List<ActionType>? _innateActions = null;
|
||||
|
||||
|
||||
// entries are removed from this if they are at the initial state (not enabled, no cooldown, toggled off).
|
||||
// a system runs which periodically removes cooldowns from entries when they are revoked and their
|
||||
// cooldowns have expired for a long enough time, also removing the entry if it is then at initial state.
|
||||
// This helps to keep our component state smaller.
|
||||
[ViewVariables]
|
||||
private Dictionary<ActionType, ActionState> _actions = new();
|
||||
|
||||
// Holds item action states. Item actions are only added to this when granted, and are removed
|
||||
// when revoked or when they leave inventory. This is almost entirely handled by ItemActionsComponent on
|
||||
// item entities.
|
||||
[ViewVariables]
|
||||
private Dictionary<EntityUid, Dictionary<ItemActionType, ActionState>> _itemActions =
|
||||
new();
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
foreach (var actionType in InnateActions)
|
||||
{
|
||||
Grant(actionType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new ActionComponentState(_actions, _itemActions);
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (curState is not ActionComponentState state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_actions = state.Actions;
|
||||
_itemActions = state.ItemActions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the action state associated with the specified action type, if it has been
|
||||
/// granted, has a cooldown, or has been toggled on
|
||||
/// </summary>
|
||||
/// <returns>false if not found for this action type</returns>
|
||||
public bool TryGetActionState(ActionType actionType, out ActionState actionState)
|
||||
{
|
||||
return _actions.TryGetValue(actionType, out actionState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item action states associated with the specified item if any have been granted
|
||||
/// and not yet revoked.
|
||||
/// </summary>
|
||||
/// <returns>false if no states found for this item action type.</returns>
|
||||
public bool TryGetItemActionStates(EntityUid item, [NotNullWhen((true))] out IReadOnlyDictionary<ItemActionType, ActionState>? itemActionStates)
|
||||
{
|
||||
if (_itemActions.TryGetValue(item, out var actualItemActionStates))
|
||||
{
|
||||
itemActionStates = actualItemActionStates;
|
||||
return true;
|
||||
}
|
||||
|
||||
itemActionStates = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item action state associated with the specified item action type for the specified item, if it has any.
|
||||
/// </summary>
|
||||
/// <returns>false if no state found for this item action type for this item</returns>
|
||||
public bool TryGetItemActionState(ItemActionType actionType, EntityUid item, out ActionState actionState)
|
||||
{
|
||||
if (_itemActions.TryGetValue(item, out var actualItemActionStates))
|
||||
{
|
||||
return actualItemActionStates.TryGetValue(actionType, out actionState);
|
||||
}
|
||||
|
||||
actionState = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <returns>true if the action is granted and enabled (if item action, if granted and enabled for any item)</returns>
|
||||
public bool IsGranted(BaseActionPrototype actionType)
|
||||
{
|
||||
return actionType switch
|
||||
{
|
||||
ActionPrototype actionPrototype => IsGranted(actionPrototype.ActionType),
|
||||
ItemActionPrototype itemActionPrototype => IsGranted(itemActionPrototype.ActionType),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public bool IsGranted(ActionType actionType)
|
||||
{
|
||||
if (TryGetActionState(actionType, out var actionState))
|
||||
{
|
||||
return actionState.Enabled;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <returns>true if the action is granted and enabled for any item. This
|
||||
/// has to traverse the entire item state dictionary so please avoid frequent calls.</returns>
|
||||
public bool IsGranted(ItemActionType actionType)
|
||||
{
|
||||
return _itemActions.Values.SelectMany(vals => vals)
|
||||
.Any(state => state.Key == actionType && state.Value.Enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all action types that have non-initial state (granted, have a cooldown, or toggled on).
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<ActionType, ActionState> ActionStates()
|
||||
{
|
||||
return _actions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all items that have actions currently granted (that are not revoked
|
||||
/// and still in inventory).
|
||||
/// Map from item uid -> (action type -> associated action state)
|
||||
/// PLEASE DO NOT MODIFY THE INNER DICTIONARY! I CANNOT CAST IT TO IReadOnlyDictionary!
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<EntityUid,Dictionary<ItemActionType, ActionState>> ItemActionStates()
|
||||
{
|
||||
return _itemActions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates the action state with the supplied non-null values
|
||||
/// </summary>
|
||||
private void GrantOrUpdate(ActionType actionType, bool? enabled = null, bool? toggleOn = null,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null, bool clearCooldown = false)
|
||||
{
|
||||
var dirty = false;
|
||||
if (!_actions.TryGetValue(actionType, out var actionState))
|
||||
{
|
||||
// no state at all for this action, create it anew
|
||||
dirty = true;
|
||||
actionState = new ActionState(enabled ?? false, toggleOn ?? false);
|
||||
}
|
||||
|
||||
if (enabled.HasValue && actionState.Enabled != enabled.Value)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Enabled = enabled.Value;
|
||||
}
|
||||
|
||||
if ((cooldown.HasValue || clearCooldown) && actionState.Cooldown != cooldown)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Cooldown = cooldown;
|
||||
}
|
||||
|
||||
if (toggleOn.HasValue && actionState.ToggledOn != toggleOn.Value)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.ToggledOn = toggleOn.Value;
|
||||
}
|
||||
|
||||
if (!dirty) return;
|
||||
|
||||
_actions[actionType] = actionState;
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended to only be used by ItemActionsComponent.
|
||||
/// Updates the state of the item action provided by the item, granting the action
|
||||
/// if it is not yet granted to the player. Should be called whenever the
|
||||
/// status changes. The existing state will be completely overwritten by the new state.
|
||||
/// </summary>
|
||||
public void GrantOrUpdateItemAction(ItemActionType actionType, EntityUid item, ActionState state)
|
||||
{
|
||||
if (!_itemActions.TryGetValue(item, out var itemStates))
|
||||
{
|
||||
itemStates = new Dictionary<ItemActionType, ActionState>();
|
||||
_itemActions[item] = itemStates;
|
||||
}
|
||||
|
||||
itemStates[actionType] = state;
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended to only be used by ItemActionsComponent. Revokes the item action so the player no longer
|
||||
/// sees it and can no longer use it.
|
||||
/// </summary>
|
||||
public void RevokeItemAction(ItemActionType actionType, EntityUid item)
|
||||
{
|
||||
if (!_itemActions.TryGetValue(item, out var itemStates))
|
||||
return;
|
||||
|
||||
itemStates.Remove(actionType);
|
||||
if (itemStates.Count == 0)
|
||||
{
|
||||
_itemActions.Remove(item);
|
||||
}
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grants the entity the ability to perform the action, optionally overriding its
|
||||
/// current state with specified values.
|
||||
///
|
||||
/// Even if the action was already granted, if the action had any state (cooldown, toggle) prior to this method
|
||||
/// being called, it will be preserved, with specific fields optionally overridden by any of the provided
|
||||
/// non-null arguments.
|
||||
/// </summary>
|
||||
/// <param name="toggleOn">When null, preserves the current toggle status of the action, defaulting
|
||||
/// to false if action has no current state.
|
||||
/// When non-null, action will be shown toggled to this value</param>
|
||||
/// <param name="cooldown"> When null, preserves the current cooldown status of the action, defaulting
|
||||
/// to no cooldown if action has no current state.
|
||||
/// When non-null, action cooldown will be set to this value.</param>
|
||||
public void Grant(ActionType actionType, bool? toggleOn = null,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
GrantOrUpdate(actionType, true, toggleOn, cooldown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grants the entity the ability to perform the action, resetting its state
|
||||
/// to its initial state and settings its state based on supplied parameters.
|
||||
///
|
||||
/// Even if the action was already granted, if the action had any state (cooldown, toggle) prior to this method
|
||||
/// being called, it will be reset to initial (no cooldown, toggled off).
|
||||
/// </summary>
|
||||
/// <param name="toggleOn">action will be shown toggled to this value</param>
|
||||
/// <param name="cooldown">action cooldown will be set to this value (by default the cooldown is cleared).</param>
|
||||
public void GrantFromInitialState(ActionType actionType, bool toggleOn = false,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
_actions.Remove(actionType);
|
||||
Grant(actionType, toggleOn, cooldown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cooldown for the action. Actions on cooldown cannot be used.
|
||||
///
|
||||
/// This will work even if the action is revoked -
|
||||
/// for example if there's an ability with a cooldown which is temporarily unusable due
|
||||
/// to the player being stunned, the cooldown will still tick down even while the player
|
||||
/// is stunned.
|
||||
///
|
||||
/// Setting cooldown to null clears it.
|
||||
/// </summary>
|
||||
public void Cooldown(ActionType actionType, (TimeSpan start, TimeSpan end)? cooldown)
|
||||
{
|
||||
GrantOrUpdate(actionType, cooldown: cooldown, clearCooldown: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revokes the ability to perform the action for this entity. Current state
|
||||
/// of the action (toggle / cooldown) is preserved.
|
||||
/// </summary>
|
||||
public void Revoke(ActionType actionType)
|
||||
{
|
||||
if (!_actions.TryGetValue(actionType, out var actionState)) return;
|
||||
|
||||
if (!actionState.Enabled) return;
|
||||
|
||||
actionState.Enabled = false;
|
||||
|
||||
// don't store it anymore if its at its initial state.
|
||||
if (actionState.IsAtInitialState)
|
||||
{
|
||||
_actions.Remove(actionType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_actions[actionType] = actionState;
|
||||
}
|
||||
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the action to the specified value. Works even if the action is on cooldown
|
||||
/// or revoked.
|
||||
/// </summary>
|
||||
public void ToggleAction(ActionType actionType, bool toggleOn)
|
||||
{
|
||||
Grant(actionType, toggleOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears any cooldowns which have expired beyond the predefined threshold.
|
||||
/// this should be run periodically to ensure we don't have unbounded growth of
|
||||
/// our saved action data, and keep our component state sent to the client as minimal as possible.
|
||||
/// </summary>
|
||||
public void ExpireCooldowns()
|
||||
{
|
||||
|
||||
// actions - only clear cooldowns and remove associated action state
|
||||
// if the action is at initial state
|
||||
var actionTypesToRemove = new List<ActionType>();
|
||||
foreach (var (actionType, actionState) in _actions)
|
||||
{
|
||||
// ignore it unless we may be able to delete it due to
|
||||
// clearing the cooldown
|
||||
if (!actionState.IsAtInitialStateExceptCooldown) continue;
|
||||
if (!actionState.Cooldown.HasValue)
|
||||
{
|
||||
actionTypesToRemove.Add(actionType);
|
||||
continue;
|
||||
}
|
||||
var expiryTime = GameTiming.CurTime - actionState.Cooldown.Value.Item2;
|
||||
if (expiryTime > CooldownExpiryThreshold)
|
||||
{
|
||||
actionTypesToRemove.Add(actionType);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var remove in actionTypesToRemove)
|
||||
{
|
||||
_actions.Remove(remove);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after a change has been made to an action state in this component.
|
||||
/// </summary>
|
||||
protected virtual void AfterActionChanged() { }
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ActionComponentState : ComponentState
|
||||
{
|
||||
public Dictionary<ActionType, ActionState> Actions;
|
||||
public Dictionary<EntityUid, Dictionary<ItemActionType, ActionState>> ItemActions;
|
||||
|
||||
public ActionComponentState(Dictionary<ActionType, ActionState> actions,
|
||||
Dictionary<EntityUid, Dictionary<ItemActionType, ActionState>> itemActions)
|
||||
{
|
||||
Actions = actions;
|
||||
ItemActions = itemActions;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public struct ActionState
|
||||
{
|
||||
/// <summary>
|
||||
/// False if this action is not currently allowed to be performed.
|
||||
/// </summary>
|
||||
public bool Enabled;
|
||||
/// <summary>
|
||||
/// Only used for toggle actions, indicates whether it's currently toggled on or off
|
||||
/// TODO: Eventually this should probably be a byte so we it can toggle through multiple states.
|
||||
/// </summary>
|
||||
public bool ToggledOn;
|
||||
public (TimeSpan start, TimeSpan end)? Cooldown;
|
||||
public bool IsAtInitialState => IsAtInitialStateExceptCooldown && !Cooldown.HasValue;
|
||||
public bool IsAtInitialStateExceptCooldown => !Enabled && !ToggledOn;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an action state for the indicated type, defaulting to the
|
||||
/// initial state.
|
||||
/// </summary>
|
||||
public ActionState(bool enabled = false, bool toggledOn = false, (TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
Enabled = enabled;
|
||||
ToggledOn = toggledOn;
|
||||
Cooldown = cooldown;
|
||||
}
|
||||
|
||||
public bool IsOnCooldown(TimeSpan curTime)
|
||||
{
|
||||
if (Cooldown == null) return false;
|
||||
return curTime < Cooldown.Value.Item2;
|
||||
}
|
||||
public bool IsOnCooldown(IGameTiming gameTiming)
|
||||
{
|
||||
return IsOnCooldown(gameTiming.CurTime);
|
||||
}
|
||||
|
||||
public bool Equals(ActionState other)
|
||||
{
|
||||
return Enabled == other.Enabled && ToggledOn == other.ToggledOn && Nullable.Equals(Cooldown, other.Cooldown);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ActionState other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Enabled, ToggledOn, Cooldown);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
#pragma warning disable 618
|
||||
public abstract class BasePerformActionMessage : ComponentMessage
|
||||
#pragma warning restore 618
|
||||
{
|
||||
public abstract BehaviorType BehaviorType { get; }
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public abstract class PerformActionMessage : BasePerformActionMessage
|
||||
{
|
||||
public readonly ActionType ActionType;
|
||||
|
||||
protected PerformActionMessage(ActionType actionType)
|
||||
{
|
||||
Directed = true;
|
||||
ActionType = actionType;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public abstract class PerformItemActionMessage : BasePerformActionMessage
|
||||
{
|
||||
public readonly ItemActionType ActionType;
|
||||
public readonly EntityUid Item;
|
||||
|
||||
protected PerformItemActionMessage(ItemActionType actionType, EntityUid item)
|
||||
{
|
||||
Directed = true;
|
||||
ActionType = actionType;
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to run the instant action logic.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformInstantActionMessage : PerformActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Instant;
|
||||
|
||||
public PerformInstantActionMessage(ActionType actionType) : base(actionType)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to run the instant action logic.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformInstantItemActionMessage : PerformItemActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Instant;
|
||||
|
||||
public PerformInstantItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public interface IToggleActionMessage
|
||||
{
|
||||
bool ToggleOn { get; }
|
||||
}
|
||||
|
||||
public interface ITargetPointActionMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Targeted local coordinates
|
||||
/// </summary>
|
||||
EntityCoordinates Target { get; }
|
||||
}
|
||||
|
||||
public interface ITargetEntityActionMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Targeted entity
|
||||
/// </summary>
|
||||
EntityUid Target { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle on the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformToggleOnActionMessage : PerformActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => true;
|
||||
public PerformToggleOnActionMessage(ActionType actionType) : base(actionType) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle off the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformToggleOffActionMessage : PerformActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => false;
|
||||
public PerformToggleOffActionMessage(ActionType actionType) : base(actionType) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle on the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformToggleOnItemActionMessage : PerformItemActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => true;
|
||||
public PerformToggleOnItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle off the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformToggleOffItemActionMessage : PerformItemActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => false;
|
||||
public PerformToggleOffItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided point with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformTargetPointActionMessage : PerformActionMessage, ITargetPointActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetPoint;
|
||||
private readonly EntityCoordinates _target;
|
||||
public EntityCoordinates Target => _target;
|
||||
|
||||
public PerformTargetPointActionMessage(ActionType actionType, EntityCoordinates target) : base(actionType)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided point with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformTargetPointItemActionMessage : PerformItemActionMessage, ITargetPointActionMessage
|
||||
{
|
||||
private readonly EntityCoordinates _target;
|
||||
public EntityCoordinates Target => _target;
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetPoint;
|
||||
|
||||
public PerformTargetPointItemActionMessage(ItemActionType actionType, EntityUid item, EntityCoordinates target) : base(actionType, item)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided entity with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformTargetEntityActionMessage : PerformActionMessage, ITargetEntityActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetEntity;
|
||||
private readonly EntityUid _target;
|
||||
public EntityUid Target => _target;
|
||||
|
||||
public PerformTargetEntityActionMessage(ActionType actionType, EntityUid target) : base(actionType)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided entity with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformTargetEntityItemActionMessage : PerformItemActionMessage, ITargetEntityActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetEntity;
|
||||
private readonly EntityUid _target;
|
||||
public EntityUid Target => _target;
|
||||
|
||||
public PerformTargetEntityItemActionMessage(ItemActionType actionType, EntityUid item, EntityUid target) : base(actionType, item)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// An attempt to perform a specific action. Main purpose of this interface is to
|
||||
/// reduce code duplication related to handling attempts to perform non-item vs item actions by
|
||||
/// providing a single interface for various functionality that needs to be performed on both.
|
||||
/// </summary>
|
||||
public interface IActionAttempt
|
||||
{
|
||||
/// <summary>
|
||||
/// Action Prototype attempting to be performed
|
||||
/// </summary>
|
||||
BaseActionPrototype Action { get; }
|
||||
#pragma warning disable 618
|
||||
ComponentMessage PerformInstantActionMessage();
|
||||
ComponentMessage PerformToggleActionMessage(bool on);
|
||||
ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args);
|
||||
ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args);
|
||||
#pragma warning restore 618
|
||||
/// <summary>
|
||||
/// Tries to get the action state for this action from the actionsComponent, returning
|
||||
/// true if found.
|
||||
/// </summary>
|
||||
bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState);
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the action within the provided action component
|
||||
/// </summary>
|
||||
void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the action
|
||||
/// </summary>
|
||||
void DoInstantAction(EntityUid player);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the toggle action
|
||||
/// </summary>
|
||||
/// <returns>true if the attempt to toggle was successful, meaning the state should be toggled to the
|
||||
/// indicated value</returns>
|
||||
bool DoToggleAction(EntityUid player, bool on);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the target point action
|
||||
/// </summary>
|
||||
void DoTargetPointAction(EntityUid player, EntityCoordinates target);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the target entity action
|
||||
/// </summary>
|
||||
void DoTargetEntityAction(EntityUid player, EntityUid target);
|
||||
}
|
||||
|
||||
public sealed class ActionAttempt : IActionAttempt
|
||||
{
|
||||
private readonly ActionPrototype _action;
|
||||
|
||||
public BaseActionPrototype Action => _action;
|
||||
|
||||
public ActionAttempt(ActionPrototype action)
|
||||
{
|
||||
_action = action;
|
||||
}
|
||||
|
||||
public bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState)
|
||||
{
|
||||
return actionsComponent.TryGetActionState(_action.ActionType, out actionState);
|
||||
}
|
||||
|
||||
public void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn)
|
||||
{
|
||||
actionsComponent.ToggleAction(_action.ActionType, toggleOn);
|
||||
}
|
||||
|
||||
public void DoInstantAction(EntityUid player)
|
||||
{
|
||||
_action.InstantAction.DoInstantAction(new InstantActionEventArgs(player, _action.ActionType));
|
||||
}
|
||||
|
||||
public bool DoToggleAction(EntityUid player, bool on)
|
||||
{
|
||||
return _action.ToggleAction.DoToggleAction(new ToggleActionEventArgs(player, _action.ActionType, on));
|
||||
}
|
||||
|
||||
public void DoTargetPointAction(EntityUid player, EntityCoordinates target)
|
||||
{
|
||||
_action.TargetPointAction.DoTargetPointAction(new TargetPointActionEventArgs(player, target, _action.ActionType));
|
||||
}
|
||||
|
||||
public void DoTargetEntityAction(EntityUid player, EntityUid target)
|
||||
{
|
||||
_action.TargetEntityAction.DoTargetEntityAction(new TargetEntityActionEventArgs(player, _action.ActionType,
|
||||
target));
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformInstantActionMessage()
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformInstantActionMessage(_action.ActionType);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformToggleActionMessage(bool toggleOn)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
if (toggleOn)
|
||||
{
|
||||
return new PerformToggleOnActionMessage(_action.ActionType);
|
||||
}
|
||||
return new PerformToggleOffActionMessage(_action.ActionType);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformTargetPointActionMessage(_action.ActionType, args.Coordinates);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformTargetEntityActionMessage(_action.ActionType, args.EntityUid);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(_action)}: {_action.ActionType}";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ItemActionAttempt : IActionAttempt
|
||||
{
|
||||
private readonly ItemActionPrototype _action;
|
||||
private readonly EntityUid _item;
|
||||
private readonly ItemActionsComponent _itemActions;
|
||||
|
||||
public BaseActionPrototype Action => _action;
|
||||
|
||||
public ItemActionAttempt(ItemActionPrototype action, EntityUid item, ItemActionsComponent itemActions)
|
||||
{
|
||||
_action = action;
|
||||
_item = item;
|
||||
_itemActions = itemActions;
|
||||
}
|
||||
|
||||
public void DoInstantAction(EntityUid player)
|
||||
{
|
||||
_action.InstantAction.DoInstantAction(new InstantItemActionEventArgs(player, _item, _action.ActionType));
|
||||
}
|
||||
|
||||
public bool DoToggleAction(EntityUid player, bool on)
|
||||
{
|
||||
return _action.ToggleAction.DoToggleAction(new ToggleItemActionEventArgs(player, on, _item, _action.ActionType));
|
||||
}
|
||||
|
||||
public void DoTargetPointAction(EntityUid player, EntityCoordinates target)
|
||||
{
|
||||
_action.TargetPointAction.DoTargetPointAction(new TargetPointItemActionEventArgs(player, target, _item,
|
||||
_action.ActionType));
|
||||
}
|
||||
|
||||
public void DoTargetEntityAction(EntityUid player, EntityUid target)
|
||||
{
|
||||
_action.TargetEntityAction.DoTargetEntityAction(new TargetEntityItemActionEventArgs(player, target,
|
||||
_item, _action.ActionType));
|
||||
}
|
||||
|
||||
public bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState)
|
||||
{
|
||||
return actionsComponent.TryGetItemActionState(_action.ActionType, _item, out actionState);
|
||||
}
|
||||
|
||||
public void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn)
|
||||
{
|
||||
_itemActions.Toggle(_action.ActionType, toggleOn);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformInstantActionMessage()
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformInstantItemActionMessage(_action.ActionType, _item);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformToggleActionMessage(bool toggleOn)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
if (toggleOn)
|
||||
{
|
||||
return new PerformToggleOnItemActionMessage(_action.ActionType, _item);
|
||||
}
|
||||
return new PerformToggleOffItemActionMessage(_action.ActionType, _item);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformTargetPointItemActionMessage(_action.ActionType, _item, args.Coordinates);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformTargetEntityItemActionMessage(_action.ActionType, _item, args.EntityUid);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(_action)}: {_action.ActionType}, {nameof(_item)}: {_item}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Module;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Shared.Actions.Prototypes
|
||||
{
|
||||
/// <summary>
|
||||
/// An action which is granted directly to an entity (such as an innate ability
|
||||
/// or skill).
|
||||
/// </summary>
|
||||
[Prototype("action")]
|
||||
[DataDefinition]
|
||||
public sealed class ActionPrototype : BaseActionPrototype, ISerializationHooks
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of action, no 2 action prototypes should have the same one.
|
||||
/// </summary>
|
||||
[DataField("actionType", required: true)]
|
||||
public ActionType ActionType { get; set; }
|
||||
|
||||
[DataField("behavior", serverOnly: true)]
|
||||
private IActionBehavior? Behavior { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The IInstantAction that should be invoked when performing this
|
||||
/// action. Null if this is not an Instant ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IInstantAction InstantAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The IToggleAction that should be invoked when performing this
|
||||
/// action. Null if this is not a Toggle ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IToggleAction ToggleAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetEntityAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetEntity ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetEntityAction TargetEntityAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetPointAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetPoint ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetPointAction TargetPointAction { get; private set; } = default!;
|
||||
|
||||
public override string ID => ActionType.ToString();
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
base.AfterDeserialization();
|
||||
|
||||
if (ActionType == ActionType.Error)
|
||||
{
|
||||
Logger.ErrorS("action", "missing or invalid actionType for action with name {0}", Name);
|
||||
}
|
||||
|
||||
if (IoCManager.Resolve<IModuleManager>().IsClientModule) return;
|
||||
|
||||
switch (Behavior)
|
||||
{
|
||||
case null:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "missing or invalid behavior for action with name {0}", Name);
|
||||
break;
|
||||
case IInstantAction instantAction:
|
||||
ValidateBehaviorType(BehaviorType.Instant, typeof(IInstantAction));
|
||||
BehaviorType = BehaviorType.Instant;
|
||||
InstantAction = instantAction;
|
||||
break;
|
||||
case IToggleAction toggleAction:
|
||||
ValidateBehaviorType(BehaviorType.Toggle, typeof(IToggleAction));
|
||||
BehaviorType = BehaviorType.Toggle;
|
||||
ToggleAction = toggleAction;
|
||||
break;
|
||||
case ITargetEntityAction targetEntity:
|
||||
ValidateBehaviorType(BehaviorType.TargetEntity, typeof(ITargetEntityAction));
|
||||
BehaviorType = BehaviorType.TargetEntity;
|
||||
TargetEntityAction = targetEntity;
|
||||
break;
|
||||
case ITargetPointAction targetPointAction:
|
||||
ValidateBehaviorType(BehaviorType.TargetPoint, typeof(ITargetPointAction));
|
||||
BehaviorType = BehaviorType.TargetPoint;
|
||||
TargetPointAction = targetPointAction;
|
||||
break;
|
||||
default:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "unrecognized behavior type for action with name {0}", Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Actions.Prototypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for action prototypes.
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract class BaseActionPrototype : IPrototype, ISerializationHooks
|
||||
{
|
||||
public abstract string ID { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Icon representing this action in the UI.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("icon")]
|
||||
public SpriteSpecifier Icon { get; } = SpriteSpecifier.Invalid;
|
||||
|
||||
/// <summary>
|
||||
/// For toggle actions only, icon to show when toggled on. If omitted,
|
||||
/// the action will simply be highlighted when turned on.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("iconOn")]
|
||||
public SpriteSpecifier IconOn { get; } = SpriteSpecifier.Invalid;
|
||||
|
||||
/// <summary>
|
||||
/// Name to show in UI. Accepts formatting.
|
||||
/// </summary>
|
||||
[DataField("name")]
|
||||
public FormattedMessage Name { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Description to show in UI. Accepts formatting.
|
||||
/// </summary>
|
||||
[DataField("description")]
|
||||
public FormattedMessage Description { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Requirements message to show in UI. Accepts formatting, but generally should be avoided
|
||||
/// so the requirements message isn't too prominent in the tooltip.
|
||||
/// </summary>
|
||||
[DataField("requires")]
|
||||
public string Requires { get; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The type of behavior this action has. This is valid clientside and serverside.
|
||||
/// </summary>
|
||||
[DataField("behaviorType")]
|
||||
public BehaviorType BehaviorType { get; protected set; } = BehaviorType.None;
|
||||
|
||||
/// <summary>
|
||||
/// For targetpoint or targetentity 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 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// For TargetEntity/TargetPoint actions, should the action be de-selected if currently selected (choosing a target)
|
||||
/// when it goes on cooldown. Defaults to false.
|
||||
/// </summary>
|
||||
[DataField("deselectOnCooldown")]
|
||||
public bool DeselectOnCooldown { get; }
|
||||
|
||||
/// <summary>
|
||||
/// For TargetEntity actions, should the action be de-selected if the user doesn't click an entity when
|
||||
/// selecting a target. Defaults to false.
|
||||
/// </summary>
|
||||
[DataField("deselectWhenEntityNotClicked")]
|
||||
public bool DeselectWhenEntityNotClicked { get; }
|
||||
|
||||
[DataField("filters")] private List<string> _filters = new();
|
||||
|
||||
/// <summary>
|
||||
/// Filters that can be used to filter this item in action menu.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Filters => _filters;
|
||||
|
||||
[DataField("keywords")] private List<string> _keywords = new();
|
||||
|
||||
/// <summary>
|
||||
/// Keywords that can be used to search this item in action menu.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Keywords => _keywords;
|
||||
|
||||
/// <summary>
|
||||
/// True if this is an action that requires selecting a target
|
||||
/// </summary>
|
||||
public bool IsTargetAction =>
|
||||
BehaviorType is BehaviorType.TargetEntity or BehaviorType.TargetPoint;
|
||||
|
||||
public virtual void AfterDeserialization()
|
||||
{
|
||||
Name = new FormattedMessage();
|
||||
Name.AddText(ID);
|
||||
|
||||
if (BehaviorType == BehaviorType.None)
|
||||
{
|
||||
Logger.ErrorS("action", $"Missing behaviorType for action with name {Name}");
|
||||
}
|
||||
|
||||
if (BehaviorType is not (BehaviorType.Toggle or BehaviorType.TargetEntity or BehaviorType.TargetPoint) && IconOn != SpriteSpecifier.Invalid)
|
||||
{
|
||||
Logger.ErrorS("action", $"for action {Name}, iconOn was specified but behavior" +
|
||||
" $type was {BehaviorType}. iconOn is only supported for Toggle behavior type.");
|
||||
}
|
||||
|
||||
if (Repeat && BehaviorType != BehaviorType.TargetEntity && BehaviorType != BehaviorType.TargetPoint)
|
||||
{
|
||||
Logger.ErrorS("action", $" action named {Name} used repeat: true, but this is only supported for" +
|
||||
" $TargetEntity and TargetPoint behaviorType and its behaviorType is {BehaviorType}");
|
||||
}
|
||||
}
|
||||
|
||||
protected void ValidateBehaviorType(BehaviorType expected, Type actualInterface)
|
||||
{
|
||||
if (BehaviorType != expected)
|
||||
{
|
||||
Logger.ErrorS("action", $"for action named {Name}, behavior implements " +
|
||||
$"{actualInterface.Name}, so behaviorType should be {expected} but was {BehaviorType}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The behavior / logic of the action. Each of these corresponds to a particular IActionBehavior
|
||||
/// (for actions) or IItemActionBehavior (for item actions)
|
||||
/// interface. Corresponds to action.behaviorType in YAML
|
||||
/// </summary>
|
||||
public enum BehaviorType
|
||||
{
|
||||
/// <summary>
|
||||
/// Action doesn't do anything.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// IInstantAction/IInstantItemAction. Action which does something immediately when used and has
|
||||
/// no target.
|
||||
/// </summary>
|
||||
Instant,
|
||||
|
||||
/// <summary>
|
||||
/// IToggleAction/IToggleItemAction Action which can be toggled on and off
|
||||
/// </summary>
|
||||
Toggle,
|
||||
|
||||
/// <summary>
|
||||
/// ITargetEntityAction/ITargetEntityItemAction. Action which is used on a targeted entity.
|
||||
/// </summary>
|
||||
TargetEntity,
|
||||
|
||||
/// <summary>
|
||||
/// ITargetPointAction/ITargetPointItemAction. Action which requires the user to select a target point, which
|
||||
/// does not necessarily have an entity on it.
|
||||
/// </summary>
|
||||
TargetPoint
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Content.Shared.Module;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Shared.Actions.Prototypes
|
||||
{
|
||||
/// <summary>
|
||||
/// An action which is granted to an entity via an item (such as toggling a flashlight).
|
||||
/// </summary>
|
||||
[Prototype("itemAction")]
|
||||
[DataDefinition]
|
||||
public sealed class ItemActionPrototype : BaseActionPrototype, ISerializationHooks
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of item action, no 2 itemAction prototypes should have the same one.
|
||||
/// </summary>
|
||||
[DataField("actionType")]
|
||||
public ItemActionType ActionType { get; private set; } = ItemActionType.Error;
|
||||
|
||||
/// <see cref="ItemActionIconStyle"/>
|
||||
[DataField("iconStyle")]
|
||||
public ItemActionIconStyle IconStyle { get; private set; } = ItemActionIconStyle.BigItem;
|
||||
|
||||
/// <summary>
|
||||
/// The IInstantItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not an Instant ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IInstantItemAction InstantAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The IToggleItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not a Toggle ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IToggleItemAction ToggleAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetEntityItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetEntity ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetEntityItemAction TargetEntityAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetPointItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetPoint ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetPointItemAction TargetPointAction { get; private set; } = default!;
|
||||
|
||||
[DataField("behavior", readOnly: true, serverOnly: true)]
|
||||
public IItemActionBehavior? ItemActionBehavior { get; private set; }
|
||||
|
||||
public override string ID => ActionType.ToString();
|
||||
|
||||
public override void AfterDeserialization()
|
||||
{
|
||||
base.AfterDeserialization();
|
||||
if (ActionType == ItemActionType.Error)
|
||||
{
|
||||
Logger.ErrorS("action", "missing or invalid actionType for action with name {0}", Name);
|
||||
}
|
||||
|
||||
// TODO: Split this class into server/client after RobustToolbox#1405
|
||||
if (IoCManager.Resolve<IModuleManager>().IsClientModule) return;
|
||||
|
||||
switch (ItemActionBehavior)
|
||||
{
|
||||
case null:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "missing or invalid behavior for action with name {0}", Name);
|
||||
break;
|
||||
case IInstantItemAction instantAction:
|
||||
ValidateBehaviorType(BehaviorType.Instant, typeof(IInstantItemAction));
|
||||
BehaviorType = BehaviorType.Instant;
|
||||
InstantAction = instantAction;
|
||||
break;
|
||||
case IToggleItemAction toggleAction:
|
||||
ValidateBehaviorType(BehaviorType.Toggle, typeof(IToggleItemAction));
|
||||
BehaviorType = BehaviorType.Toggle;
|
||||
ToggleAction = toggleAction;
|
||||
break;
|
||||
case ITargetEntityItemAction targetEntity:
|
||||
ValidateBehaviorType(BehaviorType.TargetEntity, typeof(ITargetEntityItemAction));
|
||||
BehaviorType = BehaviorType.TargetEntity;
|
||||
TargetEntityAction = targetEntity;
|
||||
break;
|
||||
case ITargetPointItemAction targetPointAction:
|
||||
ValidateBehaviorType(BehaviorType.TargetPoint, typeof(ITargetPointItemAction));
|
||||
BehaviorType = BehaviorType.TargetPoint;
|
||||
TargetPointAction = targetPointAction;
|
||||
break;
|
||||
default:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "unrecognized behavior type for action with name {0}", Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines how the action icon appears in the hotbar for item actions.
|
||||
/// </summary>
|
||||
public enum ItemActionIconStyle : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// The default - the item icon will be big with a small action icon in the corner
|
||||
/// </summary>
|
||||
BigItem,
|
||||
/// <summary>
|
||||
/// The action icon will be big with a small item icon in the corner
|
||||
/// </summary>
|
||||
BigAction,
|
||||
/// <summary>
|
||||
/// BigAction but no item icon will be shown in the corner.
|
||||
/// </summary>
|
||||
NoItem
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Robust.Shared.GameObjects;
|
||||
using System;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Evicts action states with expired cooldowns.
|
||||
/// </summary>
|
||||
public sealed class SharedActionSystem : EntitySystem
|
||||
{
|
||||
private const float CooldownCheckIntervalSeconds = 10;
|
||||
private float _timeSinceCooldownCheck;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
UpdatesOutsidePrediction = true;
|
||||
SubscribeLocalEvent<ItemActionsComponent, GotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<ItemActionsComponent, EquippedHandEvent>(OnHandEquipped);
|
||||
SubscribeLocalEvent<ItemActionsComponent, UnequippedHandEvent>((uid, comp, _) => OnUnequipped(uid, comp));
|
||||
SubscribeLocalEvent<ItemActionsComponent, GotUnequippedEvent>((uid, comp, _) => OnUnequipped(uid, comp));
|
||||
}
|
||||
|
||||
private void OnGotEquipped(EntityUid uid, ItemActionsComponent component, GotEquippedEvent args)
|
||||
{
|
||||
if (!TryComp(args.Equipee, out SharedActionsComponent? actionsComponent))
|
||||
return;
|
||||
|
||||
component.Holder = args.Equipee;
|
||||
component.HolderActionsComponent = actionsComponent;
|
||||
component.IsEquipped = true;
|
||||
component.GrantOrUpdateAllToHolder();
|
||||
}
|
||||
|
||||
private void OnHandEquipped(EntityUid uid, ItemActionsComponent component, EquippedHandEvent args)
|
||||
{
|
||||
if (!TryComp(args.User, out SharedActionsComponent? actionsComponent))
|
||||
return;
|
||||
|
||||
component.Holder = args.User;
|
||||
component.HolderActionsComponent = actionsComponent;
|
||||
component.IsEquipped = true;
|
||||
component.InHand = args.Hand;
|
||||
component.GrantOrUpdateAllToHolder();
|
||||
}
|
||||
|
||||
private void OnUnequipped(EntityUid uid, ItemActionsComponent component)
|
||||
{
|
||||
component.RevokeAllFromHolder();
|
||||
component.Holder = null;
|
||||
component.HolderActionsComponent = null;
|
||||
component.IsEquipped = false;
|
||||
component.InHand = null;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
_timeSinceCooldownCheck += frameTime;
|
||||
if (_timeSinceCooldownCheck < CooldownCheckIntervalSeconds) return;
|
||||
|
||||
foreach (var comp in EntityManager.EntityQuery<SharedActionsComponent>(false))
|
||||
{
|
||||
comp.ExpireCooldowns();
|
||||
}
|
||||
_timeSinceCooldownCheck -= CooldownCheckIntervalSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
459
Content.Shared/Actions/SharedActionsSystem.cs
Normal file
459
Content.Shared/Actions/SharedActionsSystem.cs
Normal file
@@ -0,0 +1,459 @@
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Shared.Actions;
|
||||
|
||||
public abstract class SharedActionsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAdminLogSystem _logSystem = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] protected readonly IGameTiming GameTiming = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ActionsComponent, DidEquipEvent>(OnDidEquip);
|
||||
SubscribeLocalEvent<ActionsComponent, DidEquipHandEvent>(OnHandEquipped);
|
||||
SubscribeLocalEvent<ActionsComponent, DidUnequipEvent>(OnDidUnequip);
|
||||
SubscribeLocalEvent<ActionsComponent, DidUnequipHandEvent>(OnHandUnequipped);
|
||||
|
||||
SubscribeLocalEvent<ActionsComponent, ComponentGetState>(GetState);
|
||||
SubscribeLocalEvent<ActionsComponent, ComponentGetStateAttemptEvent>(OnCanGetState);
|
||||
|
||||
SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest);
|
||||
}
|
||||
|
||||
#region ComponentStateManagement
|
||||
protected virtual void Dirty(ActionType action)
|
||||
{
|
||||
if (action.AttachedEntity == null)
|
||||
return;
|
||||
|
||||
if (!TryComp(action.AttachedEntity, out ActionsComponent? comp))
|
||||
{
|
||||
action.AttachedEntity = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Dirty(comp);
|
||||
}
|
||||
|
||||
public void SetToggled(ActionType action, bool toggled)
|
||||
{
|
||||
if (action.Toggled == toggled)
|
||||
return;
|
||||
|
||||
action.Toggled = toggled;
|
||||
Dirty(action);
|
||||
}
|
||||
|
||||
public void SetEnabled(ActionType action, bool enabled)
|
||||
{
|
||||
if (action.Enabled == enabled)
|
||||
return;
|
||||
|
||||
action.Enabled = enabled;
|
||||
Dirty(action);
|
||||
}
|
||||
|
||||
public void SetCharges(ActionType action, int? charges)
|
||||
{
|
||||
if (action.Charges == charges)
|
||||
return;
|
||||
|
||||
action.Charges = charges;
|
||||
Dirty(action);
|
||||
}
|
||||
|
||||
private void GetState(EntityUid uid, ActionsComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new ActionsComponentState(component.Actions.ToList());
|
||||
}
|
||||
|
||||
private void OnCanGetState(EntityUid uid, ActionsComponent component, ref ComponentGetStateAttemptEvent args)
|
||||
{
|
||||
// Only send action state data to the relevant player.
|
||||
if (args.Player.AttachedEntity != uid)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Execution
|
||||
/// <summary>
|
||||
/// When receiving a request to perform an action, this validates whether the action is allowed. If it is, it
|
||||
/// will raise the relevant <see cref="PerformActionEvent"/>
|
||||
/// </summary>
|
||||
private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArgs args)
|
||||
{
|
||||
if (args.SenderSession.AttachedEntity is not EntityUid user)
|
||||
return;
|
||||
|
||||
if (!TryComp(user, out ActionsComponent? component))
|
||||
return;
|
||||
|
||||
// Does the user actually have the requested action?
|
||||
if (!component.Actions.TryGetValue(ev.Action, out var act))
|
||||
{
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} attempted to perform an action that they do not have: {ev.Action.Name}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!act.Enabled)
|
||||
return;
|
||||
|
||||
var curTime = GameTiming.CurTime;
|
||||
if (act.Cooldown.HasValue && act.Cooldown.Value.End > curTime)
|
||||
return;
|
||||
|
||||
PerformActionEvent? performEvent = null;
|
||||
|
||||
// Validate request by checking action blockers and the like:
|
||||
var name = Loc.GetString(act.Name);
|
||||
|
||||
switch (act)
|
||||
{
|
||||
case EntityTargetAction entityAction:
|
||||
|
||||
if (ev.EntityTarget is not EntityUid { Valid: true } entityTarget)
|
||||
{
|
||||
Logger.Error($"Attempted to perform an entity-targeted action without a target! Action: {entityAction.Name}");
|
||||
return;
|
||||
}
|
||||
|
||||
_rotateToFaceSystem.TryFaceCoordinates(user, Transform(entityTarget).WorldPosition);
|
||||
|
||||
if (!ValidateEntityTarget(user, entityTarget, entityAction))
|
||||
return;
|
||||
|
||||
if (act.Provider == null)
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} is performing the {name:action} action targeted at {ToPrettyString(entityTarget):target}.");
|
||||
else
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(act.Provider.Value):provider}) targeted at {ToPrettyString(entityTarget):target}.");
|
||||
|
||||
if (entityAction.Event != null)
|
||||
{
|
||||
entityAction.Event.Target = entityTarget;
|
||||
performEvent = entityAction.Event;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case WorldTargetAction worldAction:
|
||||
|
||||
if (ev.MapTarget is not MapCoordinates mapTarget)
|
||||
{
|
||||
Logger.Error($"Attempted to perform a map-targeted action without a target! Action: {worldAction.Name}");
|
||||
return;
|
||||
}
|
||||
|
||||
_rotateToFaceSystem.TryFaceCoordinates(user, mapTarget.Position);
|
||||
|
||||
if (!ValidateWorldTarget(user, mapTarget, worldAction))
|
||||
return;
|
||||
|
||||
if (act.Provider == null)
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} is performing the {name:action} action targeted at {mapTarget:target}.");
|
||||
else
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(act.Provider.Value):provider}) targeted at {mapTarget:target}.");
|
||||
|
||||
if (worldAction.Event != null)
|
||||
{
|
||||
worldAction.Event.Target = mapTarget;
|
||||
performEvent = worldAction.Event;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case InstantAction instantAction:
|
||||
|
||||
if (act.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
|
||||
return;
|
||||
|
||||
if (act.Provider == null)
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} is performing the {name:action} action.");
|
||||
else
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} is performing the {name:action} action provided by {ToPrettyString(act.Provider.Value):provider}.");
|
||||
|
||||
performEvent = instantAction.Event;
|
||||
break;
|
||||
}
|
||||
|
||||
if (performEvent != null)
|
||||
performEvent.Performer = user;
|
||||
|
||||
// All checks passed. Perform the action!
|
||||
PerformAction(component, act, performEvent, curTime);
|
||||
}
|
||||
|
||||
public bool ValidateEntityTarget(EntityUid user, EntityUid target, EntityTargetAction action)
|
||||
{
|
||||
if (!target.IsValid() || Deleted(target))
|
||||
return false;
|
||||
|
||||
if (action.Whitelist != null && !action.Whitelist.IsValid(target, EntityManager))
|
||||
return false;
|
||||
|
||||
if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, target))
|
||||
return false;
|
||||
|
||||
if (user == target)
|
||||
return action.CanTargetSelf;
|
||||
|
||||
if (!action.CheckCanAccess)
|
||||
{
|
||||
// even if we don't check for obstructions, we may still need to check the range.
|
||||
var xform = Transform(user);
|
||||
var targetXform = Transform(target);
|
||||
|
||||
if (xform.MapID != targetXform.MapID)
|
||||
return false;
|
||||
|
||||
if (action.Range <= 0)
|
||||
return true;
|
||||
|
||||
return (xform.WorldPosition - targetXform.WorldPosition).Length <= action.Range;
|
||||
}
|
||||
|
||||
if (_interactionSystem.InRangeUnobstructed(user, target, range: action.Range)
|
||||
&& _containerSystem.IsInSameOrParentContainer(user, target))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _interactionSystem.CanAccessViaStorage(user, target);
|
||||
}
|
||||
|
||||
public bool ValidateWorldTarget(EntityUid user, MapCoordinates coords, WorldTargetAction action)
|
||||
{
|
||||
if (coords == MapCoordinates.Nullspace)
|
||||
return false;
|
||||
|
||||
if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
|
||||
return false;
|
||||
|
||||
if (!action.CheckCanAccess)
|
||||
{
|
||||
// even if we don't check for obstructions, we may still need to check the range.
|
||||
var xform = Transform(user);
|
||||
|
||||
if (xform.MapID != coords.MapId)
|
||||
return false;
|
||||
|
||||
if (action.Range <= 0)
|
||||
return true;
|
||||
|
||||
return (xform.WorldPosition - coords.Position).Length <= action.Range;
|
||||
}
|
||||
|
||||
return _interactionSystem.InRangeUnobstructed(user, coords, range: action.Range);
|
||||
}
|
||||
|
||||
protected void PerformAction(ActionsComponent component, ActionType action, PerformActionEvent? actionEvent, TimeSpan curTime)
|
||||
{
|
||||
var handled = false;
|
||||
|
||||
var toggledBefore = action.Toggled;
|
||||
|
||||
if (actionEvent != null)
|
||||
{
|
||||
// This here is required because of client-side prediction (RaisePredictiveEvent results in event re-use).
|
||||
actionEvent.Handled = false;
|
||||
|
||||
if (action.Provider == null)
|
||||
RaiseLocalEvent(component.Owner, (object) actionEvent, broadcast: true);
|
||||
else
|
||||
RaiseLocalEvent(action.Provider.Value, (object) actionEvent, broadcast: true);
|
||||
|
||||
handled = actionEvent.Handled;
|
||||
}
|
||||
|
||||
// Execute convenience functionality (pop-ups, sound, speech)
|
||||
handled |= PerformBasicActions(component.Owner, action);
|
||||
|
||||
if (!handled)
|
||||
return; // no interaction occurred.
|
||||
|
||||
// reduce charges, start cooldown, and mark as dirty (if required).
|
||||
|
||||
var dirty = toggledBefore == action.Toggled;
|
||||
|
||||
if (action.Charges != null)
|
||||
{
|
||||
dirty = true;
|
||||
action.Charges--;
|
||||
if (action.Charges == 0)
|
||||
action.Enabled = false;
|
||||
}
|
||||
|
||||
action.Cooldown = null;
|
||||
if (action.UseDelay != null)
|
||||
{
|
||||
dirty = true;
|
||||
action.Cooldown = (curTime, curTime + action.UseDelay.Value);
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute convenience functionality for actions (pop-ups, sound, speech)
|
||||
/// </summary>
|
||||
protected virtual bool PerformBasicActions(EntityUid performer, ActionType action)
|
||||
{
|
||||
if (action.Sound == null && string.IsNullOrWhiteSpace(action.Popup))
|
||||
return false;
|
||||
|
||||
var filter = Filter.Pvs(performer).RemoveWhereAttachedEntity(e => e == performer);
|
||||
|
||||
if (action.Sound != null)
|
||||
SoundSystem.Play(filter, action.Sound.GetSound(), performer, action.AudioParams);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(action.Popup))
|
||||
return true;
|
||||
|
||||
var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
|
||||
? Loc.GetString(action.Popup)
|
||||
: Loc.GetString(action.Popup + action.PopupToggleSuffix);
|
||||
|
||||
_popupSystem.PopupEntity(msg, performer, filter);
|
||||
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region AddRemoveActions
|
||||
/// <summary>
|
||||
/// Add an action 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="action">The action 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)
|
||||
{
|
||||
comp ??= EnsureComp<ActionsComponent>(uid);
|
||||
action.Provider = provider;
|
||||
action.AttachedEntity = comp.Owner;
|
||||
AddActionInternal(comp, action);
|
||||
|
||||
// for client-exclusive actions, the client shouldn't mark the comp as dirty. Otherwise that just leads to
|
||||
// unnecessary prediction resetting and state handling.
|
||||
if (dirty)
|
||||
Dirty(comp);
|
||||
}
|
||||
|
||||
protected virtual void AddActionInternal(ActionsComponent comp, ActionType action)
|
||||
{
|
||||
comp.Actions.Add(action);
|
||||
}
|
||||
|
||||
/// <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="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)
|
||||
{
|
||||
comp ??= EnsureComp<ActionsComponent>(uid);
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
AddAction(uid, action, provider, comp, false);
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
Dirty(comp);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return;
|
||||
|
||||
var provided = comp.Actions.Where(act => act.Provider == provider).ToList();
|
||||
|
||||
RemoveActions(uid, provided, comp);
|
||||
}
|
||||
|
||||
public virtual void RemoveActions(EntityUid uid, IEnumerable<ActionType> actions, ActionsComponent? comp = null, bool dirty = true)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return;
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
comp.Actions.Remove(action);
|
||||
action.AttachedEntity = null;
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
Dirty(comp);
|
||||
}
|
||||
|
||||
public void RemoveAction(EntityUid uid, ActionType action, ActionsComponent? comp = null)
|
||||
=> RemoveActions(uid, new[] { action }, comp);
|
||||
#endregion
|
||||
|
||||
#region EquipHandlers
|
||||
private void OnDidEquip(EntityUid uid, ActionsComponent component, DidEquipEvent args)
|
||||
{
|
||||
var ev = new GetActionsEvent();
|
||||
RaiseLocalEvent(args.Equipment, ev, false);
|
||||
|
||||
if (ev.Actions.Count == 0)
|
||||
return;
|
||||
|
||||
AddActions(args.Equipee, ev.Actions, args.Equipment, component);
|
||||
}
|
||||
|
||||
private void OnHandEquipped(EntityUid uid, ActionsComponent component, DidEquipHandEvent args)
|
||||
{
|
||||
var ev = new GetActionsEvent();
|
||||
RaiseLocalEvent(args.Equipped, ev, false);
|
||||
|
||||
if (ev.Actions.Count == 0)
|
||||
return;
|
||||
|
||||
AddActions(args.User, ev.Actions, args.Equipped, component);
|
||||
}
|
||||
|
||||
private void OnDidUnequip(EntityUid uid, ActionsComponent component, DidUnequipEvent args)
|
||||
{
|
||||
RemoveProvidedActions(uid, args.Equipment, component);
|
||||
}
|
||||
|
||||
private void OnHandUnequipped(EntityUid uid, ActionsComponent component, DidUnequipHandEvent args)
|
||||
{
|
||||
RemoveProvidedActions(uid, args.Unequipped, component);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user