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:
Leon Friedrich
2022-02-26 18:24:08 +13:00
committed by GitHub
parent d32f884157
commit ff7d4ed9f6
135 changed files with 3156 additions and 5166 deletions

View 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;
}
}
}

View 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);
}
}

View 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);
}
}