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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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--)
|
||||
|
||||
@@ -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.
|
||||
|
||||
42
Content.Shared/Actions/EntityWorldTargetActionComponent.cs
Normal file
42
Content.Shared/Actions/EntityWorldTargetActionComponent.cs
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user