Refactor actions to be entities with components (#19900)

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

View File

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