From 6870b88a56190a0916b8c3e70288020f42878152 Mon Sep 17 00:00:00 2001 From: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> Date: Mon, 7 Jun 2021 05:49:43 -0700 Subject: [PATCH] Interaction clean up (#4091) * Rename and clean up interaction events * Fix hand equip events * Refactor duplicate client input validation * Rename Use handler * Move unneeded InRangeUnobstructed methods to extensions only * Clean up UseInteractions * Clean up ActivateItemInWorld * Replace explicit range check with InRangeUnobstructed Remove TransformComponent check, since transform is guaranteed now. * Revert transform check removal * More cleanup * Reorder interaction checks * Rename attack eventargs to interact * Test V1 * Add interaction test * Fix interaction test * Fix container interaction test * Rename interaction methods * Rename player to user and attacked to target * Clean up InteractAfter * Clean up InRangeUnobstructed usages * Rename attack to interact and weapon to used * Changed can't reach message to only play when holding something Cleaned up bracket formatting * Fix Airtight validation check * Remove extra words in comments * Fix FaceClick rotation * Move duplicate map check and face to method * Fix test --- .../Click/InteractionSystemTests.cs | 407 ++++++++++-- .../Combat/Melee/SwingMeleeWeaponOperator.cs | 2 +- .../Combat/Melee/UnarmedCombatOperator.cs | 2 +- .../Inventory/InteractWithEntityOperator.cs | 2 +- .../Inventory/PickupEntityOperator.cs | 2 +- Content.Server/Actions/DisarmAction.cs | 2 +- .../Components/Atmos/AirtightComponent.cs | 2 + .../Components/GUI/HandsComponent.cs | 4 +- .../Components/GUI/InventoryComponent.cs | 2 +- .../GameObjects/EntitySystems/BuckleSystem.cs | 4 +- .../EntitySystems/Click/InteractionSystem.cs | 596 ++++++++---------- .../EntitySystems/SharedInteractionSystem.cs | 271 +------- .../Components/Interaction/IInteractHand.cs | 4 +- .../Utility/SharedUnobstructedExtensions.cs | 57 +- 14 files changed, 662 insertions(+), 695 deletions(-) diff --git a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs index 8eb02a1afb..277fafc554 100644 --- a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs +++ b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs @@ -1,5 +1,6 @@ using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.Components.Weapon.Melee; using Content.Server.GameObjects.EntitySystems.Click; using Content.Shared.Interfaces.GameObjects.Components; using NUnit.Framework; @@ -17,20 +18,300 @@ namespace Content.IntegrationTests.Tests.Interaction.Click [TestOf(typeof(InteractionSystem))] public class InteractionSystemTests : ContentIntegrationTest { - [Reflect(false)] - private class TestAttackEntitySystem : EntitySystem - { - public EntityEventHandler ClickAttackEvent; - public EntityEventHandler InteractUsingEvent; - public EntityEventHandler InteractHandEvent; + const string PROTOTYPES = @" +- type: entity + id: DummyDebugWall + components: + - type: Physics + bodyType: Dynamic + fixtures: + - shape: + !type:PhysShapeAabb + bounds: ""-0.25,-0.25,0.25,0.25"" + layer: + - MobMask + mask: + - MobMask +"; - public override void Initialize() + [Test] + public async Task InteractionTest() + { + var server = StartServerDummyTicker(new ServerContentIntegrationOption { - base.Initialize(); - SubscribeLocalEvent((e) => ClickAttackEvent?.Invoke(e)); - SubscribeLocalEvent((e) => InteractUsingEvent?.Invoke(e)); - SubscribeLocalEvent((e) => InteractHandEvent?.Invoke(e)); - } + ContentBeforeIoC = () => + { + IoCManager.Resolve().LoadExtraSystemType(); + } + }); + + await server.WaitIdleAsync(); + + var entityManager = server.ResolveDependency(); + var mapManager = server.ResolveDependency(); + + var mapId = MapId.Nullspace; + var coords = MapCoordinates.Nullspace; + server.Assert(() => + { + mapId = mapManager.CreateMap(); + coords = new MapCoordinates(Vector2.Zero, mapId); + }); + + await server.WaitIdleAsync(); + IEntity user = null; + IEntity target = null; + IEntity item = null; + + server.Assert(() => + { + user = entityManager.SpawnEntity(null, coords); + user.EnsureComponent().AddHand("hand"); + target = entityManager.SpawnEntity(null, coords); + item = entityManager.SpawnEntity(null, coords); + item.EnsureComponent(); + }); + + await server.WaitRunTicks(1); + + var entitySystemManager = server.ResolveDependency(); + Assert.That(entitySystemManager.TryGetEntitySystem(out var interactionSystem)); + Assert.That(entitySystemManager.TryGetEntitySystem(out var testInteractionSystem)); + + var attack = false; + var interactUsing = false; + var interactHand = false; + server.Assert(() => + { + testInteractionSystem.AttackEvent = (_, _, ev) => { Assert.That(ev.Target, Is.EqualTo(target.Uid)); attack = true; }; + testInteractionSystem.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; }; + testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; + + interactionSystem.DoAttack(user, target.Transform.Coordinates, false, target.Uid); + interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid); + Assert.That(attack); + Assert.That(interactUsing, Is.False); + Assert.That(interactHand); + + Assert.That(user.TryGetComponent(out var hands)); + Assert.That(hands.PutInHand(item.GetComponent())); + + interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid); + Assert.That(interactUsing); + }); + + await server.WaitIdleAsync(); + } + + [Test] + public async Task InteractionObstructionTest() + { + var server = StartServerDummyTicker(new ServerContentIntegrationOption + { + ContentBeforeIoC = () => + { + IoCManager.Resolve().LoadExtraSystemType(); + }, + ExtraPrototypes = PROTOTYPES + }); + + await server.WaitIdleAsync(); + + var entityManager = server.ResolveDependency(); + var mapManager = server.ResolveDependency(); + + var mapId = MapId.Nullspace; + var coords = MapCoordinates.Nullspace; + server.Assert(() => + { + mapId = mapManager.CreateMap(); + coords = new MapCoordinates(Vector2.Zero, mapId); + }); + + await server.WaitIdleAsync(); + IEntity user = null; + IEntity target = null; + IEntity item = null; + IEntity wall = null; + + server.Assert(() => + { + user = entityManager.SpawnEntity(null, coords); + user.EnsureComponent().AddHand("hand"); + target = entityManager.SpawnEntity(null, new MapCoordinates((1.9f, 0), mapId)); + item = entityManager.SpawnEntity(null, coords); + item.EnsureComponent(); + wall = entityManager.SpawnEntity("DummyDebugWall", new MapCoordinates((1, 0), user.Transform.MapID)); + }); + + await server.WaitRunTicks(1); + + var entitySystemManager = server.ResolveDependency(); + Assert.That(entitySystemManager.TryGetEntitySystem(out var interactionSystem)); + Assert.That(entitySystemManager.TryGetEntitySystem(out var testInteractionSystem)); + + var attack = false; + var interactUsing = false; + var interactHand = false; + server.Assert(() => + { + testInteractionSystem.AttackEvent = (_, _, ev) => { Assert.That(ev.Target, Is.EqualTo(target.Uid)); attack = true; }; + testInteractionSystem.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; }; + testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; + + interactionSystem.DoAttack(user, target.Transform.Coordinates, false, target.Uid); + interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid); + Assert.That(attack, Is.False); + Assert.That(interactUsing, Is.False); + Assert.That(interactHand, Is.False); + + Assert.That(user.TryGetComponent(out var hands)); + Assert.That(hands.PutInHand(item.GetComponent())); + + interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid); + Assert.That(interactUsing, Is.False); + }); + + await server.WaitIdleAsync(); + } + + [Test] + public async Task InteractionInRangeTest() + { + var server = StartServerDummyTicker(new ServerContentIntegrationOption + { + ContentBeforeIoC = () => + { + IoCManager.Resolve().LoadExtraSystemType(); + } + }); + + await server.WaitIdleAsync(); + + var entityManager = server.ResolveDependency(); + var mapManager = server.ResolveDependency(); + + var mapId = MapId.Nullspace; + var coords = MapCoordinates.Nullspace; + server.Assert(() => + { + mapId = mapManager.CreateMap(); + coords = new MapCoordinates(Vector2.Zero, mapId); + }); + + await server.WaitIdleAsync(); + IEntity user = null; + IEntity target = null; + IEntity item = null; + + server.Assert(() => + { + user = entityManager.SpawnEntity(null, coords); + user.EnsureComponent().AddHand("hand"); + target = entityManager.SpawnEntity(null, new MapCoordinates((InteractionSystem.InteractionRange - 0.1f, 0), mapId)); + item = entityManager.SpawnEntity(null, coords); + item.EnsureComponent(); + }); + + await server.WaitRunTicks(1); + + var entitySystemManager = server.ResolveDependency(); + Assert.That(entitySystemManager.TryGetEntitySystem(out var interactionSystem)); + Assert.That(entitySystemManager.TryGetEntitySystem(out var testInteractionSystem)); + + var attack = false; + var interactUsing = false; + var interactHand = false; + server.Assert(() => + { + testInteractionSystem.AttackEvent = (_, _, ev) => { Assert.That(ev.Target, Is.EqualTo(target.Uid)); attack = true; }; + testInteractionSystem.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; }; + testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; + + interactionSystem.DoAttack(user, target.Transform.Coordinates, false, target.Uid); + interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid); + Assert.That(attack); + Assert.That(interactUsing, Is.False); + Assert.That(interactHand); + + Assert.That(user.TryGetComponent(out var hands)); + Assert.That(hands.PutInHand(item.GetComponent())); + + interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid); + Assert.That(interactUsing); + }); + + await server.WaitIdleAsync(); + } + + + [Test] + public async Task InteractionOutOfRangeTest() + { + var server = StartServerDummyTicker(new ServerContentIntegrationOption + { + ContentBeforeIoC = () => + { + IoCManager.Resolve().LoadExtraSystemType(); + } + }); + + await server.WaitIdleAsync(); + + var entityManager = server.ResolveDependency(); + var mapManager = server.ResolveDependency(); + + var mapId = MapId.Nullspace; + var coords = MapCoordinates.Nullspace; + server.Assert(() => + { + mapId = mapManager.CreateMap(); + coords = new MapCoordinates(Vector2.Zero, mapId); + }); + + await server.WaitIdleAsync(); + IEntity user = null; + IEntity target = null; + IEntity item = null; + + server.Assert(() => + { + user = entityManager.SpawnEntity(null, coords); + user.EnsureComponent().AddHand("hand"); + target = entityManager.SpawnEntity(null, new MapCoordinates((InteractionSystem.InteractionRange, 0), mapId)); + item = entityManager.SpawnEntity(null, coords); + item.EnsureComponent(); + }); + + await server.WaitRunTicks(1); + + var entitySystemManager = server.ResolveDependency(); + Assert.That(entitySystemManager.TryGetEntitySystem(out var interactionSystem)); + Assert.That(entitySystemManager.TryGetEntitySystem(out var testInteractionSystem)); + + var attack = false; + var interactUsing = false; + var interactHand = false; + server.Assert(() => + { + testInteractionSystem.AttackEvent = (_, _, ev) => { Assert.That(ev.Target, Is.EqualTo(target.Uid)); attack = true; }; + testInteractionSystem.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; }; + testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; + + interactionSystem.DoAttack(user, target.Transform.Coordinates, false, target.Uid); + interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid); + Assert.That(attack, Is.False); + Assert.That(interactUsing, Is.False); + Assert.That(interactHand, Is.False); + + Assert.That(user.TryGetComponent(out var hands)); + Assert.That(hands.PutInHand(item.GetComponent())); + + interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid); + Assert.That(interactUsing, Is.False); + }); + + await server.WaitIdleAsync(); } [Test] @@ -40,8 +321,9 @@ namespace Content.IntegrationTests.Tests.Interaction.Click { ContentBeforeIoC = () => { - IoCManager.Resolve().LoadExtraSystemType(); - } + IoCManager.Resolve().LoadExtraSystemType(); + }, + FailureLogLevel = Robust.Shared.Log.LogLevel.Error }); await server.WaitIdleAsync(); @@ -49,23 +331,38 @@ namespace Content.IntegrationTests.Tests.Interaction.Click var entityManager = server.ResolveDependency(); var mapManager = server.ResolveDependency(); - IEntity origin = null; - IEntity other = null; + var mapId = MapId.Nullspace; + var coords = MapCoordinates.Nullspace; + server.Assert(() => + { + mapId = mapManager.CreateMap(); + coords = new MapCoordinates(Vector2.Zero, mapId); + }); + + await server.WaitIdleAsync(); + IEntity user = null; + IEntity target = null; + IEntity item = null; IEntity containerEntity = null; IContainer container = null; server.Assert(() => { - var mapId = mapManager.CreateMap(); - var coordinates = new MapCoordinates(Vector2.Zero, mapId); - - origin = entityManager.SpawnEntity(null, coordinates); - origin.EnsureComponent().AddHand("hand"); - other = entityManager.SpawnEntity(null, coordinates); - containerEntity = entityManager.SpawnEntity(null, coordinates); + user = entityManager.SpawnEntity(null, coords); + user.EnsureComponent().AddHand("hand"); + target = entityManager.SpawnEntity(null, coords); + item = entityManager.SpawnEntity(null, coords); + item.EnsureComponent(); + containerEntity = entityManager.SpawnEntity(null, coords); container = ContainerHelpers.EnsureContainer(containerEntity, "InteractionTestContainer"); }); + await server.WaitRunTicks(1); + + var entitySystemManager = server.ResolveDependency(); + Assert.That(entitySystemManager.TryGetEntitySystem(out var interactionSystem)); + Assert.That(entitySystemManager.TryGetEntitySystem(out var testInteractionSystem)); + await server.WaitIdleAsync(); var attack = false; @@ -73,53 +370,53 @@ namespace Content.IntegrationTests.Tests.Interaction.Click var interactHand = false; server.Assert(() => { - Assert.That(container.Insert(origin)); - Assert.That(origin.Transform.Parent!.Owner, Is.EqualTo(containerEntity)); + Assert.That(container.Insert(user)); + Assert.That(user.Transform.Parent.Owner, Is.EqualTo(containerEntity)); - var entitySystemManager = IoCManager.Resolve(); - Assert.That(entitySystemManager.TryGetEntitySystem(out var interactionSystem)); + testInteractionSystem.AttackEvent = (_, _, ev) => { Assert.That(ev.Target, Is.EqualTo(containerEntity.Uid)); attack = true; }; + testInteractionSystem.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(containerEntity)); interactUsing = true; }; + testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(containerEntity)); interactHand = true; }; - Assert.That(entitySystemManager.TryGetEntitySystem(out var testAttackEntitySystem)); - testAttackEntitySystem.ClickAttackEvent = (ev) => - { - Assert.That(ev.Target, Is.EqualTo(containerEntity.Uid)); - attack = true; - }; - testAttackEntitySystem.InteractUsingEvent = (ev) => - { - Assert.That(ev.Target, Is.EqualTo(containerEntity)); - interactUsing = true; - }; - testAttackEntitySystem.InteractHandEvent = (ev) => - { - Assert.That(ev.Target, Is.EqualTo(containerEntity)); - interactHand = true; - }; - - interactionSystem.DoAttack(origin, other.Transform.Coordinates, false, other.Uid); - interactionSystem.UserInteraction(origin, other.Transform.Coordinates, other.Uid); + interactionSystem.DoAttack(user, target.Transform.Coordinates, false, target.Uid); + interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid); Assert.That(attack, Is.False); Assert.That(interactUsing, Is.False); Assert.That(interactHand, Is.False); - interactionSystem.DoAttack(origin, containerEntity.Transform.Coordinates, false, containerEntity.Uid); - interactionSystem.UserInteraction(origin, containerEntity.Transform.Coordinates, containerEntity.Uid); + interactionSystem.DoAttack(user, containerEntity.Transform.Coordinates, false, containerEntity.Uid); + interactionSystem.UserInteraction(user, containerEntity.Transform.Coordinates, containerEntity.Uid); Assert.That(attack); Assert.That(interactUsing, Is.False); Assert.That(interactHand); - var itemEntity = entityManager.SpawnEntity(null, origin.Transform.Coordinates); - var item = itemEntity.EnsureComponent(); + Assert.That(user.TryGetComponent(out var hands)); + Assert.That(hands.PutInHand(item.GetComponent())); - Assert.That(origin.TryGetComponent(out var hands)); - hands.PutInHand(item); - - interactionSystem.UserInteraction(origin, other.Transform.Coordinates, other.Uid); + interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid); Assert.That(interactUsing, Is.False); - interactionSystem.UserInteraction(origin, containerEntity.Transform.Coordinates, containerEntity.Uid); + interactionSystem.UserInteraction(user, containerEntity.Transform.Coordinates, containerEntity.Uid); Assert.That(interactUsing, Is.True); }); + + await server.WaitIdleAsync(); } + + [Reflect(false)] + private class TestInteractionSystem : EntitySystem + { + public ComponentEventHandler AttackEvent; + public EntityEventHandler InteractUsingEvent; + public EntityEventHandler InteractHandEvent; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent((u, c, e) => AttackEvent?.Invoke(u, c, e)); + SubscribeLocalEvent((e) => InteractUsingEvent?.Invoke(e)); + SubscribeLocalEvent((e) => InteractHandEvent?.Invoke(e)); + } + } + } } diff --git a/Content.Server/AI/Operators/Combat/Melee/SwingMeleeWeaponOperator.cs b/Content.Server/AI/Operators/Combat/Melee/SwingMeleeWeaponOperator.cs index 36e0f1d9ca..199de1b2c0 100644 --- a/Content.Server/AI/Operators/Combat/Melee/SwingMeleeWeaponOperator.cs +++ b/Content.Server/AI/Operators/Combat/Melee/SwingMeleeWeaponOperator.cs @@ -78,7 +78,7 @@ namespace Content.Server.AI.Operators.Combat.Melee var interactionSystem = IoCManager.Resolve().GetEntitySystem(); - interactionSystem.UseItemInHand(_owner, _target.Transform.Coordinates, _target.Uid); + interactionSystem.AiUseInteraction(_owner, _target.Transform.Coordinates, _target.Uid); _elapsedTime += frameTime; return Outcome.Continuing; } diff --git a/Content.Server/AI/Operators/Combat/Melee/UnarmedCombatOperator.cs b/Content.Server/AI/Operators/Combat/Melee/UnarmedCombatOperator.cs index 18695812bb..0a0f1028f3 100644 --- a/Content.Server/AI/Operators/Combat/Melee/UnarmedCombatOperator.cs +++ b/Content.Server/AI/Operators/Combat/Melee/UnarmedCombatOperator.cs @@ -83,7 +83,7 @@ namespace Content.Server.AI.Operators.Combat.Melee } var interactionSystem = IoCManager.Resolve().GetEntitySystem(); - interactionSystem.UseItemInHand(_owner, _target.Transform.Coordinates, _target.Uid); + interactionSystem.AiUseInteraction(_owner, _target.Transform.Coordinates, _target.Uid); _elapsedTime += frameTime; return Outcome.Continuing; } diff --git a/Content.Server/AI/Operators/Inventory/InteractWithEntityOperator.cs b/Content.Server/AI/Operators/Inventory/InteractWithEntityOperator.cs index ced5bc0cef..17d3b919b4 100644 --- a/Content.Server/AI/Operators/Inventory/InteractWithEntityOperator.cs +++ b/Content.Server/AI/Operators/Inventory/InteractWithEntityOperator.cs @@ -40,7 +40,7 @@ namespace Content.Server.AI.Operators.Inventory // Click on da thing var interactionSystem = IoCManager.Resolve().GetEntitySystem(); - interactionSystem.UseItemInHand(_owner, _useTarget.Transform.Coordinates, _useTarget.Uid); + interactionSystem.AiUseInteraction(_owner, _useTarget.Transform.Coordinates, _useTarget.Uid); return Outcome.Success; } diff --git a/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs b/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs index 660387da25..e817415e5b 100644 --- a/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs +++ b/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs @@ -58,7 +58,7 @@ namespace Content.Server.AI.Operators.Inventory } var interactionSystem = IoCManager.Resolve().GetEntitySystem(); - interactionSystem.Interaction(_owner, _target); + interactionSystem.InteractHand(_owner, _target); return Outcome.Success; } } diff --git a/Content.Server/Actions/DisarmAction.cs b/Content.Server/Actions/DisarmAction.cs index 814192befd..9d8f4e45e2 100644 --- a/Content.Server/Actions/DisarmAction.cs +++ b/Content.Server/Actions/DisarmAction.cs @@ -47,7 +47,7 @@ namespace Content.Server.Actions var player = actor.PlayerSession; var coordinates = args.Target.Transform.Coordinates; var target = args.Target.Uid; - EntitySystem.Get().HandleClientUseItemInHand(player, coordinates, target); + EntitySystem.Get().HandleUseInteraction(player, coordinates, target); return; } diff --git a/Content.Server/GameObjects/Components/Atmos/AirtightComponent.cs b/Content.Server/GameObjects/Components/Atmos/AirtightComponent.cs index 86658270f7..d0633202b5 100644 --- a/Content.Server/GameObjects/Components/Atmos/AirtightComponent.cs +++ b/Content.Server/GameObjects/Components/Atmos/AirtightComponent.cs @@ -161,6 +161,8 @@ namespace Content.Server.GameObjects.Components.Atmos { if (Owner.Transform.Anchored) { + if (!Owner.Transform.GridID.IsValid()) + return; var grid = _mapManager.GetGrid(Owner.Transform.GridID); UpdatePosition(Owner.Transform.GridID, grid.TileIndicesFor(Owner.Transform.Coordinates)); } diff --git a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs index 244fc0a7ff..7bb337a8a6 100644 --- a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs +++ b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs @@ -664,13 +664,13 @@ namespace Content.Server.GameObjects.Components.GUI var interactionSystem = _entitySystemManager.GetEntitySystem(); if (used != null) { - await interactionSystem.Interaction(Owner, used, hand.Entity, + await interactionSystem.InteractUsing(Owner, used, hand.Entity, EntityCoordinates.Invalid); } else { var entity = hand.Entity; - interactionSystem.Interaction(Owner, entity); + interactionSystem.InteractHand(Owner, entity); } } diff --git a/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs b/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs index 101e678b9a..4ca58b50bb 100644 --- a/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs +++ b/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs @@ -539,7 +539,7 @@ namespace Content.Server.GameObjects.Components.GUI { if (activeHand != null) { - await interactionSystem.Interaction(Owner, activeHand.Owner, itemContainedInSlot.Owner, + await interactionSystem.InteractUsing(Owner, activeHand.Owner, itemContainedInSlot.Owner, new EntityCoordinates()); } else if (Unequip(msg.Inventoryslot)) diff --git a/Content.Server/GameObjects/EntitySystems/BuckleSystem.cs b/Content.Server/GameObjects/EntitySystems/BuckleSystem.cs index 20c0e7b712..77a5f9b4bd 100644 --- a/Content.Server/GameObjects/EntitySystems/BuckleSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/BuckleSystem.cs @@ -31,10 +31,10 @@ namespace Content.Server.GameObjects.EntitySystems SubscribeLocalEvent(ContainerModifiedBuckle); SubscribeLocalEvent(ContainerModifiedStrap); - SubscribeLocalEvent(HandleAttackHand); + SubscribeLocalEvent(HandleAttackHand); } - private void HandleAttackHand(EntityUid uid, BuckleComponent component, AttackHandEvent args) + private void HandleAttackHand(EntityUid uid, BuckleComponent component, InteractHandEvent args) { args.Handled = component.TryUnbuckle(args.User); } diff --git a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs index 676adbad9e..726cfff1a7 100644 --- a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Content.Server.GameObjects.Components.Items.Storage; @@ -29,6 +30,8 @@ using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Players; using Robust.Shared.Random; +using Robust.Shared.Localization; +using Content.Shared.Interfaces; namespace Content.Server.GameObjects.EntitySystems.Click { @@ -43,16 +46,17 @@ namespace Content.Server.GameObjects.EntitySystems.Click public override void Initialize() { - SubscribeNetworkEvent(HandleDragDropMessage); + SubscribeNetworkEvent(HandleDragDropRequestEvent); CommandBinds.Builder .Bind(EngineKeyFunctions.Use, - new PointerInputCmdHandler(HandleClientUseItemInHand)) + new PointerInputCmdHandler(HandleUseInteraction)) .Bind(ContentKeyFunctions.WideAttack, new PointerInputCmdHandler(HandleWideAttack)) .Bind(ContentKeyFunctions.ActivateItemInWorld, new PointerInputCmdHandler(HandleActivateItemInWorld)) - .Bind(ContentKeyFunctions.TryPullObject, new PointerInputCmdHandler(HandleTryPullObject)) + .Bind(ContentKeyFunctions.TryPullObject, + new PointerInputCmdHandler(HandleTryPullObject)) .Register(); } @@ -62,19 +66,57 @@ namespace Content.Server.GameObjects.EntitySystems.Click base.Shutdown(); } - private void HandleDragDropMessage(DragDropRequestEvent msg, EntitySessionEventArgs args) + #region Client Input Validation + private bool ValidateClientInput(ICommonSession? session, EntityCoordinates coords, EntityUid uid, [NotNullWhen(true)] out IEntity? userEntity) { - var performer = args.SenderSession.AttachedEntity; + userEntity = null; - if (performer == null) return; - if (!EntityManager.TryGetEntity(msg.Dropped, out var dropped)) return; - if (!EntityManager.TryGetEntity(msg.Target, out var target)) return; + if (!coords.IsValid(_entityManager)) + { + Logger.InfoS("system.interaction", $"Invalid Coordinates: client={session}, coords={coords}"); + return false; + } - var interactionArgs = new DragDropEvent(performer, msg.DropLocation, dropped, target); + if (uid.IsClientSide()) + { + Logger.WarningS("system.interaction", + $"Client sent interaction with client-side entity. Session={session}, Uid={uid}"); + return false; + } + + userEntity = ((IPlayerSession?) session)?.AttachedEntity; + + if (userEntity == null || !userEntity.IsValid()) + { + Logger.WarningS("system.interaction", + $"Client sent interaction with no attached entity. Session={session}"); + return false; + } + + return true; + } + #endregion + + #region Drag drop + private void HandleDragDropRequestEvent(DragDropRequestEvent msg, EntitySessionEventArgs args) + { + if (!ValidateClientInput(args.SenderSession, msg.DropLocation, msg.Target, out var userEntity)) + { + Logger.InfoS("system.interaction", $"DragDropRequestEvent input validation failed"); + return; + } + + if (!EntityManager.TryGetEntity(msg.Dropped, out var dropped)) + return; + if (!EntityManager.TryGetEntity(msg.Target, out var target)) + return; + + var interactionArgs = new DragDropEvent(userEntity, msg.DropLocation, dropped, target); // must be in range of both the target and the object they are drag / dropping // Client also does this check but ya know we gotta validate it. - if (!interactionArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return; + if (!interactionArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) + return; // trigger dragdrops on the dropped entity RaiseLocalEvent(dropped.Uid, interactionArgs); @@ -98,25 +140,21 @@ namespace Content.Server.GameObjects.EntitySystems.Click } } } + #endregion + #region ActivateItemInWorld private bool HandleActivateItemInWorld(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { + if (!ValidateClientInput(session, coords, uid, out var user)) + { + Logger.InfoS("system.interaction", $"ActivateItemInWorld input validation failed"); + return false; + } + if (!EntityManager.TryGetEntity(uid, out var used)) return false; - var playerEnt = ((IPlayerSession?) session)?.AttachedEntity; - - if (playerEnt == null || !playerEnt.IsValid()) - { - return false; - } - - if (!playerEnt.Transform.Coordinates.InRange(EntityManager, used.Transform.Coordinates, InteractionRange)) - { - return false; - } - - InteractionActivate(playerEnt, used); + InteractionActivate(user, used); return true; } @@ -126,61 +164,45 @@ namespace Content.Server.GameObjects.EntitySystems.Click /// public void TryInteractionActivate(IEntity? user, IEntity? used) { - if (user != null && used != null && ActionBlockerSystem.CanUse(user)) - { - InteractionActivate(user, used); - } + if (user == null || used == null) + return; + + InteractionActivate(user, used); } private void InteractionActivate(IEntity user, IEntity used) { + if (!ActionBlockerSystem.CanInteract(user) || ! ActionBlockerSystem.CanUse(user)) + return; + + // all activates should only fire when in range / unbostructed + if (!InRangeUnobstructed(user, used, ignoreInsideBlocker: true, popup: true)) + return; + var activateMsg = new ActivateInWorldEvent(user, used); RaiseLocalEvent(used.Uid, activateMsg); if (activateMsg.Handled) - { return; - } if (!used.TryGetComponent(out IActivate? activateComp)) - { return; - } - // all activates should only fire when in range / unbostructed var activateEventArgs = new ActivateEventArgs(user, used); - if (activateEventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) - { - activateComp.Activate(activateEventArgs); - } + activateComp.Activate(activateEventArgs); } + #endregion private bool HandleWideAttack(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { // client sanitization - if (!coords.IsValid(_entityManager)) - { - Logger.InfoS("system.interaction", $"Invalid Coordinates: client={session}, coords={coords}"); - return true; - } - - if (uid.IsClientSide()) - { - Logger.WarningS("system.interaction", - $"Client sent attack with client-side entity. Session={session}, Uid={uid}"); - return true; - } - - var userEntity = ((IPlayerSession?) session)?.AttachedEntity; - - if (userEntity == null || !userEntity.IsValid()) + if (!ValidateClientInput(session, coords, uid, out var userEntity)) { + Logger.InfoS("system.interaction", $"WideAttack input validation failed"); return true; } if (userEntity.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode) - { DoAttack(userEntity, coords, true); - } return true; } @@ -192,222 +214,144 @@ namespace Content.Server.GameObjects.EntitySystems.Click /// /// /// - internal void UseItemInHand(IEntity entity, EntityCoordinates coords, EntityUid uid) + internal void AiUseInteraction(IEntity entity, EntityCoordinates coords, EntityUid uid) { if (entity.HasComponent()) - { throw new InvalidOperationException(); - } - if (entity.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode) - { - DoAttack(entity, coords, false, uid); - } - else - { - UserInteraction(entity, coords, uid); - } + UserInteraction(entity, coords, uid); } - public bool HandleClientUseItemInHand(ICommonSession? session, EntityCoordinates coords, EntityUid uid) + public bool HandleUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { // client sanitization - if (!coords.IsValid(_entityManager)) + if (!ValidateClientInput(session, coords, uid, out var userEntity)) { - Logger.InfoS("system.interaction", $"Invalid Coordinates: client={session}, coords={coords}"); + Logger.InfoS("system.interaction", $"Use input validation failed"); return true; } - if (uid.IsClientSide()) - { - Logger.WarningS("system.interaction", - $"Client sent interaction with client-side entity. Session={session}, Uid={uid}"); - return true; - } - - var userEntity = ((IPlayerSession?) session)?.AttachedEntity; - - if (userEntity == null || !userEntity.IsValid()) - { - return true; - } - - if (userEntity.TryGetComponent(out CombatModeComponent? combat) && combat.IsInCombatMode) - DoAttack(userEntity, coords, false, uid); - else - UserInteraction(userEntity, coords, uid); + UserInteraction(userEntity, coords, uid); return true; } private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { - // client sanitization - if (!coords.IsValid(_entityManager)) + if (!ValidateClientInput(session, coords, uid, out var userEntity)) { - Logger.InfoS("system.interaction", $"Invalid Coordinates for pulling: client={session}, coords={coords}"); - return false; + Logger.InfoS("system.interaction", $"TryPullObject input validation failed"); + return true; } - if (uid.IsClientSide()) - { - Logger.WarningS("system.interaction", - $"Client sent pull interaction with client-side entity. Session={session}, Uid={uid}"); + if (userEntity.Uid == uid) return false; - } - - var player = session?.AttachedEntity; - - if (player == null) - { - Logger.WarningS("system.interaction", - $"Client sent pulling interaction with no attached entity. Session={session}, Uid={uid}"); - return false; - } if (!EntityManager.TryGetEntity(uid, out var pulledObject)) - { return false; - } - if (player == pulledObject) - { + if (!InRangeUnobstructed(userEntity, pulledObject, popup: true)) return false; - } if (!pulledObject.TryGetComponent(out PullableComponent? pull)) - { return false; - } - var dist = player.Transform.Coordinates.Position - pulledObject.Transform.Coordinates.Position; - if (dist.LengthSquared > InteractionRangeSquared) - { - return false; - } - - return pull.TogglePull(player); + return pull.TogglePull(userEntity); } - public async void UserInteraction(IEntity player, EntityCoordinates coordinates, EntityUid clickedUid) + public async void UserInteraction(IEntity user, EntityCoordinates coordinates, EntityUid clickedUid) { - // Get entity clicked upon from UID if valid UID, if not assume no entity clicked upon and null - if (!EntityManager.TryGetEntity(clickedUid, out var attacked)) - { - attacked = null; - } - - // Verify player has a transform component - if (!player.TryGetComponent(out var playerTransform)) + if (user.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode) { + DoAttack(user, coordinates, false, clickedUid); return; } - // Verify player is on the same map as the entity he clicked on - if (coordinates.GetMapId(_entityManager) != playerTransform.MapID) + if (!ValidateInteractAndFace(user, coordinates)) + return; + + if (!ActionBlockerSystem.CanInteract(user)) + return; + + // Get entity clicked upon from UID if valid UID, if not assume no entity clicked upon and null + EntityManager.TryGetEntity(clickedUid, out var target); + + // Check if interacted entity is in the same container, the direct child, or direct parent of the user. + if (target != null && !user.IsInSameOrParentContainer(target)) { Logger.WarningS("system.interaction", - $"Player named {player.Name} clicked on a map he isn't located on"); + $"User entity named {user.Name} clicked on object {target.Name} that isn't the parent, child, or in the same container"); return; } - // Verify player has a hand, and find what object he is currently holding in his active hand - if (!player.TryGetComponent(out var hands)) - { + // Verify user has a hand, and find what object he is currently holding in his active hand + if (!user.TryGetComponent(out var hands)) return; - } var item = hands.GetActiveHand?.Owner; - ClickFace(player, coordinates); - - if (!ActionBlockerSystem.CanInteract(player)) + // TODO: Replace with body interaction range when we get something like arm length or telekinesis or something. + var inRangeUnobstructed = user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true); + if (target == null || !inRangeUnobstructed) { - return; - } - - // In a container where the attacked entity is not the container's owner - if (player.TryGetContainer(out var playerContainer) && - attacked != playerContainer.Owner) - { - // Either the attacked entity is null, not contained or in a different container - if (attacked == null || - !attacked.TryGetContainer(out var attackedContainer) || - attackedContainer != playerContainer) - { + if (item == null) return; - } - } - // TODO: Check if client should be able to see that object to click on it in the first place - - // Clicked on empty space behavior, try using ranged attack - if (attacked == null) - { - if (item != null) + if (!await InteractUsingRanged(user, item, target, coordinates, inRangeUnobstructed) && + !inRangeUnobstructed) { - // After attack: Check if we clicked on an empty location, if so the only interaction we can do is AfterInteract - var distSqrt = (playerTransform.WorldPosition - coordinates.ToMapPos(EntityManager)).LengthSquared; - InteractAfter(player, item, coordinates, distSqrt <= InteractionRangeSquared); + var message = Loc.GetString("You can't reach there!"); + user.PopupMessage(message); } return; } - - // Verify attacked object is on the map if we managed to click on it somehow - if (!attacked.Transform.IsMapTransform) - { - Logger.WarningS("system.interaction", - $"Player named {player.Name} clicked on object {attacked.Name} that isn't currently on the map somehow"); - return; - } - - // RangedInteract/AfterInteract: Check distance between user and clicked item, if too large parse it in the ranged function - // TODO: have range based upon the item being used? or base it upon some variables of the player himself? - var distance = (playerTransform.WorldPosition - attacked.Transform.WorldPosition).LengthSquared; - if (distance > InteractionRangeSquared) - { - if (item != null) - { - RangedInteraction(player, item, attacked, coordinates); - return; - } - - return; // Add some form of ranged InteractHand here if you need it someday, or perhaps just ways to modify the range of InteractHand - } - - // We are close to the nearby object and the object isn't contained in our active hand - // InteractUsing/AfterInteract: We will either use the item on the nearby object - if (item != null) - { - await Interaction(player, item, attacked, coordinates); - } - // InteractHand/Activate: Since our hand is empty we will use InteractHand/Activate else { - Interaction(player, attacked); + // We are close to the nearby object and the object isn't contained in our active hand + // InteractUsing/AfterInteract: We will either use the item on the nearby object + if (item != null) + await InteractUsing(user, item, target, coordinates); + // InteractHand/Activate: Since our hand is empty we will use InteractHand/Activate + else + InteractHand(user, target); } } - private void ClickFace(IEntity player, EntityCoordinates coordinates) + private bool ValidateInteractAndFace(IEntity user, EntityCoordinates coordinates) { - var diff = coordinates.ToMapPos(EntityManager) - player.Transform.MapPosition.Position; + // Verify user is on the same map as the entity he clicked on + if (coordinates.GetMapId(_entityManager) != user.Transform.MapID) + { + Logger.WarningS("system.interaction", + $"User entity named {user.Name} clicked on a map he isn't located on"); + return false; + } + + FaceClickCoordinates(user, coordinates); + + return true; + } + + private void FaceClickCoordinates(IEntity user, EntityCoordinates coordinates) + { + var diff = coordinates.ToMapPos(EntityManager) - user.Transform.MapPosition.Position; if (diff.LengthSquared <= 0.01f) return; var diffAngle = Angle.FromWorldVec(diff); - if (ActionBlockerSystem.CanChangeDirection(player)) + if (ActionBlockerSystem.CanChangeDirection(user)) { - player.Transform.LocalRotation = diffAngle; + user.Transform.WorldRotation = diffAngle; } else { - if (player.TryGetComponent(out BuckleComponent? buckle) && (buckle.BuckledTo != null)) + if (user.TryGetComponent(out BuckleComponent? buckle) && (buckle.BuckledTo != null)) { // We're buckled to another object. Is that object rotatable? if (buckle.BuckledTo!.Owner.TryGetComponent(out SharedRotatableComponent? rotatable) && rotatable.RotateWhileAnchored) { - // Note the assumption that even if unanchored, player can only do spinnychair with an "independent wheel". - // (Since the player being buckled to it holds it down with their weight.) + // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel". + // (Since the user being buckled to it holds it down with their weight.) // This is logically equivalent to RotateWhileAnchored. // Barstools and office chairs have independent wheels, while regular chairs don't. rotatable.Owner.Transform.LocalRotation = diffAngle; @@ -419,90 +363,87 @@ namespace Content.Server.GameObjects.EntitySystems.Click /// /// We didn't click on any entity, try doing an AfterInteract on the click location /// - private async void InteractAfter(IEntity user, IEntity weapon, EntityCoordinates clickLocation, bool canReach) + private async Task InteractDoAfter(IEntity user, IEntity used, IEntity? target, EntityCoordinates clickLocation, bool canReach) { - var message = new AfterInteractEvent(user, weapon, null, clickLocation, canReach); - RaiseLocalEvent(weapon.Uid, message); - if (message.Handled) + var afterInteractEvent = new AfterInteractEvent(user, used, target, clickLocation, canReach); + RaiseLocalEvent(used.Uid, afterInteractEvent, false); + if (afterInteractEvent.Handled) + return true; + + var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, target, canReach); + var afterInteracts = used.GetAllComponents().OrderByDescending(x => x.Priority).ToList(); + + foreach (var afterInteract in afterInteracts) { - return; + if (await afterInteract.AfterInteract(afterInteractEventArgs)) + return true; } - var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, null, canReach); - await DoAfterInteract(weapon, afterInteractEventArgs); + return false; } /// - /// Uses a weapon/object on an entity + /// Uses a item/object on an entity /// Finds components with the InteractUsing interface and calls their function + /// NOTE: Does not have an InRangeUnobstructed check /// - public async Task Interaction(IEntity user, IEntity weapon, IEntity attacked, EntityCoordinates clickLocation) + public async Task InteractUsing(IEntity user, IEntity used, IEntity target, EntityCoordinates clickLocation) { - var attackMsg = new InteractUsingEvent(user, weapon, attacked, clickLocation); - RaiseLocalEvent(attacked.Uid, attackMsg); - if (attackMsg.Handled) + if (!ActionBlockerSystem.CanInteract(user)) return; - var attackBys = attacked.GetAllComponents().OrderByDescending(x => x.Priority); - var attackByEventArgs = new InteractUsingEventArgs(user, clickLocation, weapon, attacked); - - // all AttackBys should only happen when in range / unobstructed, so no range check is needed - if (attackByEventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) - { - foreach (var attackBy in attackBys) - { - if (await attackBy.InteractUsing(attackByEventArgs)) - { - // If an InteractUsing returns a status completion we finish our attack - return; - } - } - } - - var afterAtkMsg = new AfterInteractEvent(user, weapon, attacked, clickLocation, true); - RaiseLocalEvent(weapon.Uid, afterAtkMsg, false); - if (afterAtkMsg.Handled) - { + // all interactions should only happen when in range / unobstructed, so no range check is needed + var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation); + RaiseLocalEvent(target.Uid, interactUsingEvent); + if (interactUsingEvent.Handled) return; + + var interactUsingEventArgs = new InteractUsingEventArgs(user, clickLocation, used, target); + + var interactUsings = target.GetAllComponents().OrderByDescending(x => x.Priority); + foreach (var interactUsing in interactUsings) + { + // If an InteractUsing returns a status completion we finish our interaction + if (await interactUsing.InteractUsing(interactUsingEventArgs)) + return; } - // If we aren't directly attacking the nearby object, lets see if our item has an after attack we can do - var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: true); - - await DoAfterInteract(weapon, afterAttackEventArgs); + // If we aren't directly interacting with the nearby object, lets see if our item has an after interact we can do + await InteractDoAfter(user, used, target, clickLocation, true); } /// /// Uses an empty hand on an entity /// Finds components with the InteractHand interface and calls their function + /// NOTE: Does not have an InRangeUnobstructed check /// - public void Interaction(IEntity user, IEntity attacked) + public void InteractHand(IEntity user, IEntity target) { - var message = new AttackHandEvent(user, attacked); - RaiseLocalEvent(attacked.Uid, message); + if (!ActionBlockerSystem.CanInteract(user)) + return; + + // all interactions should only happen when in range / unobstructed, so no range check is needed + var message = new InteractHandEvent(user, target); + RaiseLocalEvent(target.Uid, message); if (message.Handled) return; - var attackHandEventArgs = new InteractHandEventArgs(user, attacked); + var interactHandEventArgs = new InteractHandEventArgs(user, target); - // all attackHands should only fire when in range / unobstructed - if (attackHandEventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) + var interactHandComps = target.GetAllComponents().ToList(); + foreach (var interactHandComp in interactHandComps) { - var attackHands = attacked.GetAllComponents().ToList(); - foreach (var attackHand in attackHands) - { - if (attackHand.InteractHand(attackHandEventArgs)) - { - // If an InteractHand returns a status completion we finish our attack - return; - } - } + // If an InteractHand returns a status completion we finish our interaction + if (interactHandComp.InteractHand(interactHandEventArgs)) + return; } // Else we run Activate. - InteractionActivate(user, attacked); + InteractionActivate(user, target); } + #region Hands + #region Use /// /// Activates the IUse behaviors of an entity /// Verifies that the user is capable of doing the use interaction first @@ -534,23 +475,21 @@ namespace Content.Server.GameObjects.EntitySystems.Click var useMsg = new UseInHandEvent(user, used); RaiseLocalEvent(used.Uid, useMsg); if (useMsg.Handled) - { return; - } var uses = used.GetAllComponents().ToList(); // Try to use item on any components which have the interface foreach (var use in uses) { + // If a Use returns a status completion we finish our interaction if (use.UseEntity(new UseEntityEventArgs(user))) - { - // If a Use returns a status completion we finish our attack return; - } } } + #endregion + #region Throw /// /// Activates the Throw behavior of an object /// Verifies that the user is capable of doing the throw interaction first @@ -572,9 +511,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click var throwMsg = new ThrownEvent(user, thrown); RaiseLocalEvent(thrown.Uid, throwMsg); if (throwMsg.Handled) - { return; - } var comps = thrown.GetAllComponents().ToList(); var args = new ThrownEventArgs(user); @@ -585,7 +522,9 @@ namespace Content.Server.GameObjects.EntitySystems.Click comp.Thrown(args); } } + #endregion + #region Equip /// /// Calls Equipped on all components that implement the IEquipped interface /// on an entity that has been equipped. @@ -595,9 +534,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click var equipMsg = new EquippedEvent(user, equipped, slot); RaiseLocalEvent(equipped.Uid, equipMsg); if (equipMsg.Handled) - { return; - } var comps = equipped.GetAllComponents().ToList(); @@ -617,9 +554,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click var unequipMsg = new UnequippedEvent(user, equipped, slot); RaiseLocalEvent(equipped.Uid, unequipMsg); if (unequipMsg.Handled) - { return; - } var comps = equipped.GetAllComponents().ToList(); @@ -630,6 +565,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click } } + #region Equip Hand /// /// Calls EquippedHand on all components that implement the IEquippedHand interface /// on an item. @@ -639,9 +575,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click var equippedHandMessage = new EquippedHandEvent(user, item, hand); RaiseLocalEvent(item.Uid, equippedHandMessage); if (equippedHandMessage.Handled) - { return; - } var comps = item.GetAllComponents().ToList(); @@ -660,9 +594,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click var unequippedHandMessage = new UnequippedHandEvent(user, item, hand); RaiseLocalEvent(item.Uid, unequippedHandMessage); if (unequippedHandMessage.Handled) - { return; - } var comps = item.GetAllComponents().ToList(); @@ -671,7 +603,10 @@ namespace Content.Server.GameObjects.EntitySystems.Click comp.UnequippedHand(new UnequippedHandEventArgs(user, hand)); } } + #endregion + #endregion + #region Drop /// /// Activates the Dropped behavior of an object /// Verifies that the user is capable of doing the drop interaction first @@ -693,9 +628,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click var dropMsg = new DroppedEvent(user, item, intentional); RaiseLocalEvent(item.Uid, dropMsg); if (dropMsg.Handled) - { return; - } item.Transform.LocalRotation = intentional ? Angle.Zero : (_random.Next(0, 100) / 100f) * MathHelper.TwoPi; @@ -707,7 +640,9 @@ namespace Content.Server.GameObjects.EntitySystems.Click comp.Dropped(new DroppedEventArgs(user, intentional)); } } + #endregion + #region Hand Selected /// /// Calls HandSelected on all components that implement the IHandSelected interface /// on an item entity on a hand that has just been selected. @@ -717,9 +652,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click var handSelectedMsg = new HandSelectedEvent(user, item); RaiseLocalEvent(item.Uid, handSelectedMsg); if (handSelectedMsg.Handled) - { return; - } var comps = item.GetAllComponents().ToList(); @@ -739,9 +672,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click var handDeselectedMsg = new HandDeselectedEvent(user, item); RaiseLocalEvent(item.Uid, handDeselectedMsg); if (handDeselectedMsg.Handled) - { return; - } var comps = item.GetAllComponents().ToList(); @@ -751,88 +682,70 @@ namespace Content.Server.GameObjects.EntitySystems.Click comp.HandDeselected(new HandDeselectedEventArgs(user)); } } + #endregion + #endregion /// - /// Will have two behaviors, either "uses" the weapon at range on the entity if it is capable of accepting that action - /// Or it will use the weapon itself on the position clicked, regardless of what was there + /// Will have two behaviors, either "uses" the used entity at range on the target entity if it is capable of accepting that action + /// Or it will use the used entity itself on the position clicked, regardless of what was there /// - public async void RangedInteraction(IEntity user, IEntity weapon, IEntity attacked, EntityCoordinates clickLocation) + public async Task InteractUsingRanged(IEntity user, IEntity used, IEntity? target, EntityCoordinates clickLocation, bool inRangeUnobstructed) { - var rangedMsg = new RangedInteractEvent(user, weapon, attacked, clickLocation); - RaiseLocalEvent(attacked.Uid, rangedMsg); - if (rangedMsg.Handled) - return; - - var rangedAttackBys = attacked.GetAllComponents().ToList(); - var rangedAttackByEventArgs = new RangedInteractEventArgs(user, weapon, clickLocation); - - // See if we have a ranged attack interaction - foreach (var t in rangedAttackBys) + if (target != null) { - if (t.RangedInteract(rangedAttackByEventArgs)) + var rangedMsg = new RangedInteractEvent(user, used, target, clickLocation); + RaiseLocalEvent(target.Uid, rangedMsg); + if (rangedMsg.Handled) + return true; + + var rangedInteractions = target.GetAllComponents().ToList(); + var rangedInteractionEventArgs = new RangedInteractEventArgs(user, used, clickLocation); + + // See if we have a ranged interaction + foreach (var t in rangedInteractions) { - // If an InteractUsing returns a status completion we finish our attack - return; + // If an InteractUsingRanged returns a status completion we finish our interaction + if (t.RangedInteract(rangedInteractionEventArgs)) + return true; } } - var afterAtkMsg = new AfterInteractEvent(user, weapon, attacked, clickLocation, false); - RaiseLocalEvent(weapon.Uid, afterAtkMsg); - if (afterAtkMsg.Handled) - return; - - // See if we have a ranged attack interaction - var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: false); - await DoAfterInteract(weapon, afterAttackEventArgs); + if (inRangeUnobstructed) + return await InteractDoAfter(user, used, target, clickLocation, false); + else + return await InteractDoAfter(user, used, null, clickLocation, false); } - private static async Task DoAfterInteract(IEntity weapon, AfterInteractEventArgs afterAttackEventArgs) + public void DoAttack(IEntity user, EntityCoordinates coordinates, bool wideAttack, EntityUid targetUid = default) { - var afterAttacks = weapon.GetAllComponents().OrderByDescending(x => x.Priority).ToList(); + if (!ValidateInteractAndFace(user, coordinates)) + return; - foreach (var afterAttack in afterAttacks) + if (!ActionBlockerSystem.CanAttack(user)) + return; + + IEntity? targetEnt = null; + + if (!wideAttack) { - if (await afterAttack.AfterInteract(afterAttackEventArgs)) + // Get entity clicked upon from UID if valid UID, if not assume no entity clicked upon and null + EntityManager.TryGetEntity(targetUid, out targetEnt); + + // Check if interacted entity is in the same container, the direct child, or direct parent of the user. + if (targetEnt != null && !user.IsInSameOrParentContainer(targetEnt)) { + Logger.WarningS("system.interaction", + $"User entity named {user.Name} clicked on object {targetEnt.Name} that isn't the parent, child, or in the same container"); return; } - } - } - public void DoAttack(IEntity player, EntityCoordinates coordinates, bool wideAttack, EntityUid targetUid = default) - { - // Verify player is on the same map as the entity he clicked on - if (coordinates.GetMapId(EntityManager) != player.Transform.MapID) - { - Logger.WarningS("system.interaction", - $"Player named {player.Name} clicked on a map he isn't located on"); - return; - } - - ClickFace(player, coordinates); - - if (!ActionBlockerSystem.CanAttack(player) || - (!wideAttack && !player.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true))) - { - return; - } - - // In a container where the target entity is not the container's owner - if (player.TryGetContainer(out var playerContainer) && - (!EntityManager.TryGetEntity(targetUid, out var target) || - target != playerContainer.Owner)) - { - // Either the target entity is null, not contained or in a different container - if (target == null || - !target.TryGetContainer(out var attackedContainer) || - attackedContainer != playerContainer) - { + // TODO: Replace with body attack range when we get something like arm length or telekinesis or something. + if (!user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true)) return; - } } - // Verify player has a hand, and find what object he is currently holding in his active hand - if (player.TryGetComponent(out var hands)) + // Verify user has a hand, and find what object he is currently holding in his active hand + if (user.TryGetComponent(out var hands)) { var item = hands.GetActiveHand?.Owner; @@ -840,7 +753,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click { if (wideAttack) { - var ev = new WideAttackEvent(item, player, coordinates); + var ev = new WideAttackEvent(item, user, coordinates); RaiseLocalEvent(item.Uid, ev, false); if(ev.Handled) @@ -848,30 +761,29 @@ namespace Content.Server.GameObjects.EntitySystems.Click } else { - var ev = new ClickAttackEvent(item, player, coordinates, targetUid); + var ev = new ClickAttackEvent(item, user, coordinates, targetUid); RaiseLocalEvent(item.Uid, ev, false); if(ev.Handled) return; } } - else + else if (!wideAttack && + (targetEnt != null || EntityManager.TryGetEntity(targetUid, out targetEnt)) && + targetEnt.HasComponent()) { // We pick up items if our hand is empty, even if we're in combat mode. - if (EntityManager.TryGetEntity(targetUid, out var targetEnt) && targetEnt.HasComponent()) - { - Interaction(player, targetEnt); - return; - } + InteractHand(user, targetEnt); + return; } } // TODO: Make this saner? // Attempt to do unarmed combat. We don't check for handled just because at this point it doesn't matter. if(wideAttack) - RaiseLocalEvent(player.Uid, new WideAttackEvent(player, player, coordinates), false); + RaiseLocalEvent(user.Uid, new WideAttackEvent(user, user, coordinates), false); else - RaiseLocalEvent(player.Uid, new ClickAttackEvent(player, player, coordinates, targetUid), false); + RaiseLocalEvent(user.Uid, new ClickAttackEvent(user, user, coordinates, targetUid), false); } } } diff --git a/Content.Shared/GameObjects/EntitySystems/SharedInteractionSystem.cs b/Content.Shared/GameObjects/EntitySystems/SharedInteractionSystem.cs index 173888e01f..500f7906b0 100644 --- a/Content.Shared/GameObjects/EntitySystems/SharedInteractionSystem.cs +++ b/Content.Shared/GameObjects/EntitySystems/SharedInteractionSystem.cs @@ -188,19 +188,8 @@ namespace Content.Shared.GameObjects.EntitySystems bool ignoreInsideBlocker = false, bool popup = false) { - var originPosition = origin.Transform.MapPosition; - var otherPosition = other.Transform.MapPosition; predicate ??= e => e == origin || e == other; - - var inRange = InRangeUnobstructed(originPosition, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker); - - if (!inRange && popup) - { - var message = Loc.GetString("You can't reach there!"); - origin.PopupMessage(message); - } - - return inRange; + return InRangeUnobstructed(origin, other.Transform.MapPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup); } /// @@ -244,19 +233,7 @@ namespace Content.Shared.GameObjects.EntitySystems bool ignoreInsideBlocker = false, bool popup = false) { - var originPosition = origin.Transform.MapPosition; - var otherPosition = other.Owner.Transform.MapPosition; - predicate ??= e => e == origin || e == other.Owner; - - var inRange = InRangeUnobstructed(originPosition, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker); - - if (!inRange && popup) - { - var message = Loc.GetString("You can't reach there!"); - origin.PopupMessage(message); - } - - return inRange; + return InRangeUnobstructed(origin, other.Owner, range, collisionMask, predicate, ignoreInsideBlocker, popup); } /// @@ -300,19 +277,7 @@ namespace Content.Shared.GameObjects.EntitySystems bool ignoreInsideBlocker = false, bool popup = false) { - var originPosition = origin.Transform.MapPosition; - var otherPosition = other.ToMap(EntityManager); - predicate ??= e => e == origin; - - var inRange = InRangeUnobstructed(originPosition, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker); - - if (!inRange && popup) - { - var message = Loc.GetString("You can't reach there!"); - origin.PopupMessage(message); - } - - return inRange; + return InRangeUnobstructed(origin, other.ToMap(EntityManager), range, collisionMask, predicate, ignoreInsideBlocker, popup); } /// @@ -369,235 +334,5 @@ namespace Content.Shared.GameObjects.EntitySystems return inRange; } - - /// - /// Checks that the user and target of a - /// are within a certain - /// distance without any entity that matches the collision mask - /// obstructing them. - /// If the is zero or negative, - /// this method will only check if nothing obstructs the entity and component. - /// - /// The event args to use. - /// - /// Maximum distance between the two entity and set of map coordinates. - /// - /// The mask to check for collisions. - /// - /// A predicate to check whether to ignore an entity or not. - /// If it returns true, it will be ignored. - /// - /// - /// If true and both the user and target are inside - /// the obstruction, ignores the obstruction and considers the interaction - /// unobstructed. - /// Therefore, setting this to true makes this check more permissive, - /// such as allowing an interaction to occur inside something impassable - /// (like a wall). The default, false, makes the check more restrictive. - /// - /// - /// Whether or not to popup a feedback message on the user entity for - /// it to see. - /// - /// - /// True if the two points are within a given range without being obstructed. - /// - public bool InRangeUnobstructed( - ITargetedInteractEventArgs args, - float range = InteractionRange, - CollisionGroup collisionMask = CollisionGroup.Impassable, - Ignored? predicate = null, - bool ignoreInsideBlocker = false, - bool popup = false) - { - var origin = args.User; - var other = args.Target; - - return InRangeUnobstructed(origin, other, range, collisionMask, predicate, ignoreInsideBlocker, popup); - } - - /// - /// Checks that the user of a is within a - /// certain distance of the target and dropped entities without any entity - /// that matches the collision mask obstructing them. - /// If the is zero or negative, - /// this method will only check if nothing obstructs the entity and component. - /// - /// The event args to use. - /// - /// Maximum distance between the two entity and set of map coordinates. - /// - /// The mask to check for collisions. - /// - /// A predicate to check whether to ignore an entity or not. - /// If it returns true, it will be ignored. - /// - /// - /// If true and both the user and target are inside - /// the obstruction, ignores the obstruction and considers the interaction - /// unobstructed. - /// Therefore, setting this to true makes this check more permissive, - /// such as allowing an interaction to occur inside something impassable - /// (like a wall). The default, false, makes the check more restrictive. - /// - /// - /// Whether or not to popup a feedback message on the user entity for - /// it to see. - /// - /// - /// True if the two points are within a given range without being obstructed. - /// - public bool InRangeUnobstructed( - DragDropEvent args, - float range = InteractionRange, - CollisionGroup collisionMask = CollisionGroup.Impassable, - Ignored? predicate = null, - bool ignoreInsideBlocker = false, - bool popup = false) - { - var user = args.User; - var dropped = args.Dragged; - var target = args.Target; - - if (!InRangeUnobstructed(user, target, range, collisionMask, predicate, ignoreInsideBlocker)) - { - if (popup) - { - var message = Loc.GetString("You can't reach there!"); - target.PopupMessage(user, message); - } - - return false; - } - - if (!InRangeUnobstructed(user, dropped, range, collisionMask, predicate, ignoreInsideBlocker)) - { - if (popup) - { - var message = Loc.GetString("You can't reach there!"); - dropped.PopupMessage(user, message); - } - - return false; - } - - return true; - } - - /// - /// Checks that the user and target of a - /// are within a certain distance - /// without any entity that matches the collision mask obstructing them. - /// If the is zero or negative, - /// this method will only check if nothing obstructs the entity and component. - /// - /// The event args to use. - /// - /// Maximum distance between the two entity and set of map coordinates. - /// - /// The mask to check for collisions. - /// - /// A predicate to check whether to ignore an entity or not. - /// If it returns true, it will be ignored. - /// - /// - /// If true and both the user and target are inside - /// the obstruction, ignores the obstruction and considers the interaction - /// unobstructed. - /// Therefore, setting this to true makes this check more permissive, - /// such as allowing an interaction to occur inside something impassable - /// (like a wall). The default, false, makes the check more restrictive. - /// - /// - /// Whether or not to popup a feedback message on the user entity for - /// it to see. - /// - /// - /// True if the two points are within a given range without being obstructed. - /// - public bool InRangeUnobstructed( - AfterInteractEventArgs args, - float range = InteractionRange, - CollisionGroup collisionMask = CollisionGroup.Impassable, - Ignored? predicate = null, - bool ignoreInsideBlocker = false, - bool popup = false) - { - var user = args.User; - var target = args.Target; - predicate ??= e => e == user; - - MapCoordinates otherPosition; - - if (target == null) - { - otherPosition = args.ClickLocation.ToMap(EntityManager); - } - else - { - otherPosition = target.Transform.MapPosition; - predicate += e => e == target; - } - - return InRangeUnobstructed(user, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup); - } - - /// - /// Checks that the user and target of a - /// are within a certain distance - /// without any entity that matches the collision mask obstructing them. - /// If the is zero or negative, - /// this method will only check if nothing obstructs the entity and component. - /// - /// The event args to use. - /// - /// Maximum distance between the two entity and set of map coordinates. - /// - /// The mask to check for collisions. - /// - /// A predicate to check whether to ignore an entity or not. - /// If it returns true, it will be ignored. - /// - /// - /// If true and both the user and target are inside - /// the obstruction, ignores the obstruction and considers the interaction - /// unobstructed. - /// Therefore, setting this to true makes this check more permissive, - /// such as allowing an interaction to occur inside something impassable - /// (like a wall). The default, false, makes the check more restrictive. - /// - /// - /// Whether or not to popup a feedback message on the user entity for - /// it to see. - /// - /// - /// True if the two points are within a given range without being obstructed. - /// - public bool InRangeUnobstructed( - AfterInteractEvent args, - float range = InteractionRange, - CollisionGroup collisionMask = CollisionGroup.Impassable, - Ignored? predicate = null, - bool ignoreInsideBlocker = false, - bool popup = false) - { - var user = args.User; - var target = args.Target; - predicate ??= e => e == user; - - MapCoordinates otherPosition; - - if (target == null) - { - otherPosition = args.ClickLocation.ToMap(EntityManager); - } - else - { - otherPosition = target.Transform.MapPosition; - predicate += e => e == target; - } - - return InRangeUnobstructed(user, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup); - } } } diff --git a/Content.Shared/Interfaces/GameObjects/Components/Interaction/IInteractHand.cs b/Content.Shared/Interfaces/GameObjects/Components/Interaction/IInteractHand.cs index bf9ed4a964..a3d00e60d4 100644 --- a/Content.Shared/Interfaces/GameObjects/Components/Interaction/IInteractHand.cs +++ b/Content.Shared/Interfaces/GameObjects/Components/Interaction/IInteractHand.cs @@ -35,7 +35,7 @@ namespace Content.Shared.Interfaces.GameObjects.Components /// Raised when a target entity is interacted with by a user with an empty hand. /// [PublicAPI] - public class AttackHandEvent : HandledEntityEventArgs + public class InteractHandEvent : HandledEntityEventArgs { /// /// Entity that triggered the interaction. @@ -47,7 +47,7 @@ namespace Content.Shared.Interfaces.GameObjects.Components /// public IEntity Target { get; } - public AttackHandEvent(IEntity user, IEntity target) + public InteractHandEvent(IEntity user, IEntity target) { User = user; Target = target; diff --git a/Content.Shared/Utility/SharedUnobstructedExtensions.cs b/Content.Shared/Utility/SharedUnobstructedExtensions.cs index 6011c2d3ef..78b7e7553e 100644 --- a/Content.Shared/Utility/SharedUnobstructedExtensions.cs +++ b/Content.Shared/Utility/SharedUnobstructedExtensions.cs @@ -1,10 +1,12 @@ #nullable enable using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Physics; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Localization; using Robust.Shared.Map; using static Content.Shared.GameObjects.EntitySystems.SharedInteractionSystem; @@ -402,20 +404,7 @@ namespace Content.Shared.Utility bool ignoreInsideBlocker = false, bool popup = false) { - return SharedInteractionSystem.InRangeUnobstructed(args, range, collisionMask, predicate, - ignoreInsideBlocker, popup); - } - - public static bool InRangeUnobstructed( - this DragDropEvent args, - float range = InteractionRange, - CollisionGroup collisionMask = CollisionGroup.Impassable, - Ignored? predicate = null, - bool ignoreInsideBlocker = false, - bool popup = false) - { - return SharedInteractionSystem.InRangeUnobstructed(args, range, collisionMask, predicate, - ignoreInsideBlocker, popup); + return SharedInteractionSystem.InRangeUnobstructed(args.User, args.Target, range, collisionMask, predicate, ignoreInsideBlocker, popup); } public static bool InRangeUnobstructed( @@ -426,12 +415,39 @@ namespace Content.Shared.Utility bool ignoreInsideBlocker = false, bool popup = false) { - return SharedInteractionSystem.InRangeUnobstructed(args, range, collisionMask, predicate, - ignoreInsideBlocker, popup); + var user = args.User; + var target = args.Target; + + if (target == null) + return SharedInteractionSystem.InRangeUnobstructed(user, args.ClickLocation, range, collisionMask, predicate, ignoreInsideBlocker, popup); + else + return SharedInteractionSystem.InRangeUnobstructed(user, target, range, collisionMask, predicate, ignoreInsideBlocker, popup); + } #endregion #region EntityEventArgs + public static bool InRangeUnobstructed( + this DragDropEvent args, + float range = InteractionRange, + CollisionGroup collisionMask = CollisionGroup.Impassable, + Ignored? predicate = null, + bool ignoreInsideBlocker = false, + bool popup = false) + { + var user = args.User; + var dropped = args.Dragged; + var target = args.Target; + + if (!SharedInteractionSystem.InRangeUnobstructed(user, target, range, collisionMask, predicate, ignoreInsideBlocker, popup)) + return false; + + if (!SharedInteractionSystem.InRangeUnobstructed(user, dropped, range, collisionMask, predicate, ignoreInsideBlocker, popup)) + return false; + + return true; + } + public static bool InRangeUnobstructed( this AfterInteractEvent args, float range = InteractionRange, @@ -440,8 +456,13 @@ namespace Content.Shared.Utility bool ignoreInsideBlocker = false, bool popup = false) { - return SharedInteractionSystem.InRangeUnobstructed(args, range, collisionMask, predicate, - ignoreInsideBlocker, popup); + var user = args.User; + var target = args.Target; + + if (target == null) + return SharedInteractionSystem.InRangeUnobstructed(user, args.ClickLocation, range, collisionMask, predicate, ignoreInsideBlocker, popup); + else + return SharedInteractionSystem.InRangeUnobstructed(user, target, range, collisionMask, predicate, ignoreInsideBlocker, popup); } #endregion }