Add EntityWorldTargetAction (#29819)

* Add EntityWorldTargetAction initial implementation

* Update obsolete methods

* Partially working EntityWorldTargetAction

* Fix entity selection

* Move and clean up AfterInteract

* Fix building new walls

* Readd no entity or coordinates error

* Consolidate action validation code

* Add summaries to component

---------

Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
This commit is contained in:
ShadowCommander
2024-08-08 02:47:08 -07:00
committed by GitHub
parent 489efeb717
commit 1df84515c7
7 changed files with 266 additions and 19 deletions

View File

@@ -48,6 +48,7 @@ namespace Content.Client.Actions
SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState);
SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState);
SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState);
SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
}
private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
@@ -76,6 +77,18 @@ namespace Content.Client.Actions
BaseHandleState<WorldTargetActionComponent>(uid, component, state);
}
private void OnEntityWorldTargetHandleState(EntityUid uid,
EntityWorldTargetActionComponent component,
ref ComponentHandleState args)
{
if (args.Current is not EntityWorldTargetActionComponentState state)
return;
component.Whitelist = state.Whitelist;
component.CanTargetSelf = state.CanTargetSelf;
BaseHandleState<EntityWorldTargetActionComponent>(uid, component, state);
}
private void BaseHandleState<T>(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent
{
// TODO ACTIONS use auto comp states

View File

@@ -189,6 +189,9 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
case EntityTargetActionComponent entTarget:
return TryTargetEntity(args, actionId, entTarget, user, comp) || !entTarget.InteractOnMiss;
case EntityWorldTargetActionComponent entMapTarget:
return TryTargetEntityWorld(args, actionId, entMapTarget, user, comp) || !entMapTarget.InteractOnMiss;
default:
Logger.Error($"Unknown targeting action: {actionId.GetType()}");
return false;
@@ -266,6 +269,47 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
return true;
}
private bool TryTargetEntityWorld(in PointerInputCmdArgs args,
EntityUid actionId,
EntityWorldTargetActionComponent action,
EntityUid user,
ActionsComponent actionComp)
{
if (_actionsSystem == null)
return false;
var entity = args.EntityUid;
var coords = args.Coordinates;
if (!_actionsSystem.ValidateEntityWorldTarget(user, entity, coords, (actionId, action)))
{
if (action.DeselectOnMiss)
StopTargeting();
return false;
}
if (action.ClientExclusive)
{
if (action.Event != null)
{
action.Event.Entity = entity;
action.Event.Coords = coords;
action.Event.Performer = user;
action.Event.Action = actionId;
}
_actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
}
else
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetEntity(args.EntityUid), EntityManager.GetNetCoordinates(coords)));
if (!action.Repeat)
StopTargeting();
return true;
}
public void UnloadButton()
{
if (ActionButton == null)

View File

@@ -105,6 +105,31 @@ public sealed class ActionOnInteractSystem : EntitySystem
}
}
// Then EntityWorld target actions
var entWorldOptions = GetValidActions<EntityWorldTargetActionComponent>(actionEnts, args.CanReach);
for (var i = entWorldOptions.Count - 1; i >= 0; i--)
{
var action = entWorldOptions[i];
if (!_actions.ValidateEntityWorldTarget(args.User, args.Target, args.ClickLocation, action))
entWorldOptions.RemoveAt(i);
}
if (entWorldOptions.Count > 0)
{
var (entActId, entAct) = _random.Pick(entWorldOptions);
if (entAct.Event != null)
{
entAct.Event.Performer = args.User;
entAct.Event.Action = entActId;
entAct.Event.Entity = args.Target;
entAct.Event.Coords = args.ClickLocation;
}
_actions.PerformAction(args.User, null, entActId, entAct, entAct.Event, _timing.CurTime, false);
args.Handled = true;
return;
}
// else: try world target actions
var options = GetValidActions<WorldTargetActionComponent>(component.ActionEntities, args.CanReach);
for (var i = options.Count - 1; i >= 0; i--)

View File

@@ -101,6 +101,13 @@ public sealed class RequestPerformActionEvent : EntityEventArgs
Action = action;
EntityCoordinatesTarget = entityCoordinatesTarget;
}
public RequestPerformActionEvent(NetEntity action, NetEntity entityTarget, NetCoordinates entityCoordinatesTarget)
{
Action = action;
EntityTarget = entityTarget;
EntityCoordinatesTarget = entityCoordinatesTarget;
}
}
/// <summary>
@@ -144,6 +151,27 @@ public abstract partial class WorldTargetActionEvent : BaseActionEvent
public EntityCoordinates Target;
}
/// <summary>
/// This is the type of event that gets raised when an <see cref="EntityWorldTargetActionComponent"/> is performed.
/// The <see cref="BaseActionEvent.Performer"/>, <see cref="Entity"/>, and <see cref="Coords"/>
/// fields will automatically be filled out by the <see cref="SharedActionsSystem"/>.
/// </summary>
/// <remarks>
/// To define a new action for some system, you need to create an event that inherits from this class.
/// </remarks>
public abstract partial class EntityWorldTargetActionEvent : BaseActionEvent
{
/// <summary>
/// The entity that the user targeted.
/// </summary>
public EntityUid? Entity;
/// <summary>
/// The coordinates of the location that the user targeted.
/// </summary>
public EntityCoordinates? Coords;
}
/// <summary>
/// Base class for events that are raised when an action gets performed. This should not generally be used outside of the action
/// system.

View File

@@ -0,0 +1,42 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Actions;
/// <summary>
/// Used on action entities to define an action that triggers when targeting an entity or entity coordinates.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class EntityWorldTargetActionComponent : BaseTargetActionComponent
{
public override BaseActionEvent? BaseEvent => Event;
/// <summary>
/// The local-event to raise when this action is performed.
/// </summary>
[DataField]
[NonSerialized]
public EntityWorldTargetActionEvent? Event;
/// <summary>
/// Determines which entities are valid targets for this action.
/// </summary>
/// <remarks>No whitelist check when null.</remarks>
[DataField] public EntityWhitelist? Whitelist;
/// <summary>
/// Whether this action considers the user as a valid target entity when using this action.
/// </summary>
[DataField] public bool CanTargetSelf = true;
}
[Serializable, NetSerializable]
public sealed class EntityWorldTargetActionComponentState(
EntityWorldTargetActionComponent component,
IEntityManager entManager)
: BaseActionComponentState(component, entManager)
{
public EntityWhitelist? Whitelist = component.Whitelist;
public bool CanTargetSelf = component.CanTargetSelf;
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.Map;
namespace Content.Shared.Actions.Events;
[ByRefEvent]
public record struct ValidateActionEntityWorldTargetEvent(
EntityUid User,
EntityUid? Target,
EntityCoordinates? Coords,
bool Cancelled = false);

View File

@@ -38,10 +38,12 @@ public abstract class SharedActionsSystem : EntitySystem
SubscribeLocalEvent<InstantActionComponent, MapInitEvent>(OnActionMapInit);
SubscribeLocalEvent<EntityTargetActionComponent, MapInitEvent>(OnActionMapInit);
SubscribeLocalEvent<WorldTargetActionComponent, MapInitEvent>(OnActionMapInit);
SubscribeLocalEvent<EntityWorldTargetActionComponent, MapInitEvent>(OnActionMapInit);
SubscribeLocalEvent<InstantActionComponent, ComponentShutdown>(OnActionShutdown);
SubscribeLocalEvent<EntityTargetActionComponent, ComponentShutdown>(OnActionShutdown);
SubscribeLocalEvent<WorldTargetActionComponent, ComponentShutdown>(OnActionShutdown);
SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentShutdown>(OnActionShutdown);
SubscribeLocalEvent<ActionsComponent, DidEquipEvent>(OnDidEquip);
SubscribeLocalEvent<ActionsComponent, DidEquipHandEvent>(OnHandEquipped);
@@ -56,10 +58,12 @@ public abstract class SharedActionsSystem : EntitySystem
SubscribeLocalEvent<InstantActionComponent, ComponentGetState>(OnInstantGetState);
SubscribeLocalEvent<EntityTargetActionComponent, ComponentGetState>(OnEntityTargetGetState);
SubscribeLocalEvent<WorldTargetActionComponent, ComponentGetState>(OnWorldTargetGetState);
SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentGetState>(OnEntityWorldTargetGetState);
SubscribeLocalEvent<InstantActionComponent, GetActionDataEvent>(OnGetActionData);
SubscribeLocalEvent<EntityTargetActionComponent, GetActionDataEvent>(OnGetActionData);
SubscribeLocalEvent<WorldTargetActionComponent, GetActionDataEvent>(OnGetActionData);
SubscribeLocalEvent<EntityWorldTargetActionComponent, GetActionDataEvent>(OnGetActionData);
SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest);
}
@@ -102,6 +106,11 @@ public abstract class SharedActionsSystem : EntitySystem
args.State = new WorldTargetActionComponentState(component, EntityManager);
}
private void OnEntityWorldTargetGetState(EntityUid uid, EntityWorldTargetActionComponent component, ref ComponentGetState args)
{
args.State = new EntityWorldTargetActionComponentState(component, EntityManager);
}
private void OnGetActionData<T>(EntityUid uid, T component, ref GetActionDataEvent args) where T : BaseActionComponent
{
args.Action = component;
@@ -442,6 +451,34 @@ public abstract class SharedActionsSystem : EntitySystem
}
break;
case EntityWorldTargetActionComponent entityWorldAction:
{
var actionEntity = GetEntity(ev.EntityTarget);
var actionCoords = GetCoordinates(ev.EntityCoordinatesTarget);
if (actionEntity is null && actionCoords is null)
{
Log.Error($"Attempted to perform an entity-world-targeted action without an entity or world coordinates! Action: {name}");
return;
}
var entWorldAction = new Entity<EntityWorldTargetActionComponent>(actionEnt, entityWorldAction);
if (!ValidateEntityWorldTarget(user, actionEntity, actionCoords, entWorldAction))
return;
_adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(action.Container ?? user):provider}) targeted at {ToPrettyString(actionEntity):target} {actionCoords:target}.");
if (entityWorldAction.Event != null)
{
entityWorldAction.Event.Entity = actionEntity;
entityWorldAction.Event.Coords = actionCoords;
Dirty(actionEnt, entityWorldAction);
performEvent = entityWorldAction.Event;
}
break;
}
case InstantActionComponent instantAction:
if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
return;
@@ -465,7 +502,14 @@ public abstract class SharedActionsSystem : EntitySystem
public bool ValidateEntityTarget(EntityUid user, EntityUid target, Entity<EntityTargetActionComponent> actionEnt)
{
if (!ValidateEntityTargetBase(user, target, actionEnt))
var comp = actionEnt.Comp;
if (!ValidateEntityTargetBase(user,
target,
comp.Whitelist,
comp.CheckCanInteract,
comp.CanTargetSelf,
comp.CheckCanAccess,
comp.Range))
return false;
var ev = new ValidateActionEntityTargetEvent(user, target);
@@ -473,21 +517,27 @@ public abstract class SharedActionsSystem : EntitySystem
return !ev.Cancelled;
}
private bool ValidateEntityTargetBase(EntityUid user, EntityUid target, EntityTargetActionComponent action)
private bool ValidateEntityTargetBase(EntityUid user,
EntityUid? targetEntity,
EntityWhitelist? whitelist,
bool checkCanInteract,
bool canTargetSelf,
bool checkCanAccess,
float range)
{
if (!target.IsValid() || Deleted(target))
if (targetEntity is not { } target || !target.IsValid() || Deleted(target))
return false;
if (_whitelistSystem.IsWhitelistFail(action.Whitelist, target))
if (_whitelistSystem.IsWhitelistFail(whitelist, target))
return false;
if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, target))
if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target))
return false;
if (user == target)
return action.CanTargetSelf;
return canTargetSelf;
if (!action.CheckCanAccess)
if (!checkCanAccess)
{
// even if we don't check for obstructions, we may still need to check the range.
var xform = Transform(user);
@@ -496,19 +546,20 @@ public abstract class SharedActionsSystem : EntitySystem
if (xform.MapID != targetXform.MapID)
return false;
if (action.Range <= 0)
if (range <= 0)
return true;
var distance = (_transformSystem.GetWorldPosition(xform) - _transformSystem.GetWorldPosition(targetXform)).Length();
return distance <= action.Range;
return distance <= range;
}
return _interactionSystem.InRangeAndAccessible(user, target, range: action.Range);
return _interactionSystem.InRangeAndAccessible(user, target, range: range);
}
public bool ValidateWorldTarget(EntityUid user, EntityCoordinates coords, Entity<WorldTargetActionComponent> action)
{
if (!ValidateWorldTargetBase(user, coords, action))
var comp = action.Comp;
if (!ValidateWorldTargetBase(user, coords, comp.CheckCanInteract, comp.CheckCanAccess, comp.Range))
return false;
var ev = new ValidateActionWorldTargetEvent(user, coords);
@@ -516,12 +567,19 @@ public abstract class SharedActionsSystem : EntitySystem
return !ev.Cancelled;
}
private bool ValidateWorldTargetBase(EntityUid user, EntityCoordinates coords, WorldTargetActionComponent action)
private bool ValidateWorldTargetBase(EntityUid user,
EntityCoordinates? entityCoordinates,
bool checkCanInteract,
bool checkCanAccess,
float range)
{
if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
if (entityCoordinates is not { } coords)
return false;
if (!action.CheckCanAccess)
if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, null))
return false;
if (!checkCanAccess)
{
// even if we don't check for obstructions, we may still need to check the range.
var xform = Transform(user);
@@ -529,13 +587,40 @@ public abstract class SharedActionsSystem : EntitySystem
if (xform.MapID != coords.GetMapId(EntityManager))
return false;
if (action.Range <= 0)
if (range <= 0)
return true;
return _transformSystem.InRange(coords, Transform(user).Coordinates, action.Range);
return coords.InRange(EntityManager, _transformSystem, Transform(user).Coordinates, range);
}
return _interactionSystem.InRangeUnobstructed(user, coords, range: action.Range);
return _interactionSystem.InRangeUnobstructed(user, coords, range: range);
}
public bool ValidateEntityWorldTarget(EntityUid user,
EntityUid? entity,
EntityCoordinates? coords,
Entity<EntityWorldTargetActionComponent> action)
{
var comp = action.Comp;
var entityValidated = ValidateEntityTargetBase(user,
entity,
comp.Whitelist,
comp.CheckCanInteract,
comp.CanTargetSelf,
comp.CheckCanAccess,
comp.Range);
var worldValidated
= ValidateWorldTargetBase(user, coords, comp.CheckCanInteract, comp.CheckCanAccess, comp.Range);
if (!entityValidated && !worldValidated)
return false;
var ev = new ValidateActionEntityWorldTargetEvent(user,
entityValidated ? entity : null,
worldValidated ? coords : null);
RaiseLocalEvent(action, ref ev);
return !ev.Cancelled;
}
public void PerformAction(EntityUid performer, ActionsComponent? component, EntityUid actionId, BaseActionComponent action, BaseActionEvent? actionEvent, TimeSpan curTime, bool predicted = true)