HandsSystem Refactor (#38438)

* checkpoint

* pt 2

* pt... i forgot

* pt 4

* patch

* More test fixes

* optimization!!!

* the REAL hand system

* fix RetractableItemActionSystem.cs oversight

* the review

* test

* remove test usage of body prototype

* Update Content.IntegrationTests/Tests/Interaction/InteractionTest.cs

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>

* hellcode

* hellcode 2

* Minor cleanup

* test

* Chasing the last of the bugs

* changes

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
This commit is contained in:
Nemanja
2025-06-25 09:13:03 -04:00
committed by GitHub
parent 6cffa8aabe
commit 524725d378
79 changed files with 849 additions and 897 deletions

View File

@@ -25,7 +25,7 @@ public sealed class RetractableItemActionTest : InteractionTest
await Server.WaitAssertion(() =>
{
// Make sure the player's hand starts empty
var heldItem = Hands.ActiveHandEntity;
var heldItem = handsSystem.GetActiveItem((playerUid, Hands));
Assert.That(heldItem, Is.Null, $"Player is holding an item ({SEntMan.ToPrettyString(heldItem)}) at start of test.");
// Inspect the action prototype to find the item it spawns
@@ -43,14 +43,14 @@ public sealed class RetractableItemActionTest : InteractionTest
var actionEnt = actionsSystem.GetAction(actionUid);
// Make sure the player's hand is still empty
heldItem = Hands.ActiveHandEntity;
heldItem = handsSystem.GetActiveItem((playerUid, Hands));
Assert.That(heldItem, Is.Null, $"Player is holding an item ({SEntMan.ToPrettyString(heldItem)}) after adding action.");
// Activate the arm blade
actionsSystem.PerformAction(ToServer(Player), actionEnt!.Value);
// Make sure the player is now holding the expected item
heldItem = Hands.ActiveHandEntity;
heldItem = handsSystem.GetActiveItem((playerUid, Hands));
Assert.That(heldItem, Is.Not.Null, $"Expected player to be holding {spawnedProtoId} but was holding nothing.");
AssertPrototype(spawnedProtoId, SEntMan.GetNetEntity(heldItem));
@@ -58,7 +58,7 @@ public sealed class RetractableItemActionTest : InteractionTest
actionsSystem.PerformAction(ToServer(Player), actionEnt.Value);
// Make sure the player's hand is empty again
heldItem = Hands.ActiveHandEntity;
heldItem = handsSystem.GetActiveItem((playerUid, Hands));
Assert.That(heldItem, Is.Null, $"Player is still holding an item ({SEntMan.ToPrettyString(heldItem)}) after second use.");
});
}

View File

@@ -293,9 +293,9 @@ namespace Content.IntegrationTests.Tests.Buckle
Assert.That(buckle.Buckled);
// With items in all hands
foreach (var hand in hands.Hands.Values)
foreach (var hand in hands.Hands.Keys)
{
Assert.That(hand.HeldEntity, Is.Not.Null);
Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Not.Null);
}
var bodySystem = entityManager.System<BodySystem>();
@@ -316,9 +316,9 @@ namespace Content.IntegrationTests.Tests.Buckle
Assert.That(buckle.Buckled);
// Now with no item in any hand
foreach (var hand in hands.Hands.Values)
foreach (var hand in hands.Hands.Keys)
{
Assert.That(hand.HeldEntity, Is.Null);
Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Null);
}
buckleSystem.Unbuckle(human, human);

View File

@@ -1,7 +1,6 @@
using Content.Client.Chemistry.UI;
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Chemistry;
using Content.Server.Chemistry.Components;
using Content.Shared.Containers.ItemSlots;
namespace Content.IntegrationTests.Tests.Chemistry;
@@ -19,7 +18,7 @@ public sealed class DispenserTest : InteractionTest
// Insert beaker
await InteractUsing("Beaker");
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
// Open BUI
await Interact();
@@ -29,18 +28,18 @@ public sealed class DispenserTest : InteractionTest
await SendBui(ReagentDispenserUiKey.Key, ev);
// Beaker is back in the player's hands
Assert.That(Hands.ActiveHandEntity, Is.Not.Null);
AssertPrototype("Beaker", SEntMan.GetNetEntity(Hands.ActiveHandEntity));
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Not.Null);
AssertPrototype("Beaker", SEntMan.GetNetEntity(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands))));
// Re-insert the beaker
await Interact();
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
// Re-eject using the button directly instead of sending a BUI event. This test is really just a test of the
// bui/window helper methods.
await ClickControl<ReagentDispenserWindow>(nameof(ReagentDispenserWindow.EjectButton));
await RunTicks(5);
Assert.That(Hands.ActiveHandEntity, Is.Not.Null);
AssertPrototype("Beaker", SEntMan.GetNetEntity(Hands.ActiveHandEntity));
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Not.Null);
AssertPrototype("Beaker", SEntMan.GetNetEntity(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands))));
}
}

View File

@@ -267,7 +267,7 @@ public sealed class SuicideCommandTests
await server.WaitPost(() =>
{
var item = entManager.SpawnEntity("SharpTestObject", transformSystem.GetMapCoordinates(player));
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHandId!));
entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
Assert.That(executionComponent, Is.Not.EqualTo(null));
});
@@ -342,7 +342,7 @@ public sealed class SuicideCommandTests
await server.WaitPost(() =>
{
var item = entManager.SpawnEntity("MixedDamageTestObject", transformSystem.GetMapCoordinates(player));
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHandId!));
entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
Assert.That(executionComponent, Is.Not.EqualTo(null));
});

View File

@@ -13,10 +13,10 @@ public sealed class WallConstruction : InteractionTest
{
await StartConstruction(Wall);
await InteractUsing(Steel, 2);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
ClientAssertPrototype(Girder, Target);
await InteractUsing(Steel, 2);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
AssertPrototype(WallSolid);
}

View File

@@ -53,20 +53,20 @@ public sealed class HandTests
var xform = entMan.GetComponent<TransformComponent>(player);
item = entMan.SpawnEntity("Crowbar", tSys.GetMapCoordinates(player, xform: xform));
hands = entMan.GetComponent<HandsComponent>(player);
sys.TryPickup(player, item, hands.ActiveHand!);
sys.TryPickup(player, item, hands.ActiveHandId!);
});
// run ticks here is important, as errors may happen within the container system's frame update methods.
await pair.RunTicksSync(5);
Assert.That(hands.ActiveHandEntity, Is.EqualTo(item));
Assert.That(sys.GetActiveItem((player, hands)), Is.EqualTo(item));
await server.WaitPost(() =>
{
sys.TryDrop(player, item, null!);
sys.TryDrop(player, item);
});
await pair.RunTicksSync(5);
Assert.That(hands.ActiveHandEntity, Is.Null);
Assert.That(sys.GetActiveItem((player, hands)), Is.Null);
await server.WaitPost(() => mapSystem.DeleteMap(data.MapId));
await pair.CleanReturnAsync();
@@ -105,10 +105,10 @@ public sealed class HandTests
player = playerMan.Sessions.First().AttachedEntity!.Value;
tSys.PlaceNextTo(player, item);
hands = entMan.GetComponent<HandsComponent>(player);
sys.TryPickup(player, item, hands.ActiveHand!);
sys.TryPickup(player, item, hands.ActiveHandId!);
});
await pair.RunTicksSync(5);
Assert.That(hands.ActiveHandEntity, Is.EqualTo(item));
Assert.That(sys.GetActiveItem((player, hands)), Is.EqualTo(item));
// Open then close the box to place the player, who is holding the crowbar, inside of it
var storage = server.System<EntityStorageSystem>();
@@ -125,12 +125,12 @@ public sealed class HandTests
// with the item not being in the player's hands
await server.WaitPost(() =>
{
sys.TryDrop(player, item, null!);
sys.TryDrop(player, item);
});
await pair.RunTicksSync(5);
var xform = entMan.GetComponent<TransformComponent>(player);
var itemXform = entMan.GetComponent<TransformComponent>(item);
Assert.That(hands.ActiveHandEntity, Is.Not.EqualTo(item));
Assert.That(sys.GetActiveItem((player, hands)), Is.Not.EqualTo(item));
Assert.That(containerSystem.IsInSameOrNoContainer((player, xform), (item, itemXform)));
await server.WaitPost(() => mapSystem.DeleteMap(map.MapId));

View File

@@ -120,18 +120,18 @@ public abstract partial class InteractionTest
/// </summary>
protected async Task DeleteHeldEntity()
{
if (Hands.ActiveHandEntity is { } held)
if (HandSys.GetActiveItem((ToServer(Player), Hands)) is { } held)
{
await Server.WaitPost(() =>
{
Assert.That(HandSys.TryDrop(SEntMan.GetEntity(Player), null, false, true, Hands));
Assert.That(HandSys.TryDrop((SEntMan.GetEntity(Player), Hands), null, false, true));
SEntMan.DeleteEntity(held);
SLogger.Debug($"Deleting held entity");
});
}
await RunTicks(1);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.Null);
}
/// <summary>
@@ -152,7 +152,7 @@ public abstract partial class InteractionTest
/// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableToggleable = true)
{
if (Hands.ActiveHand == null)
if (Hands.ActiveHandId == null)
{
Assert.Fail("No active hand");
return default;
@@ -169,7 +169,7 @@ public abstract partial class InteractionTest
{
var playerEnt = SEntMan.GetEntity(Player);
Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHand, false, false, Hands));
Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHandId, false, false, false, Hands));
// turn on welders
if (enableToggleable && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
@@ -179,7 +179,7 @@ public abstract partial class InteractionTest
});
await RunTicks(1);
Assert.That(Hands.ActiveHandEntity, Is.EqualTo(item));
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.EqualTo(item));
if (enableToggleable && itemToggle != null)
Assert.That(itemToggle.Activated);
@@ -193,7 +193,7 @@ public abstract partial class InteractionTest
{
entity ??= Target;
if (Hands.ActiveHand == null)
if (Hands.ActiveHandId == null)
{
Assert.Fail("No active hand");
return;
@@ -212,11 +212,11 @@ public abstract partial class InteractionTest
await Server.WaitPost(() =>
{
Assert.That(HandSys.TryPickup(SEntMan.GetEntity(Player), uid.Value, Hands.ActiveHand, false, false, Hands, item));
Assert.That(HandSys.TryPickup(ToServer(Player), uid.Value, Hands.ActiveHandId, false, false, false, Hands, item));
});
await RunTicks(1);
Assert.That(Hands.ActiveHandEntity, Is.EqualTo(uid));
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.EqualTo(uid));
}
/// <summary>
@@ -224,7 +224,7 @@ public abstract partial class InteractionTest
/// </summary>
protected async Task Drop()
{
if (Hands.ActiveHandEntity == null)
if (HandSys.GetActiveItem((ToServer(Player), Hands)) == null)
{
Assert.Fail("Not holding any entity to drop");
return;
@@ -232,11 +232,11 @@ public abstract partial class InteractionTest
await Server.WaitPost(() =>
{
Assert.That(HandSys.TryDrop(SEntMan.GetEntity(Player), handsComp: Hands));
Assert.That(HandSys.TryDrop((ToServer(Player), Hands)));
});
await RunTicks(1);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.Null);
}
#region Interact
@@ -246,7 +246,7 @@ public abstract partial class InteractionTest
/// </summary>
protected async Task UseInHand()
{
if (Hands.ActiveHandEntity is not { } target)
if (HandSys.GetActiveItem((ToServer(Player), Hands)) is not { } target)
{
Assert.Fail("Not holding any entity");
return;

View File

@@ -1,15 +1,12 @@
#nullable enable
using System.Linq;
using System.Numerics;
using Content.Client.Construction;
using Content.Client.Examine;
using Content.Client.Gameplay;
using Content.IntegrationTests.Pair;
using Content.Server.Body.Systems;
using Content.Server.Hands.Systems;
using Content.Server.Stack;
using Content.Server.Tools;
using Content.Shared.Body.Part;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
@@ -135,10 +132,13 @@ public abstract partial class InteractionTest
- type: entity
id: InteractionTestMob
components:
- type: Body
prototype: Aghost
- type: DoAfter
- type: Hands
hands:
hand_right: # only one hand, so that they do not accidentally pick up deconstruction products
location: Right
sortedHands:
- hand_right
- type: ComplexInteraction
- type: MindContainer
- type: Stripping
@@ -230,20 +230,6 @@ public abstract partial class InteractionTest
SEntMan.DeleteEntity(old.Value);
});
// Ensure that the player only has one hand, so that they do not accidentally pick up deconstruction products
await Server.WaitPost(() =>
{
// I lost an hour of my life trying to track down how the hell interaction tests were breaking
// so greatz to this. Just make your own body prototype!
var bodySystem = SEntMan.System<BodySystem>();
var hands = bodySystem.GetBodyChildrenOfType(SEntMan.GetEntity(Player), BodyPartType.Hand).ToArray();
for (var i = 1; i < hands.Length; i++)
{
SEntMan.DeleteEntity(hands[i].Id);
}
});
// Change UI state to in-game.
var state = Client.ResolveDependency<IStateManager>();
await Client.WaitPost(() => state.RequestStateChange<GameplayState>());

View File

@@ -17,7 +17,7 @@ public sealed class TileConstructionTests : InteractionTest
await SetTile(null);
await InteractUsing(Rod);
await AssertTile(Lattice);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
await InteractUsing(Cut);
await AssertTile(null);
await AssertEntityLookup((Rod, 1));
@@ -49,7 +49,7 @@ public sealed class TileConstructionTests : InteractionTest
AssertGridCount(1);
// Cut lattice
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
await InteractUsing(Cut);
await AssertTile(null);
AssertGridCount(0);
@@ -83,13 +83,13 @@ public sealed class TileConstructionTests : InteractionTest
// Lattice -> Plating
await InteractUsing(FloorItem);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
await AssertTile(Plating);
AssertGridCount(1);
// Plating -> Tile
await InteractUsing(FloorItem);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
await AssertTile(Floor);
AssertGridCount(1);