]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add EntityWorldTargetAction (#29819)
authorShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
Thu, 8 Aug 2024 09:47:08 +0000 (02:47 -0700)
committerGitHub <noreply@github.com>
Thu, 8 Aug 2024 09:47:08 +0000 (12:47 +0300)
* 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
Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
Content.Server/Actions/ActionOnInteractSystem.cs
Content.Shared/Actions/ActionEvents.cs
Content.Shared/Actions/EntityWorldTargetActionComponent.cs [new file with mode: 0644]
Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs [new file with mode: 0644]
Content.Shared/Actions/SharedActionsSystem.cs

index 30f657a2b5f38fe31612407b5ab31d7067a4457a..f05e4455880def7bf8a5d4954d34de17727178bb 100644 (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
index 4d2ac20d12058a59740a36c609e5953834c92f3b..1c76b30075516ea6f34c3edf956a0c02479b1cc2 100644 (file)
@@ -184,10 +184,13 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
         switch (action)
         {
             case WorldTargetActionComponent mapTarget:
-                    return TryTargetWorld(args, actionId, mapTarget, user, comp) || !mapTarget.InteractOnMiss;
+                return TryTargetWorld(args, actionId, mapTarget, user, comp) || !mapTarget.InteractOnMiss;
 
             case EntityTargetActionComponent entTarget:
-                    return TryTargetEntity(args, actionId, entTarget, user, comp) || !entTarget.InteractOnMiss;
+                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()}");
@@ -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)
index 28685858592954d66fe82d970a482351f4722052..36e5920b32561b5c33caeb551d88f28a9f7dbe39 100644 (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--)
index 6cc50bc21b4233e48a150aca21828a06f059f14a..4f1cd6da44aa214e54ff9135afdb0f064a55f58e 100644 (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.
diff --git a/Content.Shared/Actions/EntityWorldTargetActionComponent.cs b/Content.Shared/Actions/EntityWorldTargetActionComponent.cs
new file mode 100644 (file)
index 0000000..3cfa60d
--- /dev/null
@@ -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;
+}
diff --git a/Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs b/Content.Shared/Actions/Events/ValidateActionEntityWorldTargetEvent.cs
new file mode 100644 (file)
index 0000000..57c4702
--- /dev/null
@@ -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);
index 0033078b1b76c3a5c11fe74852aef5749af2e0c5..635d78b8dd939046ed7712dc76ed05c3217805f9 100644 (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 (checkCanInteract && !_actionBlockerSystem.CanInteract(user, null))
             return false;
 
-        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);
@@ -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)