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);
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; }
--- /dev/null
+#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));
+ }
+}
+
+++ /dev/null
-#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();
- }
- }
-}
// 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";
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;
/// <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);
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
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>
return false;
}
- var first = ui.Interfaces.First();
-
-
bui = ui.Interfaces.FirstOrDefault(x => x.UiKey.Equals(key));
if (bui == null)
{
}
#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
}
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;
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public abstract partial class InteractionTest
{
+ protected virtual string PlayerPrototype => "AdminObserver";
+
protected PairTracker PairTracker = default!;
protected TestMapData MapData = default!;
/// </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>
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!;
// 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()
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>();
// 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>();
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);
});
// 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]
--- /dev/null
+#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;
+ }
+}
+
--- /dev/null
+#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);
+ }
+}
+
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>
// 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,
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);