Add climb & slip tests (#15459)

This commit is contained in:
Leon Friedrich
2023-04-17 18:07:03 +12:00
committed by GitHub
parent 27fbc4e235
commit 8af149e61c
11 changed files with 360 additions and 106 deletions

View File

@@ -564,7 +564,8 @@ we are just going to end this here to save a lot of time. This is the exception
mapData.MapId = mapManager.CreateMap();
mapData.MapUid = mapManager.GetMapEntityId(mapData.MapId);
mapData.MapGrid = mapManager.CreateGrid(mapData.MapId);
mapData.GridCoords = new EntityCoordinates(mapData.MapGrid.Owner, 0, 0);
mapData.GridUid = mapData.MapGrid.Owner;
mapData.GridCoords = new EntityCoordinates(mapData.GridUid, 0, 0);
var tileDefinitionManager = IoCManager.Resolve<ITileDefinitionManager>();
var plating = tileDefinitionManager["Plating"];
var platingTile = new Tile(plating.TileId);
@@ -793,6 +794,7 @@ public sealed class PoolSettings
public sealed class TestMapData
{
public EntityUid MapUid { get; set; }
public EntityUid GridUid { get; set; }
public MapId MapId { get; set; }
public MapGridComponent MapGrid { get; set; }
public EntityCoordinates GridCoords { get; set; }

View File

@@ -0,0 +1,64 @@
#nullable enable
using System.Threading.Tasks;
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.Climbing;
using Content.Shared.Climbing;
using NUnit.Framework;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
namespace Content.IntegrationTests.Tests.Climbing;
public sealed class ClimbingTest : MovementTest
{
[Test]
public async Task ClimbTableTest()
{
// Spawn a table to the right of the player.
await SpawnTarget("Table");
Assert.That(Delta(), Is.GreaterThan(0));
// Player is not initially climbing anything.
var comp = Comp<ClimbingComponent>(Player);
Assert.That(comp.IsClimbing, Is.False);
Assert.That(comp.DisabledFixtureMasks.Count, Is.EqualTo(0));
// Attempt (and fail) to walk past the table.
await Move(DirectionFlag.East, 1f);
Assert.That(Delta(), Is.GreaterThan(0));
// Try to start climbing
var sys = SEntMan.System<ClimbSystem>();
await Server.WaitPost(() => sys.TryClimb(Player, Player, Target.Value));
await AwaitDoAfters();
// Player should now be climbing
Assert.That(comp.IsClimbing, Is.True);
Assert.That(comp.DisabledFixtureMasks.Count, Is.GreaterThan(0));
// Can now walk over the table.
await Move(DirectionFlag.East, 1f);
Assert.That(Delta(), Is.LessThan(0));
// After walking away from the table, player should have stopped climbing.
Assert.That(comp.IsClimbing, Is.False);
Assert.That(comp.DisabledFixtureMasks.Count, Is.EqualTo(0));
// Try to walk back to the other side (and fail).
await Move(DirectionFlag.West, 1f);
Assert.That(Delta(), Is.LessThan(0));
// Start climbing
await Server.WaitPost(() => sys.TryClimb(Player, Player, Target.Value));
await AwaitDoAfters();
Assert.That(comp.IsClimbing, Is.True);
Assert.That(comp.DisabledFixtureMasks.Count, Is.GreaterThan(0));
// Walk past table and stop climbing again.
await Move(DirectionFlag.West, 1f);
Assert.That(Delta(), Is.GreaterThan(0));
Assert.That(comp.IsClimbing, Is.False);
Assert.That(comp.DisabledFixtureMasks.Count, Is.EqualTo(0));
}
}

View File

@@ -1,72 +0,0 @@
#nullable enable
using System.Threading.Tasks;
using Content.Server.Climbing.Components;
using Content.Shared.Climbing;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.GameObjects.Components.Movement
{
[TestFixture]
[TestOf(typeof(ClimbableComponent))]
[TestOf(typeof(ClimbingComponent))]
public sealed class ClimbUnitTest
{
private const string Prototypes = @"
- type: entity
name: HumanDummy
id: HumanDummy
components:
- type: Climbing
- type: Physics
- type: entity
name: TableDummy
id: TableDummy
components:
- type: Climbable
- type: Physics
";
[Test]
public async Task Test()
{
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
var server = pairTracker.Pair.Server;
EntityUid human;
EntityUid table;
ClimbingComponent climbing;
await server.WaitAssertion(() =>
{
var mapManager = IoCManager.Resolve<IMapManager>();
var entityManager = IoCManager.Resolve<IEntityManager>();
// Spawn the entities
human = entityManager.SpawnEntity("HumanDummy", MapCoordinates.Nullspace);
table = entityManager.SpawnEntity("TableDummy", MapCoordinates.Nullspace);
// Test for climb components existing
// Players and tables should have these in their prototypes.
Assert.That(entityManager.TryGetComponent(human, out climbing!), "Human has no climbing");
Assert.That(entityManager.TryGetComponent(table, out ClimbableComponent? _), "Table has no climbable");
// TODO ShadowCommander: Implement climbing test
// // Now let's make the player enter a climbing transitioning state.
// climbing.IsClimbing = true;
// EntitySystem.Get<ClimbSystem>().MoveEntityToward(human, table, climbing:climbing);
// var body = entityManager.GetComponent<PhysicsComponent>(human);
// // TODO: Check it's climbing
//
// // Force the player out of climb state. It should immediately remove the ClimbController.
// climbing.IsClimbing = false;
});
await pairTracker.CleanReturnAsync();
}
}
}

View File

@@ -5,8 +5,6 @@ namespace Content.IntegrationTests.Tests.Interaction;
// Should make it easier to mass-change hard coded strings if prototypes get renamed.
public abstract partial class InteractionTest
{
protected const string PlayerEntity = "AdminObserver";
// Tiles
protected const string Floor = "FloorSteel";
protected const string FloorItem = "FloorTileItemSteel";

View File

@@ -8,10 +8,15 @@ using System.Reflection;
using System.Threading.Tasks;
using Content.Client.Chemistry.UI;
using Content.Client.Construction;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Construction.Components;
using Content.Server.Gravity;
using Content.Server.Power.Components;
using Content.Server.Tools.Components;
using Content.Shared.Atmos;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Gravity;
using Content.Shared.Item;
using NUnit.Framework;
using OpenToolkit.GraphicsLibraryFramework;
@@ -84,8 +89,10 @@ public abstract partial class InteractionTest
/// <summary>
/// Spawn an entity entity and set it as the target.
/// </summary>
[MemberNotNull(nameof(Target))]
protected async Task SpawnTarget(string prototype)
{
Target = EntityUid.Invalid;
await Server.WaitPost(() =>
{
Target = SEntMan.SpawnEntity(prototype, TargetCoords);
@@ -493,6 +500,19 @@ public abstract partial class InteractionTest
Assert.That(tile.TypeId, Is.EqualTo(targetTile.TypeId));
}
protected void AssertGridCount(int value)
{
var count = 0;
var query = SEntMan.AllEntityQueryEnumerator<MapGridComponent, TransformComponent>();
while (query.MoveNext(out _, out var xform))
{
if (xform.MapUid == MapData.MapUid)
count++;
}
Assert.That(count, Is.EqualTo(value));
}
#endregion
#region Entity lookups
@@ -669,13 +689,20 @@ public abstract partial class InteractionTest
await RunTicks(5);
}
#region Time/Tick managment
protected async Task RunTicks(int ticks)
{
await PoolManager.RunTicksSync(PairTracker.Pair, ticks);
}
protected int SecondsToTicks(float seconds)
=> (int) Math.Ceiling(seconds / TickPeriod);
protected async Task RunSeconds(float seconds)
=> await RunTicks((int) Math.Ceiling(seconds / TickPeriod));
=> await RunTicks(SecondsToTicks(seconds));
#endregion
#region BUI
/// <summary>
@@ -723,9 +750,6 @@ public abstract partial class InteractionTest
return false;
}
var first = ui.Interfaces.First();
bui = ui.Interfaces.FirstOrDefault(x => x.UiKey.Equals(key));
if (bui == null)
{
@@ -878,4 +902,110 @@ public abstract partial class InteractionTest
}
#endregion
#region Map Setup
/// <summary>
/// Adds gravity to a given entity. Defaults to the grid if no entity is specified.
/// </summary>
protected async Task AddGravity(EntityUid? uid = null)
{
var target = uid ?? MapData.GridUid;
await Server.WaitPost(() =>
{
var gravity = SEntMan.EnsureComponent<GravityComponent>(target);
SEntMan.System<GravitySystem>().EnableGravity(target, gravity);
});
}
/// <summary>
/// Adds a default atmosphere to the test map.
/// </summary>
protected async Task AddAtmosphere(EntityUid? uid = null)
{
var target = uid ?? MapData.MapUid;
await Server.WaitPost(() =>
{
var atmos = SEntMan.EnsureComponent<MapAtmosphereComponent>(target);
atmos.Space = false;
var moles = new float[Atmospherics.AdjustedNumberOfGases];
moles[(int) Gas.Oxygen] = 21.824779f;
moles[(int) Gas.Nitrogen] = 82.10312f;
atmos.Mixture = new GasMixture(2500)
{
Temperature = 293.15f,
Moles = moles,
};
});
}
#endregion
#region Inputs
/// <summary>
/// Make the client press and then release a key. This assumes the key is currently released.
/// </summary>
protected async Task PressKey(
BoundKeyFunction key,
int ticks = 1,
EntityCoordinates? coordinates = null,
EntityUid cursorEntity = default)
{
await SetKey(key, BoundKeyState.Down, coordinates, cursorEntity);
await RunTicks(ticks);
await SetKey(key, BoundKeyState.Up, coordinates, cursorEntity);
await RunTicks(1);
}
/// <summary>
/// Make the client press or release a key
/// </summary>
protected async Task SetKey(
BoundKeyFunction key,
BoundKeyState state,
EntityCoordinates? coordinates = null,
EntityUid cursorEntity = default)
{
var coords = coordinates ?? TargetCoords;
ScreenCoordinates screen = default;
var funcId = InputManager.NetworkBindMap.KeyFunctionID(key);
var message = new FullInputCmdMessage(CTiming.CurTick, CTiming.TickFraction, funcId, state,
coords, screen, cursorEntity);
await Client.WaitPost(() => InputSystem.HandleInputCommand(ClientSession, key, message));
}
/// <summary>
/// Variant of <see cref="SetKey"/> for setting movement keys.
/// </summary>
protected async Task SetMovementKey(DirectionFlag dir, BoundKeyState state)
{
if ((dir & DirectionFlag.South) != 0)
await SetKey(EngineKeyFunctions.MoveDown, state);
if ((dir & DirectionFlag.East) != 0)
await SetKey(EngineKeyFunctions.MoveRight, state);
if ((dir & DirectionFlag.North) != 0)
await SetKey(EngineKeyFunctions.MoveUp, state);
if ((dir & DirectionFlag.West) != 0)
await SetKey(EngineKeyFunctions.MoveLeft, state);
}
/// <summary>
/// Make the client hold the move key in some direction for some amount of time.
/// </summary>
protected async Task Move(DirectionFlag dir, float seconds)
{
await SetMovementKey(dir, BoundKeyState.Down);
await RunSeconds(seconds);
await SetMovementKey(dir, BoundKeyState.Up);
await RunTicks(1);
}
#endregion
}

View File

@@ -14,9 +14,12 @@ using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using NUnit.Framework;
using Robust.Client.GameObjects;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.UnitTesting;
@@ -34,6 +37,8 @@ namespace Content.IntegrationTests.Tests.Interaction;
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public abstract partial class InteractionTest
{
protected virtual string PlayerPrototype => "AdminObserver";
protected PairTracker PairTracker = default!;
protected TestMapData MapData = default!;
@@ -59,6 +64,9 @@ public abstract partial class InteractionTest
/// </summary>
protected EntityUid Player;
protected ICommonSession ClientSession = default!;
protected IPlayerSession ServerSession = default!;
/// <summary>
/// The current target entity. This is the default entity for various helper functions.
/// </summary>
@@ -79,7 +87,7 @@ public abstract partial class InteractionTest
protected ITileDefinitionManager TileMan = default!;
protected IMapManager MapMan = default!;
protected IPrototypeManager ProtoMan = default!;
protected IGameTiming Timing = default!;
protected IGameTiming STiming = default!;
protected IComponentFactory Factory = default!;
protected SharedHandsSystem HandSys = default!;
protected StackSystem Stack = default!;
@@ -92,16 +100,20 @@ public abstract partial class InteractionTest
// CLIENT dependencies
protected IEntityManager CEntMan = default!;
protected IGameTiming CTiming = default!;
protected IUserInterfaceManager UiMan = default!;
protected IInputManager InputManager = default!;
protected InputSystem InputSystem = default!;
protected ConstructionSystem CConSys = default!;
protected ExamineSystem ExamineSys = default!;
protected InteractionTestSystem CTestSystem = default!;
protected UserInterfaceSystem CUISystem = default!;
// player components
protected HandsComponent Hands = default!;
protected DoAfterComponent DoAfters = default!;
public float TickPeriod => (float)Timing.TickPeriod.TotalSeconds;
public float TickPeriod => (float)STiming.TickPeriod.TotalSeconds;
[SetUp]
public virtual async Task Setup()
@@ -114,7 +126,7 @@ public abstract partial class InteractionTest
MapMan = Server.ResolveDependency<IMapManager>();
ProtoMan = Server.ResolveDependency<IPrototypeManager>();
Factory = Server.ResolveDependency<IComponentFactory>();
Timing = Server.ResolveDependency<IGameTiming>();
STiming = Server.ResolveDependency<IGameTiming>();
HandSys = SEntMan.System<SharedHandsSystem>();
InteractSys = SEntMan.System<SharedInteractionSystem>();
ToolSys = SEntMan.System<ToolSystem>();
@@ -127,6 +139,9 @@ public abstract partial class InteractionTest
// client dependencies
CEntMan = Client.ResolveDependency<IEntityManager>();
UiMan = Client.ResolveDependency<IUserInterfaceManager>();
CTiming = Client.ResolveDependency<IGameTiming>();
InputManager = Client.ResolveDependency<IInputManager>();
InputSystem = CEntMan.System<InputSystem>();
CTestSystem = CEntMan.System<InteractionTestSystem>();
CConSys = CEntMan.System<ConstructionSystem>();
ExamineSys = CEntMan.System<ExamineSystem>();
@@ -142,16 +157,16 @@ public abstract partial class InteractionTest
var cPlayerMan = Client.ResolveDependency<Robust.Client.Player.IPlayerManager>();
if (cPlayerMan.LocalPlayer?.Session == null)
Assert.Fail("No player");
var cSession = cPlayerMan.LocalPlayer!.Session!;
var sSession = sPlayerMan.GetSessionByUserId(cSession.UserId);
ClientSession = cPlayerMan.LocalPlayer!.Session!;
ServerSession = sPlayerMan.GetSessionByUserId(ClientSession.UserId);
// Spawn player entity & attach
EntityUid? old = default;
await Server.WaitPost(() =>
{
old = cPlayerMan.LocalPlayer.ControlledEntity;
Player = SEntMan.SpawnEntity(PlayerEntity, PlayerCoords);
sSession.AttachToEntity(Player);
Player = SEntMan.SpawnEntity(PlayerPrototype, PlayerCoords);
ServerSession.AttachToEntity(Player);
Hands = SEntMan.GetComponent<HandsComponent>(Player);
DoAfters = SEntMan.GetComponent<DoAfterComponent>(Player);
});
@@ -189,7 +204,7 @@ public abstract partial class InteractionTest
// Final player asserts/checks.
await PoolManager.ReallyBeIdle(PairTracker.Pair, 5);
Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.EqualTo(Player));
Assert.That(sPlayerMan.GetSessionByUserId(cSession.UserId).AttachedEntity, Is.EqualTo(Player));
Assert.That(sPlayerMan.GetSessionByUserId(ClientSession.UserId).AttachedEntity, Is.EqualTo(Player));
}
[TearDown]

View File

@@ -0,0 +1,65 @@
#nullable enable
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Content.IntegrationTests.Tests.Interaction;
/// <summary>
/// This is a variation of <see cref="InteractionTest"/> that sets up the player with a normal human entity and a simple
/// linear grid with gravity and an atmosphere. It is intended to make it easier to test interactions that involve
/// walking (e.g., slipping or climbing tables).
/// </summary>
public abstract class MovementTest : InteractionTest
{
protected override string PlayerPrototype => "MobHuman";
/// <summary>
/// Number of tiles to add either side of the player.
/// </summary>
protected virtual int Tiles => 3;
/// <summary>
/// If true, the tiles at the ends of the grid will have a wall placed on them to avoid players moving off grid.
/// </summary>
protected virtual bool AddWalls => true;
[SetUp]
public override async Task Setup()
{
await base.Setup();
for (var i = -Tiles; i <= Tiles; i++)
{
await SetTile(Plating, PlayerCoords.Offset((i,0)), MapData.MapGrid);
}
AssertGridCount(1);
if (AddWalls)
{
await SpawnEntity("WallSolid", PlayerCoords.Offset((-Tiles,0)));
await SpawnEntity("WallSolid", PlayerCoords.Offset((Tiles,0)));
}
await AddGravity();
await AddAtmosphere();
}
/// <summary>
/// Get the relative horizontal between two entities. Defaults to using the target & player entity.
/// </summary>
protected float Delta(EntityUid? target = null, EntityUid? other = null)
{
target ??= Target;
if (target == null)
{
Assert.Fail("No target specified");
return 0;
}
var delta = Transform.GetWorldPosition(target.Value) - Transform.GetWorldPosition(other ?? Player);
return delta.X;
}
}

View File

@@ -0,0 +1,54 @@
#nullable enable
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Slippery;
using Content.Shared.Stunnable;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Maths;
namespace Content.IntegrationTests.Tests.Slipping;
public sealed class SlippingTest : MovementTest
{
public sealed class SlipTestSystem : EntitySystem
{
public HashSet<EntityUid> Slipped = new();
public override void Initialize()
{
SubscribeLocalEvent<SlipperyComponent, SlipEvent>(OnSlip);
}
private void OnSlip(EntityUid uid, SlipperyComponent component, ref SlipEvent args)
{
Slipped.Add(args.Slipped);
}
}
[Test]
public async Task BananaSlipTest()
{
var sys = SEntMan.System<SlipTestSystem>();
await SpawnTarget("TrashBananaPeel");
// Player is to the left of the banana peel and has not slipped.
Assert.That(Delta(), Is.GreaterThan(0.5f));
Assert.That(sys.Slipped.Contains(Player), Is.False);
// Walking over the banana slowly does not trigger a slip.
await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Down);
await Move(DirectionFlag.East, 1f);
Assert.That(Delta(), Is.LessThan(0.5f));
Assert.That(sys.Slipped.Contains(Player), Is.False);
AssertComp<KnockedDownComponent>(false, Player);
// Moving at normal speeds does trigger a slip.
await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Up);
await Move(DirectionFlag.West, 1f);
Assert.That(sys.Slipped.Contains(Player), Is.True);
AssertComp<KnockedDownComponent>(true, Player);
}
}

View File

@@ -9,19 +9,6 @@ namespace Content.IntegrationTests.Tests.Tiles;
public sealed class TileConstructionTests : InteractionTest
{
private void AssertGridCount(int value)
{
var count = 0;
var query = SEntMan.AllEntityQueryEnumerator<MapGridComponent, TransformComponent>();
while (query.MoveNext(out _, out var xform))
{
if (xform.MapUid == MapData.MapUid)
count++;
}
Assert.That(count, Is.EqualTo(value));
}
/// <summary>
/// Test placing and cutting a single lattice.
/// </summary>

View File

@@ -93,23 +93,31 @@ public sealed class ClimbSystem : SharedClimbSystem
// TODO VERBS ICON add a climbing icon?
args.Verbs.Add(new AlternativeVerb
{
Act = () => TryMoveEntity(component, args.User, args.User, args.Target),
Act = () => TryClimb(args.User, args.User, args.Target, component),
Text = Loc.GetString("comp-climbable-verb-climb")
});
}
private void OnClimbableDragDrop(EntityUid uid, ClimbableComponent component, ref DragDropTargetEvent args)
{
TryMoveEntity(component, args.User, args.Dragged, uid);
TryClimb(args.User, args.Dragged, uid, component);
}
private void TryMoveEntity(ClimbableComponent component, EntityUid user, EntityUid entityToMove,
EntityUid climbable)
public void TryClimb(EntityUid user,
EntityUid entityToMove,
EntityUid climbable,
ClimbableComponent? comp = null,
ClimbingComponent? climbing = null)
{
if (!TryComp(entityToMove, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing)
if (!Resolve(climbable, ref comp) || !Resolve(entityToMove, ref climbing))
return;
var args = new DoAfterArgs(user, component.ClimbDelay, new ClimbDoAfterEvent(), entityToMove, target: climbable, used: entityToMove)
// Note, IsClimbing does not mean a DoAfter is active, it means the target has already finished a DoAfter and
// is currently on top of something..
if (climbing.IsClimbing)
return;
var args = new DoAfterArgs(user, comp.ClimbDelay, new ClimbDoAfterEvent(), entityToMove, target: climbable, used: entityToMove)
{
BreakOnTargetMove = true,
BreakOnUserMove = true,

View File

@@ -121,5 +121,8 @@ public sealed class SlipAttemptEvent : CancellableEntityEventArgs, IInventoryRel
public SlotFlags TargetSlots { get; } = SlotFlags.FEET;
}
/// <summary>
/// This event is raised directed at an entity that CAUSED some other entity to slip (e.g., the banana peel).
/// </summary>
[ByRefEvent]
public readonly record struct SlipEvent(EntityUid Slipped);