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
This commit is contained in:
ShadowCommander
2021-06-07 05:49:43 -07:00
committed by GitHub
parent 9d7094fbd7
commit 6870b88a56
14 changed files with 662 additions and 695 deletions

View File

@@ -1,5 +1,6 @@
using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Weapon.Melee;
using Content.Server.GameObjects.EntitySystems.Click; using Content.Server.GameObjects.EntitySystems.Click;
using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Interfaces.GameObjects.Components;
using NUnit.Framework; using NUnit.Framework;
@@ -17,20 +18,300 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
[TestOf(typeof(InteractionSystem))] [TestOf(typeof(InteractionSystem))]
public class InteractionSystemTests : ContentIntegrationTest public class InteractionSystemTests : ContentIntegrationTest
{ {
[Reflect(false)] const string PROTOTYPES = @"
private class TestAttackEntitySystem : EntitySystem - type: entity
{ id: DummyDebugWall
public EntityEventHandler<ClickAttackEvent> ClickAttackEvent; components:
public EntityEventHandler<InteractUsingEvent> InteractUsingEvent; - type: Physics
public EntityEventHandler<AttackHandEvent> InteractHandEvent; 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()
{ {
base.Initialize(); var server = StartServerDummyTicker(new ServerContentIntegrationOption
SubscribeLocalEvent<ClickAttackEvent>((e) => ClickAttackEvent?.Invoke(e)); {
SubscribeLocalEvent<InteractUsingEvent>((e) => InteractUsingEvent?.Invoke(e)); ContentBeforeIoC = () =>
SubscribeLocalEvent<AttackHandEvent>((e) => InteractHandEvent?.Invoke(e)); {
IoCManager.Resolve<IEntitySystemManager>().LoadExtraSystemType<TestInteractionSystem>();
} }
});
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
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<HandsComponent>().AddHand("hand");
target = entityManager.SpawnEntity(null, coords);
item = entityManager.SpawnEntity(null, coords);
item.EnsureComponent<ItemComponent>();
});
await server.WaitRunTicks(1);
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem));
Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(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<HandsComponent>(out var hands));
Assert.That(hands.PutInHand(item.GetComponent<ItemComponent>()));
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<IEntitySystemManager>().LoadExtraSystemType<TestInteractionSystem>();
},
ExtraPrototypes = PROTOTYPES
});
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
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<HandsComponent>().AddHand("hand");
target = entityManager.SpawnEntity(null, new MapCoordinates((1.9f, 0), mapId));
item = entityManager.SpawnEntity(null, coords);
item.EnsureComponent<ItemComponent>();
wall = entityManager.SpawnEntity("DummyDebugWall", new MapCoordinates((1, 0), user.Transform.MapID));
});
await server.WaitRunTicks(1);
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem));
Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(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<HandsComponent>(out var hands));
Assert.That(hands.PutInHand(item.GetComponent<ItemComponent>()));
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<IEntitySystemManager>().LoadExtraSystemType<TestInteractionSystem>();
}
});
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
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<HandsComponent>().AddHand("hand");
target = entityManager.SpawnEntity(null, new MapCoordinates((InteractionSystem.InteractionRange - 0.1f, 0), mapId));
item = entityManager.SpawnEntity(null, coords);
item.EnsureComponent<ItemComponent>();
});
await server.WaitRunTicks(1);
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem));
Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(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<HandsComponent>(out var hands));
Assert.That(hands.PutInHand(item.GetComponent<ItemComponent>()));
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<IEntitySystemManager>().LoadExtraSystemType<TestInteractionSystem>();
}
});
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
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<HandsComponent>().AddHand("hand");
target = entityManager.SpawnEntity(null, new MapCoordinates((InteractionSystem.InteractionRange, 0), mapId));
item = entityManager.SpawnEntity(null, coords);
item.EnsureComponent<ItemComponent>();
});
await server.WaitRunTicks(1);
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem));
Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(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<HandsComponent>(out var hands));
Assert.That(hands.PutInHand(item.GetComponent<ItemComponent>()));
interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid);
Assert.That(interactUsing, Is.False);
});
await server.WaitIdleAsync();
} }
[Test] [Test]
@@ -40,8 +321,9 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
{ {
ContentBeforeIoC = () => ContentBeforeIoC = () =>
{ {
IoCManager.Resolve<IEntitySystemManager>().LoadExtraSystemType<TestAttackEntitySystem>(); IoCManager.Resolve<IEntitySystemManager>().LoadExtraSystemType<TestInteractionSystem>();
} },
FailureLogLevel = Robust.Shared.Log.LogLevel.Error
}); });
await server.WaitIdleAsync(); await server.WaitIdleAsync();
@@ -49,23 +331,38 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
var entityManager = server.ResolveDependency<IEntityManager>(); var entityManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>(); var mapManager = server.ResolveDependency<IMapManager>();
IEntity origin = null; var mapId = MapId.Nullspace;
IEntity other = null; 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; IEntity containerEntity = null;
IContainer container = null; IContainer container = null;
server.Assert(() => server.Assert(() =>
{ {
var mapId = mapManager.CreateMap(); user = entityManager.SpawnEntity(null, coords);
var coordinates = new MapCoordinates(Vector2.Zero, mapId); user.EnsureComponent<HandsComponent>().AddHand("hand");
target = entityManager.SpawnEntity(null, coords);
origin = entityManager.SpawnEntity(null, coordinates); item = entityManager.SpawnEntity(null, coords);
origin.EnsureComponent<HandsComponent>().AddHand("hand"); item.EnsureComponent<ItemComponent>();
other = entityManager.SpawnEntity(null, coordinates); containerEntity = entityManager.SpawnEntity(null, coords);
containerEntity = entityManager.SpawnEntity(null, coordinates);
container = ContainerHelpers.EnsureContainer<Container>(containerEntity, "InteractionTestContainer"); container = ContainerHelpers.EnsureContainer<Container>(containerEntity, "InteractionTestContainer");
}); });
await server.WaitRunTicks(1);
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem));
Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(out var testInteractionSystem));
await server.WaitIdleAsync(); await server.WaitIdleAsync();
var attack = false; var attack = false;
@@ -73,53 +370,53 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
var interactHand = false; var interactHand = false;
server.Assert(() => server.Assert(() =>
{ {
Assert.That(container.Insert(origin)); Assert.That(container.Insert(user));
Assert.That(origin.Transform.Parent!.Owner, Is.EqualTo(containerEntity)); Assert.That(user.Transform.Parent.Owner, Is.EqualTo(containerEntity));
var entitySystemManager = IoCManager.Resolve<IEntitySystemManager>(); testInteractionSystem.AttackEvent = (_, _, ev) => { Assert.That(ev.Target, Is.EqualTo(containerEntity.Uid)); attack = true; };
Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem)); 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<TestAttackEntitySystem>(out var testAttackEntitySystem)); interactionSystem.DoAttack(user, target.Transform.Coordinates, false, target.Uid);
testAttackEntitySystem.ClickAttackEvent = (ev) => interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid);
{
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);
Assert.That(attack, Is.False); Assert.That(attack, Is.False);
Assert.That(interactUsing, Is.False); Assert.That(interactUsing, Is.False);
Assert.That(interactHand, Is.False); Assert.That(interactHand, Is.False);
interactionSystem.DoAttack(origin, containerEntity.Transform.Coordinates, false, containerEntity.Uid); interactionSystem.DoAttack(user, containerEntity.Transform.Coordinates, false, containerEntity.Uid);
interactionSystem.UserInteraction(origin, containerEntity.Transform.Coordinates, containerEntity.Uid); interactionSystem.UserInteraction(user, containerEntity.Transform.Coordinates, containerEntity.Uid);
Assert.That(attack); Assert.That(attack);
Assert.That(interactUsing, Is.False); Assert.That(interactUsing, Is.False);
Assert.That(interactHand); Assert.That(interactHand);
var itemEntity = entityManager.SpawnEntity(null, origin.Transform.Coordinates); Assert.That(user.TryGetComponent<HandsComponent>(out var hands));
var item = itemEntity.EnsureComponent<ItemComponent>(); Assert.That(hands.PutInHand(item.GetComponent<ItemComponent>()));
Assert.That(origin.TryGetComponent<HandsComponent>(out var hands)); interactionSystem.UserInteraction(user, target.Transform.Coordinates, target.Uid);
hands.PutInHand(item);
interactionSystem.UserInteraction(origin, other.Transform.Coordinates, other.Uid);
Assert.That(interactUsing, Is.False); 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); Assert.That(interactUsing, Is.True);
}); });
await server.WaitIdleAsync();
} }
[Reflect(false)]
private class TestInteractionSystem : EntitySystem
{
public ComponentEventHandler<HandsComponent, ClickAttackEvent> AttackEvent;
public EntityEventHandler<InteractUsingEvent> InteractUsingEvent;
public EntityEventHandler<InteractHandEvent> InteractHandEvent;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HandsComponent, ClickAttackEvent>((u, c, e) => AttackEvent?.Invoke(u, c, e));
SubscribeLocalEvent<InteractUsingEvent>((e) => InteractUsingEvent?.Invoke(e));
SubscribeLocalEvent<InteractHandEvent>((e) => InteractHandEvent?.Invoke(e));
}
}
} }
} }

View File

@@ -78,7 +78,7 @@ namespace Content.Server.AI.Operators.Combat.Melee
var interactionSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InteractionSystem>(); var interactionSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.UseItemInHand(_owner, _target.Transform.Coordinates, _target.Uid); interactionSystem.AiUseInteraction(_owner, _target.Transform.Coordinates, _target.Uid);
_elapsedTime += frameTime; _elapsedTime += frameTime;
return Outcome.Continuing; return Outcome.Continuing;
} }

View File

@@ -83,7 +83,7 @@ namespace Content.Server.AI.Operators.Combat.Melee
} }
var interactionSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InteractionSystem>(); var interactionSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.UseItemInHand(_owner, _target.Transform.Coordinates, _target.Uid); interactionSystem.AiUseInteraction(_owner, _target.Transform.Coordinates, _target.Uid);
_elapsedTime += frameTime; _elapsedTime += frameTime;
return Outcome.Continuing; return Outcome.Continuing;
} }

View File

@@ -40,7 +40,7 @@ namespace Content.Server.AI.Operators.Inventory
// Click on da thing // Click on da thing
var interactionSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InteractionSystem>(); var interactionSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.UseItemInHand(_owner, _useTarget.Transform.Coordinates, _useTarget.Uid); interactionSystem.AiUseInteraction(_owner, _useTarget.Transform.Coordinates, _useTarget.Uid);
return Outcome.Success; return Outcome.Success;
} }

View File

@@ -58,7 +58,7 @@ namespace Content.Server.AI.Operators.Inventory
} }
var interactionSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InteractionSystem>(); var interactionSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.Interaction(_owner, _target); interactionSystem.InteractHand(_owner, _target);
return Outcome.Success; return Outcome.Success;
} }
} }

View File

@@ -47,7 +47,7 @@ namespace Content.Server.Actions
var player = actor.PlayerSession; var player = actor.PlayerSession;
var coordinates = args.Target.Transform.Coordinates; var coordinates = args.Target.Transform.Coordinates;
var target = args.Target.Uid; var target = args.Target.Uid;
EntitySystem.Get<InteractionSystem>().HandleClientUseItemInHand(player, coordinates, target); EntitySystem.Get<InteractionSystem>().HandleUseInteraction(player, coordinates, target);
return; return;
} }

View File

@@ -161,6 +161,8 @@ namespace Content.Server.GameObjects.Components.Atmos
{ {
if (Owner.Transform.Anchored) if (Owner.Transform.Anchored)
{ {
if (!Owner.Transform.GridID.IsValid())
return;
var grid = _mapManager.GetGrid(Owner.Transform.GridID); var grid = _mapManager.GetGrid(Owner.Transform.GridID);
UpdatePosition(Owner.Transform.GridID, grid.TileIndicesFor(Owner.Transform.Coordinates)); UpdatePosition(Owner.Transform.GridID, grid.TileIndicesFor(Owner.Transform.Coordinates));
} }

View File

@@ -664,13 +664,13 @@ namespace Content.Server.GameObjects.Components.GUI
var interactionSystem = _entitySystemManager.GetEntitySystem<InteractionSystem>(); var interactionSystem = _entitySystemManager.GetEntitySystem<InteractionSystem>();
if (used != null) if (used != null)
{ {
await interactionSystem.Interaction(Owner, used, hand.Entity, await interactionSystem.InteractUsing(Owner, used, hand.Entity,
EntityCoordinates.Invalid); EntityCoordinates.Invalid);
} }
else else
{ {
var entity = hand.Entity; var entity = hand.Entity;
interactionSystem.Interaction(Owner, entity); interactionSystem.InteractHand(Owner, entity);
} }
} }

View File

@@ -539,7 +539,7 @@ namespace Content.Server.GameObjects.Components.GUI
{ {
if (activeHand != null) if (activeHand != null)
{ {
await interactionSystem.Interaction(Owner, activeHand.Owner, itemContainedInSlot.Owner, await interactionSystem.InteractUsing(Owner, activeHand.Owner, itemContainedInSlot.Owner,
new EntityCoordinates()); new EntityCoordinates());
} }
else if (Unequip(msg.Inventoryslot)) else if (Unequip(msg.Inventoryslot))

View File

@@ -31,10 +31,10 @@ namespace Content.Server.GameObjects.EntitySystems
SubscribeLocalEvent<BuckleComponent, EntRemovedFromContainerMessage>(ContainerModifiedBuckle); SubscribeLocalEvent<BuckleComponent, EntRemovedFromContainerMessage>(ContainerModifiedBuckle);
SubscribeLocalEvent<StrapComponent, EntRemovedFromContainerMessage>(ContainerModifiedStrap); SubscribeLocalEvent<StrapComponent, EntRemovedFromContainerMessage>(ContainerModifiedStrap);
SubscribeLocalEvent<BuckleComponent, AttackHandEvent>(HandleAttackHand); SubscribeLocalEvent<BuckleComponent, InteractHandEvent>(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); args.Handled = component.TryUnbuckle(args.User);
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Items.Storage;
@@ -29,6 +30,8 @@ using Robust.Shared.Map;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Players; using Robust.Shared.Players;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Localization;
using Content.Shared.Interfaces;
namespace Content.Server.GameObjects.EntitySystems.Click namespace Content.Server.GameObjects.EntitySystems.Click
{ {
@@ -43,16 +46,17 @@ namespace Content.Server.GameObjects.EntitySystems.Click
public override void Initialize() public override void Initialize()
{ {
SubscribeNetworkEvent<DragDropRequestEvent>(HandleDragDropMessage); SubscribeNetworkEvent<DragDropRequestEvent>(HandleDragDropRequestEvent);
CommandBinds.Builder CommandBinds.Builder
.Bind(EngineKeyFunctions.Use, .Bind(EngineKeyFunctions.Use,
new PointerInputCmdHandler(HandleClientUseItemInHand)) new PointerInputCmdHandler(HandleUseInteraction))
.Bind(ContentKeyFunctions.WideAttack, .Bind(ContentKeyFunctions.WideAttack,
new PointerInputCmdHandler(HandleWideAttack)) new PointerInputCmdHandler(HandleWideAttack))
.Bind(ContentKeyFunctions.ActivateItemInWorld, .Bind(ContentKeyFunctions.ActivateItemInWorld,
new PointerInputCmdHandler(HandleActivateItemInWorld)) new PointerInputCmdHandler(HandleActivateItemInWorld))
.Bind(ContentKeyFunctions.TryPullObject, new PointerInputCmdHandler(HandleTryPullObject)) .Bind(ContentKeyFunctions.TryPullObject,
new PointerInputCmdHandler(HandleTryPullObject))
.Register<InteractionSystem>(); .Register<InteractionSystem>();
} }
@@ -62,19 +66,57 @@ namespace Content.Server.GameObjects.EntitySystems.Click
base.Shutdown(); 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 (!coords.IsValid(_entityManager))
if (!EntityManager.TryGetEntity(msg.Dropped, out var dropped)) return; {
if (!EntityManager.TryGetEntity(msg.Target, out var target)) return; 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 // 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. // 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 // trigger dragdrops on the dropped entity
RaiseLocalEvent(dropped.Uid, interactionArgs); 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) 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)) if (!EntityManager.TryGetEntity(uid, out var used))
return false; return false;
var playerEnt = ((IPlayerSession?) session)?.AttachedEntity; InteractionActivate(user, used);
if (playerEnt == null || !playerEnt.IsValid())
{
return false;
}
if (!playerEnt.Transform.Coordinates.InRange(EntityManager, used.Transform.Coordinates, InteractionRange))
{
return false;
}
InteractionActivate(playerEnt, used);
return true; return true;
} }
@@ -126,61 +164,45 @@ namespace Content.Server.GameObjects.EntitySystems.Click
/// </summary> /// </summary>
public void TryInteractionActivate(IEntity? user, IEntity? used) public void TryInteractionActivate(IEntity? user, IEntity? used)
{ {
if (user != null && used != null && ActionBlockerSystem.CanUse(user)) if (user == null || used == null)
{ return;
InteractionActivate(user, used); InteractionActivate(user, used);
} }
}
private void InteractionActivate(IEntity user, IEntity 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); var activateMsg = new ActivateInWorldEvent(user, used);
RaiseLocalEvent(used.Uid, activateMsg); RaiseLocalEvent(used.Uid, activateMsg);
if (activateMsg.Handled) if (activateMsg.Handled)
{
return; return;
}
if (!used.TryGetComponent(out IActivate? activateComp)) if (!used.TryGetComponent(out IActivate? activateComp))
{
return; return;
}
// all activates should only fire when in range / unbostructed
var activateEventArgs = new ActivateEventArgs(user, used); 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) private bool HandleWideAttack(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{ {
// client sanitization // client sanitization
if (!coords.IsValid(_entityManager)) if (!ValidateClientInput(session, coords, uid, out var userEntity))
{
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())
{ {
Logger.InfoS("system.interaction", $"WideAttack input validation failed");
return true; return true;
} }
if (userEntity.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode) if (userEntity.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode)
{
DoAttack(userEntity, coords, true); DoAttack(userEntity, coords, true);
}
return true; return true;
} }
@@ -192,49 +214,23 @@ namespace Content.Server.GameObjects.EntitySystems.Click
/// <param name="entity"></param> /// <param name="entity"></param>
/// <param name="coords"></param> /// <param name="coords"></param>
/// <param name="uid"></param> /// <param name="uid"></param>
internal void UseItemInHand(IEntity entity, EntityCoordinates coords, EntityUid uid) internal void AiUseInteraction(IEntity entity, EntityCoordinates coords, EntityUid uid)
{ {
if (entity.HasComponent<ActorComponent>()) if (entity.HasComponent<ActorComponent>())
{
throw new InvalidOperationException(); 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 // 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; 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; return true;
@@ -242,172 +238,120 @@ namespace Content.Server.GameObjects.EntitySystems.Click
private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid) private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{ {
// client sanitization if (!ValidateClientInput(session, coords, uid, out var userEntity))
if (!coords.IsValid(_entityManager))
{ {
Logger.InfoS("system.interaction", $"Invalid Coordinates for pulling: client={session}, coords={coords}"); Logger.InfoS("system.interaction", $"TryPullObject input validation failed");
return false; return true;
} }
if (uid.IsClientSide()) if (userEntity.Uid == uid)
{
Logger.WarningS("system.interaction",
$"Client sent pull interaction with client-side entity. Session={session}, Uid={uid}");
return false; 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)) if (!EntityManager.TryGetEntity(uid, out var pulledObject))
{
return false; return false;
}
if (player == pulledObject) if (!InRangeUnobstructed(userEntity, pulledObject, popup: true))
{
return false; return false;
}
if (!pulledObject.TryGetComponent(out PullableComponent? pull)) if (!pulledObject.TryGetComponent(out PullableComponent? pull))
{
return false; return false;
return pull.TogglePull(userEntity);
} }
var dist = player.Transform.Coordinates.Position - pulledObject.Transform.Coordinates.Position; public async void UserInteraction(IEntity user, EntityCoordinates coordinates, EntityUid clickedUid)
if (dist.LengthSquared > InteractionRangeSquared)
{ {
return false; if (user.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode)
}
return pull.TogglePull(player);
}
public async void UserInteraction(IEntity player, 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<ITransformComponent>(out var playerTransform))
{ {
DoAttack(user, coordinates, false, clickedUid);
return; return;
} }
// Verify player is on the same map as the entity he clicked on if (!ValidateInteractAndFace(user, coordinates))
if (coordinates.GetMapId(_entityManager) != playerTransform.MapID) 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", 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; return;
} }
// Verify player has a hand, and find what object he is currently holding in his active hand // Verify user has a hand, and find what object he is currently holding in his active hand
if (!player.TryGetComponent<IHandsComponent>(out var hands)) if (!user.TryGetComponent<IHandsComponent>(out var hands))
{
return; return;
}
var item = hands.GetActiveHand?.Owner; var item = hands.GetActiveHand?.Owner;
ClickFace(player, coordinates); // 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 (!ActionBlockerSystem.CanInteract(player)) if (target == null || !inRangeUnobstructed)
{ {
if (item == null)
return; return;
}
// In a container where the attacked entity is not the container's owner if (!await InteractUsingRanged(user, item, target, coordinates, inRangeUnobstructed) &&
if (player.TryGetContainer(out var playerContainer) && !inRangeUnobstructed)
attacked != playerContainer.Owner)
{ {
// Either the attacked entity is null, not contained or in a different container var message = Loc.GetString("You can't reach there!");
if (attacked == null || user.PopupMessage(message);
!attacked.TryGetContainer(out var attackedContainer) ||
attackedContainer != playerContainer)
{
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)
{
// 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);
} }
return; return;
} }
else
// 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 // 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 // InteractUsing/AfterInteract: We will either use the item on the nearby object
if (item != null) if (item != null)
{ await InteractUsing(user, item, target, coordinates);
await Interaction(player, item, attacked, coordinates);
}
// InteractHand/Activate: Since our hand is empty we will use InteractHand/Activate // InteractHand/Activate: Since our hand is empty we will use InteractHand/Activate
else else
{ InteractHand(user, target);
Interaction(player, attacked);
} }
} }
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) if (diff.LengthSquared <= 0.01f)
return; return;
var diffAngle = Angle.FromWorldVec(diff); var diffAngle = Angle.FromWorldVec(diff);
if (ActionBlockerSystem.CanChangeDirection(player)) if (ActionBlockerSystem.CanChangeDirection(user))
{ {
player.Transform.LocalRotation = diffAngle; user.Transform.WorldRotation = diffAngle;
} }
else 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? // We're buckled to another object. Is that object rotatable?
if (buckle.BuckledTo!.Owner.TryGetComponent(out SharedRotatableComponent? rotatable) && rotatable.RotateWhileAnchored) 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". // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel".
// (Since the player being buckled to it holds it down with their weight.) // (Since the user being buckled to it holds it down with their weight.)
// This is logically equivalent to RotateWhileAnchored. // This is logically equivalent to RotateWhileAnchored.
// Barstools and office chairs have independent wheels, while regular chairs don't. // Barstools and office chairs have independent wheels, while regular chairs don't.
rotatable.Owner.Transform.LocalRotation = diffAngle; rotatable.Owner.Transform.LocalRotation = diffAngle;
@@ -419,90 +363,87 @@ namespace Content.Server.GameObjects.EntitySystems.Click
/// <summary> /// <summary>
/// We didn't click on any entity, try doing an AfterInteract on the click location /// We didn't click on any entity, try doing an AfterInteract on the click location
/// </summary> /// </summary>
private async void InteractAfter(IEntity user, IEntity weapon, EntityCoordinates clickLocation, bool canReach) private async Task<bool> InteractDoAfter(IEntity user, IEntity used, IEntity? target, EntityCoordinates clickLocation, bool canReach)
{ {
var message = new AfterInteractEvent(user, weapon, null, clickLocation, canReach); var afterInteractEvent = new AfterInteractEvent(user, used, target, clickLocation, canReach);
RaiseLocalEvent(weapon.Uid, message); RaiseLocalEvent(used.Uid, afterInteractEvent, false);
if (message.Handled) if (afterInteractEvent.Handled)
return true;
var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, target, canReach);
var afterInteracts = used.GetAllComponents<IAfterInteract>().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); return false;
await DoAfterInteract(weapon, afterInteractEventArgs);
} }
/// <summary> /// <summary>
/// Uses a weapon/object on an entity /// Uses a item/object on an entity
/// Finds components with the InteractUsing interface and calls their function /// Finds components with the InteractUsing interface and calls their function
/// NOTE: Does not have an InRangeUnobstructed check
/// </summary> /// </summary>
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); if (!ActionBlockerSystem.CanInteract(user))
RaiseLocalEvent(attacked.Uid, attackMsg);
if (attackMsg.Handled)
return; return;
var attackBys = attacked.GetAllComponents<IInteractUsing>().OrderByDescending(x => x.Priority); // all interactions should only happen when in range / unobstructed, so no range check is needed
var attackByEventArgs = new InteractUsingEventArgs(user, clickLocation, weapon, attacked); var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation);
RaiseLocalEvent(target.Uid, interactUsingEvent);
// all AttackBys should only happen when in range / unobstructed, so no range check is needed if (interactUsingEvent.Handled)
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; return;
}
}
}
var afterAtkMsg = new AfterInteractEvent(user, weapon, attacked, clickLocation, true); var interactUsingEventArgs = new InteractUsingEventArgs(user, clickLocation, used, target);
RaiseLocalEvent(weapon.Uid, afterAtkMsg, false);
if (afterAtkMsg.Handled) var interactUsings = target.GetAllComponents<IInteractUsing>().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; return;
} }
// If we aren't directly attacking the nearby object, lets see if our item has an after attack we can do // If we aren't directly interacting with the nearby object, lets see if our item has an after interact we can do
var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: true); await InteractDoAfter(user, used, target, clickLocation, true);
await DoAfterInteract(weapon, afterAttackEventArgs);
} }
/// <summary> /// <summary>
/// Uses an empty hand on an entity /// Uses an empty hand on an entity
/// Finds components with the InteractHand interface and calls their function /// Finds components with the InteractHand interface and calls their function
/// NOTE: Does not have an InRangeUnobstructed check
/// </summary> /// </summary>
public void Interaction(IEntity user, IEntity attacked) public void InteractHand(IEntity user, IEntity target)
{ {
var message = new AttackHandEvent(user, attacked); if (!ActionBlockerSystem.CanInteract(user))
RaiseLocalEvent(attacked.Uid, message); 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) if (message.Handled)
return; return;
var attackHandEventArgs = new InteractHandEventArgs(user, attacked); var interactHandEventArgs = new InteractHandEventArgs(user, target);
// all attackHands should only fire when in range / unobstructed var interactHandComps = target.GetAllComponents<IInteractHand>().ToList();
if (attackHandEventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) foreach (var interactHandComp in interactHandComps)
{ {
var attackHands = attacked.GetAllComponents<IInteractHand>().ToList(); // If an InteractHand returns a status completion we finish our interaction
foreach (var attackHand in attackHands) if (interactHandComp.InteractHand(interactHandEventArgs))
{
if (attackHand.InteractHand(attackHandEventArgs))
{
// If an InteractHand returns a status completion we finish our attack
return; return;
} }
}
}
// Else we run Activate. // Else we run Activate.
InteractionActivate(user, attacked); InteractionActivate(user, target);
} }
#region Hands
#region Use
/// <summary> /// <summary>
/// Activates the IUse behaviors of an entity /// Activates the IUse behaviors of an entity
/// Verifies that the user is capable of doing the use interaction first /// 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); var useMsg = new UseInHandEvent(user, used);
RaiseLocalEvent(used.Uid, useMsg); RaiseLocalEvent(used.Uid, useMsg);
if (useMsg.Handled) if (useMsg.Handled)
{
return; return;
}
var uses = used.GetAllComponents<IUse>().ToList(); var uses = used.GetAllComponents<IUse>().ToList();
// Try to use item on any components which have the interface // Try to use item on any components which have the interface
foreach (var use in uses) foreach (var use in uses)
{ {
// If a Use returns a status completion we finish our interaction
if (use.UseEntity(new UseEntityEventArgs(user))) if (use.UseEntity(new UseEntityEventArgs(user)))
{
// If a Use returns a status completion we finish our attack
return; return;
} }
} }
} #endregion
#region Throw
/// <summary> /// <summary>
/// Activates the Throw behavior of an object /// Activates the Throw behavior of an object
/// Verifies that the user is capable of doing the throw interaction first /// 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); var throwMsg = new ThrownEvent(user, thrown);
RaiseLocalEvent(thrown.Uid, throwMsg); RaiseLocalEvent(thrown.Uid, throwMsg);
if (throwMsg.Handled) if (throwMsg.Handled)
{
return; return;
}
var comps = thrown.GetAllComponents<IThrown>().ToList(); var comps = thrown.GetAllComponents<IThrown>().ToList();
var args = new ThrownEventArgs(user); var args = new ThrownEventArgs(user);
@@ -585,7 +522,9 @@ namespace Content.Server.GameObjects.EntitySystems.Click
comp.Thrown(args); comp.Thrown(args);
} }
} }
#endregion
#region Equip
/// <summary> /// <summary>
/// Calls Equipped on all components that implement the IEquipped interface /// Calls Equipped on all components that implement the IEquipped interface
/// on an entity that has been equipped. /// on an entity that has been equipped.
@@ -595,9 +534,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var equipMsg = new EquippedEvent(user, equipped, slot); var equipMsg = new EquippedEvent(user, equipped, slot);
RaiseLocalEvent(equipped.Uid, equipMsg); RaiseLocalEvent(equipped.Uid, equipMsg);
if (equipMsg.Handled) if (equipMsg.Handled)
{
return; return;
}
var comps = equipped.GetAllComponents<IEquipped>().ToList(); var comps = equipped.GetAllComponents<IEquipped>().ToList();
@@ -617,9 +554,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var unequipMsg = new UnequippedEvent(user, equipped, slot); var unequipMsg = new UnequippedEvent(user, equipped, slot);
RaiseLocalEvent(equipped.Uid, unequipMsg); RaiseLocalEvent(equipped.Uid, unequipMsg);
if (unequipMsg.Handled) if (unequipMsg.Handled)
{
return; return;
}
var comps = equipped.GetAllComponents<IUnequipped>().ToList(); var comps = equipped.GetAllComponents<IUnequipped>().ToList();
@@ -630,6 +565,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
} }
} }
#region Equip Hand
/// <summary> /// <summary>
/// Calls EquippedHand on all components that implement the IEquippedHand interface /// Calls EquippedHand on all components that implement the IEquippedHand interface
/// on an item. /// on an item.
@@ -639,9 +575,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var equippedHandMessage = new EquippedHandEvent(user, item, hand); var equippedHandMessage = new EquippedHandEvent(user, item, hand);
RaiseLocalEvent(item.Uid, equippedHandMessage); RaiseLocalEvent(item.Uid, equippedHandMessage);
if (equippedHandMessage.Handled) if (equippedHandMessage.Handled)
{
return; return;
}
var comps = item.GetAllComponents<IEquippedHand>().ToList(); var comps = item.GetAllComponents<IEquippedHand>().ToList();
@@ -660,9 +594,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var unequippedHandMessage = new UnequippedHandEvent(user, item, hand); var unequippedHandMessage = new UnequippedHandEvent(user, item, hand);
RaiseLocalEvent(item.Uid, unequippedHandMessage); RaiseLocalEvent(item.Uid, unequippedHandMessage);
if (unequippedHandMessage.Handled) if (unequippedHandMessage.Handled)
{
return; return;
}
var comps = item.GetAllComponents<IUnequippedHand>().ToList(); var comps = item.GetAllComponents<IUnequippedHand>().ToList();
@@ -671,7 +603,10 @@ namespace Content.Server.GameObjects.EntitySystems.Click
comp.UnequippedHand(new UnequippedHandEventArgs(user, hand)); comp.UnequippedHand(new UnequippedHandEventArgs(user, hand));
} }
} }
#endregion
#endregion
#region Drop
/// <summary> /// <summary>
/// Activates the Dropped behavior of an object /// Activates the Dropped behavior of an object
/// Verifies that the user is capable of doing the drop interaction first /// 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); var dropMsg = new DroppedEvent(user, item, intentional);
RaiseLocalEvent(item.Uid, dropMsg); RaiseLocalEvent(item.Uid, dropMsg);
if (dropMsg.Handled) if (dropMsg.Handled)
{
return; return;
}
item.Transform.LocalRotation = intentional ? Angle.Zero : (_random.Next(0, 100) / 100f) * MathHelper.TwoPi; 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)); comp.Dropped(new DroppedEventArgs(user, intentional));
} }
} }
#endregion
#region Hand Selected
/// <summary> /// <summary>
/// Calls HandSelected on all components that implement the IHandSelected interface /// Calls HandSelected on all components that implement the IHandSelected interface
/// on an item entity on a hand that has just been selected. /// 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); var handSelectedMsg = new HandSelectedEvent(user, item);
RaiseLocalEvent(item.Uid, handSelectedMsg); RaiseLocalEvent(item.Uid, handSelectedMsg);
if (handSelectedMsg.Handled) if (handSelectedMsg.Handled)
{
return; return;
}
var comps = item.GetAllComponents<IHandSelected>().ToList(); var comps = item.GetAllComponents<IHandSelected>().ToList();
@@ -739,9 +672,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var handDeselectedMsg = new HandDeselectedEvent(user, item); var handDeselectedMsg = new HandDeselectedEvent(user, item);
RaiseLocalEvent(item.Uid, handDeselectedMsg); RaiseLocalEvent(item.Uid, handDeselectedMsg);
if (handDeselectedMsg.Handled) if (handDeselectedMsg.Handled)
{
return; return;
}
var comps = item.GetAllComponents<IHandDeselected>().ToList(); var comps = item.GetAllComponents<IHandDeselected>().ToList();
@@ -751,88 +682,70 @@ namespace Content.Server.GameObjects.EntitySystems.Click
comp.HandDeselected(new HandDeselectedEventArgs(user)); comp.HandDeselected(new HandDeselectedEventArgs(user));
} }
} }
#endregion
#endregion
/// <summary> /// <summary>
/// Will have two behaviors, either "uses" the weapon at range on the entity if it is capable of accepting that action /// 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 weapon itself on the position clicked, regardless of what was there /// Or it will use the used entity itself on the position clicked, regardless of what was there
/// </summary> /// </summary>
public async void RangedInteraction(IEntity user, IEntity weapon, IEntity attacked, EntityCoordinates clickLocation) public async Task<bool> InteractUsingRanged(IEntity user, IEntity used, IEntity? target, EntityCoordinates clickLocation, bool inRangeUnobstructed)
{ {
var rangedMsg = new RangedInteractEvent(user, weapon, attacked, clickLocation); if (target != null)
RaiseLocalEvent(attacked.Uid, rangedMsg); {
var rangedMsg = new RangedInteractEvent(user, used, target, clickLocation);
RaiseLocalEvent(target.Uid, rangedMsg);
if (rangedMsg.Handled) if (rangedMsg.Handled)
return true;
var rangedInteractions = target.GetAllComponents<IRangedInteract>().ToList();
var rangedInteractionEventArgs = new RangedInteractEventArgs(user, used, clickLocation);
// See if we have a ranged interaction
foreach (var t in rangedInteractions)
{
// If an InteractUsingRanged returns a status completion we finish our interaction
if (t.RangedInteract(rangedInteractionEventArgs))
return true;
}
}
if (inRangeUnobstructed)
return await InteractDoAfter(user, used, target, clickLocation, false);
else
return await InteractDoAfter(user, used, null, clickLocation, false);
}
public void DoAttack(IEntity user, EntityCoordinates coordinates, bool wideAttack, EntityUid targetUid = default)
{
if (!ValidateInteractAndFace(user, coordinates))
return; return;
var rangedAttackBys = attacked.GetAllComponents<IRangedInteract>().ToList(); if (!ActionBlockerSystem.CanAttack(user))
var rangedAttackByEventArgs = new RangedInteractEventArgs(user, weapon, clickLocation);
// See if we have a ranged attack interaction
foreach (var t in rangedAttackBys)
{
if (t.RangedInteract(rangedAttackByEventArgs))
{
// If an InteractUsing returns a status completion we finish our attack
return;
}
}
var afterAtkMsg = new AfterInteractEvent(user, weapon, attacked, clickLocation, false);
RaiseLocalEvent(weapon.Uid, afterAtkMsg);
if (afterAtkMsg.Handled)
return; return;
// See if we have a ranged attack interaction IEntity? targetEnt = null;
var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: false);
await DoAfterInteract(weapon, afterAttackEventArgs);
}
private static async Task DoAfterInteract(IEntity weapon, AfterInteractEventArgs afterAttackEventArgs) if (!wideAttack)
{ {
var afterAttacks = weapon.GetAllComponents<IAfterInteract>().OrderByDescending(x => x.Priority).ToList(); // Get entity clicked upon from UID if valid UID, if not assume no entity clicked upon and null
EntityManager.TryGetEntity(targetUid, out targetEnt);
foreach (var afterAttack in afterAttacks) // Check if interacted entity is in the same container, the direct child, or direct parent of the user.
{ if (targetEnt != null && !user.IsInSameOrParentContainer(targetEnt))
if (await afterAttack.AfterInteract(afterAttackEventArgs))
{
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", 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 {targetEnt.Name} that isn't the parent, child, or in the same container");
return; return;
} }
ClickFace(player, coordinates); // TODO: Replace with body attack range when we get something like arm length or telekinesis or something.
if (!user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true))
if (!ActionBlockerSystem.CanAttack(player) ||
(!wideAttack && !player.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true)))
{
return; return;
} }
// In a container where the target entity is not the container's owner // Verify user has a hand, and find what object he is currently holding in his active hand
if (player.TryGetContainer(out var playerContainer) && if (user.TryGetComponent<IHandsComponent>(out var hands))
(!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)
{
return;
}
}
// Verify player has a hand, and find what object he is currently holding in his active hand
if (player.TryGetComponent<IHandsComponent>(out var hands))
{ {
var item = hands.GetActiveHand?.Owner; var item = hands.GetActiveHand?.Owner;
@@ -840,7 +753,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
{ {
if (wideAttack) if (wideAttack)
{ {
var ev = new WideAttackEvent(item, player, coordinates); var ev = new WideAttackEvent(item, user, coordinates);
RaiseLocalEvent(item.Uid, ev, false); RaiseLocalEvent(item.Uid, ev, false);
if(ev.Handled) if(ev.Handled)
@@ -848,30 +761,29 @@ namespace Content.Server.GameObjects.EntitySystems.Click
} }
else else
{ {
var ev = new ClickAttackEvent(item, player, coordinates, targetUid); var ev = new ClickAttackEvent(item, user, coordinates, targetUid);
RaiseLocalEvent(item.Uid, ev, false); RaiseLocalEvent(item.Uid, ev, false);
if(ev.Handled) if(ev.Handled)
return; return;
} }
} }
else else if (!wideAttack &&
(targetEnt != null || EntityManager.TryGetEntity(targetUid, out targetEnt)) &&
targetEnt.HasComponent<ItemComponent>())
{ {
// We pick up items if our hand is empty, even if we're in combat mode. // 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<ItemComponent>()) InteractHand(user, targetEnt);
{
Interaction(player, targetEnt);
return; return;
} }
} }
}
// TODO: Make this saner? // TODO: Make this saner?
// Attempt to do unarmed combat. We don't check for handled just because at this point it doesn't matter. // Attempt to do unarmed combat. We don't check for handled just because at this point it doesn't matter.
if(wideAttack) if(wideAttack)
RaiseLocalEvent(player.Uid, new WideAttackEvent(player, player, coordinates), false); RaiseLocalEvent(user.Uid, new WideAttackEvent(user, user, coordinates), false);
else else
RaiseLocalEvent(player.Uid, new ClickAttackEvent(player, player, coordinates, targetUid), false); RaiseLocalEvent(user.Uid, new ClickAttackEvent(user, user, coordinates, targetUid), false);
} }
} }
} }

View File

@@ -188,19 +188,8 @@ namespace Content.Shared.GameObjects.EntitySystems
bool ignoreInsideBlocker = false, bool ignoreInsideBlocker = false,
bool popup = false) bool popup = false)
{ {
var originPosition = origin.Transform.MapPosition;
var otherPosition = other.Transform.MapPosition;
predicate ??= e => e == origin || e == other; predicate ??= e => e == origin || e == other;
return InRangeUnobstructed(origin, other.Transform.MapPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup);
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;
} }
/// <summary> /// <summary>
@@ -244,19 +233,7 @@ namespace Content.Shared.GameObjects.EntitySystems
bool ignoreInsideBlocker = false, bool ignoreInsideBlocker = false,
bool popup = false) bool popup = false)
{ {
var originPosition = origin.Transform.MapPosition; return InRangeUnobstructed(origin, other.Owner, range, collisionMask, predicate, ignoreInsideBlocker, popup);
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;
} }
/// <summary> /// <summary>
@@ -300,19 +277,7 @@ namespace Content.Shared.GameObjects.EntitySystems
bool ignoreInsideBlocker = false, bool ignoreInsideBlocker = false,
bool popup = false) bool popup = false)
{ {
var originPosition = origin.Transform.MapPosition; return InRangeUnobstructed(origin, other.ToMap(EntityManager), range, collisionMask, predicate, ignoreInsideBlocker, popup);
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;
} }
/// <summary> /// <summary>
@@ -369,235 +334,5 @@ namespace Content.Shared.GameObjects.EntitySystems
return inRange; return inRange;
} }
/// <summary>
/// Checks that the user and target of a
/// <see cref="ITargetedInteractEventArgs"/> are within a certain
/// distance without any entity that matches the collision mask
/// obstructing them.
/// If the <paramref name="range"/> is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
/// </summary>
/// <param name="args">The event args to use.</param>
/// <param name="range">
/// Maximum distance between the two entity and set of map coordinates.
/// </param>
/// <param name="collisionMask">The mask to check for collisions.</param>
/// <param name="predicate">
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
/// </param>
/// <param name="ignoreInsideBlocker">
/// 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.
/// </param>
/// <param name="popup">
/// Whether or not to popup a feedback message on the user entity for
/// it to see.
/// </param>
/// <returns>
/// True if the two points are within a given range without being obstructed.
/// </returns>
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);
}
/// <summary>
/// Checks that the user of a <see cref="DragDropEvent"/> is within a
/// certain distance of the target and dropped entities without any entity
/// that matches the collision mask obstructing them.
/// If the <paramref name="range"/> is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
/// </summary>
/// <param name="args">The event args to use.</param>
/// <param name="range">
/// Maximum distance between the two entity and set of map coordinates.
/// </param>
/// <param name="collisionMask">The mask to check for collisions.</param>
/// <param name="predicate">
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
/// </param>
/// <param name="ignoreInsideBlocker">
/// 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.
/// </param>
/// <param name="popup">
/// Whether or not to popup a feedback message on the user entity for
/// it to see.
/// </param>
/// <returns>
/// True if the two points are within a given range without being obstructed.
/// </returns>
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;
}
/// <summary>
/// Checks that the user and target of a
/// <see cref="AfterInteractEventArgs"/> are within a certain distance
/// without any entity that matches the collision mask obstructing them.
/// If the <paramref name="range"/> is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
/// </summary>
/// <param name="args">The event args to use.</param>
/// <param name="range">
/// Maximum distance between the two entity and set of map coordinates.
/// </param>
/// <param name="collisionMask">The mask to check for collisions.</param>
/// <param name="predicate">
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
/// </param>
/// <param name="ignoreInsideBlocker">
/// 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.
/// </param>
/// <param name="popup">
/// Whether or not to popup a feedback message on the user entity for
/// it to see.
/// </param>
/// <returns>
/// True if the two points are within a given range without being obstructed.
/// </returns>
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);
}
/// <summary>
/// Checks that the user and target of a
/// <see cref="AfterInteractEvent"/> are within a certain distance
/// without any entity that matches the collision mask obstructing them.
/// If the <paramref name="range"/> is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
/// </summary>
/// <param name="args">The event args to use.</param>
/// <param name="range">
/// Maximum distance between the two entity and set of map coordinates.
/// </param>
/// <param name="collisionMask">The mask to check for collisions.</param>
/// <param name="predicate">
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
/// </param>
/// <param name="ignoreInsideBlocker">
/// 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.
/// </param>
/// <param name="popup">
/// Whether or not to popup a feedback message on the user entity for
/// it to see.
/// </param>
/// <returns>
/// True if the two points are within a given range without being obstructed.
/// </returns>
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);
}
} }
} }

View File

@@ -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. /// Raised when a target entity is interacted with by a user with an empty hand.
/// </summary> /// </summary>
[PublicAPI] [PublicAPI]
public class AttackHandEvent : HandledEntityEventArgs public class InteractHandEvent : HandledEntityEventArgs
{ {
/// <summary> /// <summary>
/// Entity that triggered the interaction. /// Entity that triggered the interaction.
@@ -47,7 +47,7 @@ namespace Content.Shared.Interfaces.GameObjects.Components
/// </summary> /// </summary>
public IEntity Target { get; } public IEntity Target { get; }
public AttackHandEvent(IEntity user, IEntity target) public InteractHandEvent(IEntity user, IEntity target)
{ {
User = user; User = user;
Target = target; Target = target;

View File

@@ -1,10 +1,12 @@
#nullable enable #nullable enable
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Physics; using Content.Shared.Physics;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map; using Robust.Shared.Map;
using static Content.Shared.GameObjects.EntitySystems.SharedInteractionSystem; using static Content.Shared.GameObjects.EntitySystems.SharedInteractionSystem;
@@ -402,20 +404,7 @@ namespace Content.Shared.Utility
bool ignoreInsideBlocker = false, bool ignoreInsideBlocker = false,
bool popup = false) bool popup = false)
{ {
return SharedInteractionSystem.InRangeUnobstructed(args, range, collisionMask, predicate, return SharedInteractionSystem.InRangeUnobstructed(args.User, args.Target, range, collisionMask, predicate, ignoreInsideBlocker, popup);
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);
} }
public static bool InRangeUnobstructed( public static bool InRangeUnobstructed(
@@ -426,12 +415,39 @@ namespace Content.Shared.Utility
bool ignoreInsideBlocker = false, bool ignoreInsideBlocker = false,
bool popup = false) bool popup = false)
{ {
return SharedInteractionSystem.InRangeUnobstructed(args, range, collisionMask, predicate, var user = args.User;
ignoreInsideBlocker, popup); 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 #endregion
#region EntityEventArgs #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( public static bool InRangeUnobstructed(
this AfterInteractEvent args, this AfterInteractEvent args,
float range = InteractionRange, float range = InteractionRange,
@@ -440,8 +456,13 @@ namespace Content.Shared.Utility
bool ignoreInsideBlocker = false, bool ignoreInsideBlocker = false,
bool popup = false) bool popup = false)
{ {
return SharedInteractionSystem.InRangeUnobstructed(args, range, collisionMask, predicate, var user = args.User;
ignoreInsideBlocker, popup); 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 #endregion
} }