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.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> ClickAttackEvent;
public EntityEventHandler<InteractUsingEvent> InteractUsingEvent;
public EntityEventHandler<AttackHandEvent> 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<ClickAttackEvent>((e) => ClickAttackEvent?.Invoke(e));
SubscribeLocalEvent<InteractUsingEvent>((e) => InteractUsingEvent?.Invoke(e));
SubscribeLocalEvent<AttackHandEvent>((e) => InteractHandEvent?.Invoke(e));
}
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, 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]
@@ -40,8 +321,9 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
{
ContentBeforeIoC = () =>
{
IoCManager.Resolve<IEntitySystemManager>().LoadExtraSystemType<TestAttackEntitySystem>();
}
IoCManager.Resolve<IEntitySystemManager>().LoadExtraSystemType<TestInteractionSystem>();
},
FailureLogLevel = Robust.Shared.Log.LogLevel.Error
});
await server.WaitIdleAsync();
@@ -49,23 +331,38 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
var entityManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
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<HandsComponent>().AddHand("hand");
other = entityManager.SpawnEntity(null, coordinates);
containerEntity = entityManager.SpawnEntity(null, coordinates);
user = entityManager.SpawnEntity(null, coords);
user.EnsureComponent<HandsComponent>().AddHand("hand");
target = entityManager.SpawnEntity(null, coords);
item = entityManager.SpawnEntity(null, coords);
item.EnsureComponent<ItemComponent>();
containerEntity = entityManager.SpawnEntity(null, coords);
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();
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<IEntitySystemManager>();
Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(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<TestAttackEntitySystem>(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<ItemComponent>();
Assert.That(user.TryGetComponent<HandsComponent>(out var hands));
Assert.That(hands.PutInHand(item.GetComponent<ItemComponent>()));
Assert.That(origin.TryGetComponent<HandsComponent>(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<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>();
interactionSystem.UseItemInHand(_owner, _target.Transform.Coordinates, _target.Uid);
interactionSystem.AiUseInteraction(_owner, _target.Transform.Coordinates, _target.Uid);
_elapsedTime += frameTime;
return Outcome.Continuing;
}

View File

@@ -83,7 +83,7 @@ namespace Content.Server.AI.Operators.Combat.Melee
}
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;
return Outcome.Continuing;
}

View File

@@ -40,7 +40,7 @@ namespace Content.Server.AI.Operators.Inventory
// Click on da thing
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;
}

View File

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

View File

@@ -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<InteractionSystem>().HandleClientUseItemInHand(player, coordinates, target);
EntitySystem.Get<InteractionSystem>().HandleUseInteraction(player, coordinates, target);
return;
}

View File

@@ -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));
}

View File

@@ -664,13 +664,13 @@ namespace Content.Server.GameObjects.Components.GUI
var interactionSystem = _entitySystemManager.GetEntitySystem<InteractionSystem>();
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);
}
}

View File

@@ -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))

View File

@@ -31,10 +31,10 @@ namespace Content.Server.GameObjects.EntitySystems
SubscribeLocalEvent<BuckleComponent, EntRemovedFromContainerMessage>(ContainerModifiedBuckle);
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);
}

View File

@@ -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<DragDropRequestEvent>(HandleDragDropMessage);
SubscribeNetworkEvent<DragDropRequestEvent>(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<InteractionSystem>();
}
@@ -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
/// </summary>
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
/// <param name="entity"></param>
/// <param name="coords"></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>())
{
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<ITransformComponent>(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<IHandsComponent>(out var hands))
{
// Verify user has a hand, and find what object he is currently holding in his active hand
if (!user.TryGetComponent<IHandsComponent>(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
/// <summary>
/// We didn't click on any entity, try doing an AfterInteract on the click location
/// </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);
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<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);
await DoAfterInteract(weapon, afterInteractEventArgs);
return false;
}
/// <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
/// NOTE: Does not have an InRangeUnobstructed check
/// </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);
RaiseLocalEvent(attacked.Uid, attackMsg);
if (attackMsg.Handled)
if (!ActionBlockerSystem.CanInteract(user))
return;
var attackBys = attacked.GetAllComponents<IInteractUsing>().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<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;
}
// 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);
}
/// <summary>
/// Uses an empty hand on an entity
/// Finds components with the InteractHand interface and calls their function
/// NOTE: Does not have an InRangeUnobstructed check
/// </summary>
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<IInteractHand>().ToList();
foreach (var interactHandComp in interactHandComps)
{
var attackHands = attacked.GetAllComponents<IInteractHand>().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
/// <summary>
/// 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<IUse>().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
/// <summary>
/// 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<IThrown>().ToList();
var args = new ThrownEventArgs(user);
@@ -585,7 +522,9 @@ namespace Content.Server.GameObjects.EntitySystems.Click
comp.Thrown(args);
}
}
#endregion
#region Equip
/// <summary>
/// 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<IEquipped>().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<IUnequipped>().ToList();
@@ -630,6 +565,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
}
}
#region Equip Hand
/// <summary>
/// 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<IEquippedHand>().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<IUnequippedHand>().ToList();
@@ -671,7 +603,10 @@ namespace Content.Server.GameObjects.EntitySystems.Click
comp.UnequippedHand(new UnequippedHandEventArgs(user, hand));
}
}
#endregion
#endregion
#region Drop
/// <summary>
/// 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
/// <summary>
/// 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<IHandSelected>().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<IHandDeselected>().ToList();
@@ -751,88 +682,70 @@ namespace Content.Server.GameObjects.EntitySystems.Click
comp.HandDeselected(new HandDeselectedEventArgs(user));
}
}
#endregion
#endregion
/// <summary>
/// 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
/// </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);
RaiseLocalEvent(attacked.Uid, rangedMsg);
if (rangedMsg.Handled)
return;
var rangedAttackBys = attacked.GetAllComponents<IRangedInteract>().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<IRangedInteract>().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<IAfterInteract>().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<IHandsComponent>(out var hands))
// Verify user has a hand, and find what object he is currently holding in his active hand
if (user.TryGetComponent<IHandsComponent>(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<ItemComponent>())
{
// 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>())
{
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);
}
}
}

View File

@@ -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);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -369,235 +334,5 @@ namespace Content.Shared.GameObjects.EntitySystems
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.
/// </summary>
[PublicAPI]
public class AttackHandEvent : HandledEntityEventArgs
public class InteractHandEvent : HandledEntityEventArgs
{
/// <summary>
/// Entity that triggered the interaction.
@@ -47,7 +47,7 @@ namespace Content.Shared.Interfaces.GameObjects.Components
/// </summary>
public IEntity Target { get; }
public AttackHandEvent(IEntity user, IEntity target)
public InteractHandEvent(IEntity user, IEntity target)
{
User = user;
Target = target;

View File

@@ -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
}