From 1df84515c72f4a421a395718ce8b394fcd2b6fed Mon Sep 17 00:00:00 2001 From: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> Date: Thu, 8 Aug 2024 02:47:08 -0700 Subject: [PATCH] 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> --- Content.Client/Actions/ActionsSystem.cs | 13 ++ .../Systems/Actions/ActionUIController.cs | 48 ++++++- .../Actions/ActionOnInteractSystem.cs | 25 ++++ Content.Shared/Actions/ActionEvents.cs | 28 +++++ .../EntityWorldTargetActionComponent.cs | 42 +++++++ .../ValidateActionEntityWorldTargetEvent.cs | 10 ++ Content.Shared/Actions/SharedActionsSystem.cs | 119 +++++++++++++++--- 7 files changed, 266 insertions(+), 19 deletions(-) create mode 100644 Content.Shared/Actions/EntityWorldTargetActionComponent.cs create mode 100644 Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 30f657a2b5..f05e445588 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -48,6 +48,7 @@ namespace Content.Client.Actions SubscribeLocalEvent(OnInstantHandleState); SubscribeLocalEvent(OnEntityTargetHandleState); SubscribeLocalEvent(OnWorldTargetHandleState); + SubscribeLocalEvent(OnEntityWorldTargetHandleState); } private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args) @@ -76,6 +77,18 @@ namespace Content.Client.Actions BaseHandleState(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(uid, component, state); + } + private void BaseHandleState(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent { // TODO ACTIONS use auto comp states diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs index 4d2ac20d12..1c76b30075 100644 --- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs +++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs @@ -184,10 +184,13 @@ public sealed class ActionUIController : UIController, IOnStateChanged(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(component.ActionEntities, args.CanReach); for (var i = options.Count - 1; i >= 0; i--) diff --git a/Content.Shared/Actions/ActionEvents.cs b/Content.Shared/Actions/ActionEvents.cs index 6cc50bc21b..4f1cd6da44 100644 --- a/Content.Shared/Actions/ActionEvents.cs +++ b/Content.Shared/Actions/ActionEvents.cs @@ -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; + } } /// @@ -144,6 +151,27 @@ public abstract partial class WorldTargetActionEvent : BaseActionEvent public EntityCoordinates Target; } +/// +/// This is the type of event that gets raised when an is performed. +/// The , , and +/// fields will automatically be filled out by the . +/// +/// +/// To define a new action for some system, you need to create an event that inherits from this class. +/// +public abstract partial class EntityWorldTargetActionEvent : BaseActionEvent +{ + /// + /// The entity that the user targeted. + /// + public EntityUid? Entity; + + /// + /// The coordinates of the location that the user targeted. + /// + public EntityCoordinates? Coords; +} + /// /// Base class for events that are raised when an action gets performed. This should not generally be used outside of the action /// system. diff --git a/Content.Shared/Actions/EntityWorldTargetActionComponent.cs b/Content.Shared/Actions/EntityWorldTargetActionComponent.cs new file mode 100644 index 0000000000..3cfa60d030 --- /dev/null +++ b/Content.Shared/Actions/EntityWorldTargetActionComponent.cs @@ -0,0 +1,42 @@ +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Actions; + +/// +/// Used on action entities to define an action that triggers when targeting an entity or entity coordinates. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class EntityWorldTargetActionComponent : BaseTargetActionComponent +{ + public override BaseActionEvent? BaseEvent => Event; + + /// + /// The local-event to raise when this action is performed. + /// + [DataField] + [NonSerialized] + public EntityWorldTargetActionEvent? Event; + + /// + /// Determines which entities are valid targets for this action. + /// + /// No whitelist check when null. + [DataField] public EntityWhitelist? Whitelist; + + /// + /// Whether this action considers the user as a valid target entity when using this action. + /// + [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; +} diff --git a/Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs b/Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs new file mode 100644 index 0000000000..57c47026be --- /dev/null +++ b/Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs @@ -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); diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 0033078b1b..635d78b8dd 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -38,10 +38,12 @@ public abstract class SharedActionsSystem : EntitySystem SubscribeLocalEvent(OnActionMapInit); SubscribeLocalEvent(OnActionMapInit); SubscribeLocalEvent(OnActionMapInit); + SubscribeLocalEvent(OnActionMapInit); SubscribeLocalEvent(OnActionShutdown); SubscribeLocalEvent(OnActionShutdown); SubscribeLocalEvent(OnActionShutdown); + SubscribeLocalEvent(OnActionShutdown); SubscribeLocalEvent(OnDidEquip); SubscribeLocalEvent(OnHandEquipped); @@ -56,10 +58,12 @@ public abstract class SharedActionsSystem : EntitySystem SubscribeLocalEvent(OnInstantGetState); SubscribeLocalEvent(OnEntityTargetGetState); SubscribeLocalEvent(OnWorldTargetGetState); + SubscribeLocalEvent(OnEntityWorldTargetGetState); SubscribeLocalEvent(OnGetActionData); SubscribeLocalEvent(OnGetActionData); SubscribeLocalEvent(OnGetActionData); + SubscribeLocalEvent(OnGetActionData); SubscribeAllEvent(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(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(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 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 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 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)