#nullable enable
using System.Numerics;
using Content.Client.Construction;
using Content.Client.Examine;
using Content.Client.Gameplay;
using Content.Client.Interaction;
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.Server.Hands.Systems;
using Content.Server.Stack;
using Content.Server.Tools;
using Content.Shared.CombatMode;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.Input;
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Interaction;
///
/// This is a base class designed to make it easier to test various interactions like construction & DoAfters.
///
///
/// For construction tests, the interactions are intentionally hard-coded and not pulled automatically from the
/// construction graph, even though this may be a pain to maintain. This is because otherwise these tests could not
/// detect errors in the graph pathfinding (e.g., infinite loops, missing steps, etc).
///
[TestFixture]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public abstract partial class InteractionTest : GameTest
{
///
/// The prototype that will be spawned for the player entity at .
/// This is not a full humanoid and only has one hand by default.
///
protected virtual string PlayerPrototype => "InteractionTestMob";
///
/// The map path to load for the integration test.
/// If null an empty map with a single 1x1 plating grid will be generated.
///
protected virtual ResPath? TestMapPath => null;
protected TestMapData MapData => TestMap!;
protected MapId MapId => MapData.MapId;
///
/// Target coordinates. Note that this does not necessarily correspond to the position of the
/// entity.
///
protected NetCoordinates TargetCoords;
///
/// Initial player coordinates. Note that this does not necessarily correspond to the position of the
/// entity.
///
protected NetCoordinates PlayerCoords;
///
/// The player entity that performs all these interactions. Defaults to an admin-observer with 1 hand.
///
protected NetEntity Player;
protected EntityUid SPlayer;
protected EntityUid CPlayer;
protected ICommonSession ClientSession = default!;
///
/// The current target entity. This is the default entity for various helper functions.
///
///
/// Note that this target may be automatically modified by various interactions, in particular construction
/// interactions often swap out entities, and there are helper methods that attempt to automatically upddate
/// the target entity. See
///
protected NetEntity? Target;
protected EntityUid? STarget => ToServer(Target);
protected EntityUid? CTarget => ToClient(Target);
///
/// When attempting to start construction, this is the client-side ID of the construction ghost.
///
protected int ConstructionGhostId;
// SERVER dependencies
[SidedDependency(Side.Server)] protected ITileDefinitionManager TileMan = default!;
[SidedDependency(Side.Server)] protected IPrototypeManager ProtoMan => SProtoMan;
protected IGameTiming STiming => SGameTiming;
[SidedDependency(Side.Server)] protected IComponentFactory Factory = default!;
[SidedDependency(Side.Server)] protected HandsSystem HandSys = default!;
[SidedDependency(Side.Server)] protected StackSystem Stack = default!;
[SidedDependency(Side.Server)] protected SharedInteractionSystem InteractSys = default!;
[SidedDependency(Side.Server)] protected Content.Server.Construction.ConstructionSystem SConstruction = default!;
[SidedDependency(Side.Server)] protected SharedDoAfterSystem DoAfterSys = default!;
[SidedDependency(Side.Server)] protected ToolSystem ToolSys = default!;
[SidedDependency(Side.Server)] protected ItemToggleSystem ItemToggleSys = default!;
[SidedDependency(Side.Server)] protected InteractionTestSystem STestSystem = default!;
[SidedDependency(Side.Server)] protected SharedTransformSystem Transform = default!;
[SidedDependency(Side.Server)] protected SharedMapSystem MapSystem = default!;
[SidedDependency(Side.Server)] protected ILogManager SLogMan = default!;
protected ISawmill SLogger = default!;
[SidedDependency(Side.Server)] protected SharedUserInterfaceSystem SUiSys = default!;
[SidedDependency(Side.Server)] protected SharedCombatModeSystem SCombatMode = default!;
[SidedDependency(Side.Server)] protected SharedGunSystem SGun = default!;
[SidedDependency(Side.Server)] protected SharedMeleeWeaponSystem SMelee = default!;
// CLIENT dependencies
protected IClientGameTiming CTiming => CGameTiming;
[SidedDependency(Side.Client)] protected IUserInterfaceManager UiMan = default!;
[SidedDependency(Side.Client)] protected IInputManager InputManager = default!;
[SidedDependency(Side.Client)] protected Robust.Client.GameObjects.InputSystem InputSystem = default!;
[SidedDependency(Side.Client)] protected ConstructionSystem CConSys = default!;
[SidedDependency(Side.Client)] protected ExamineSystem ExamineSys = default!;
[SidedDependency(Side.Client)] protected InteractionTestSystem CTestSystem = default!;
[SidedDependency(Side.Client)] protected ILogManager CLogMan = default!;
protected ISawmill CLogger = default!;
[SidedDependency(Side.Client)] protected SharedUserInterfaceSystem CUiSys = default!;
[SidedDependency(Side.Client)] protected DragDropSystem CDragDropSys = default!;
// player components
protected HandsComponent? Hands;
protected DoAfterComponent? DoAfters;
public float TickPeriod => (float)STiming.TickPeriod.TotalSeconds;
// Simple mob that has one hand and can perform misc interactions.
[TestPrototypes]
private const string TestPrototypes = @"
- type: entity
id: InteractionTestMob
components:
- 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
- type: Puller
- type: Physics
- type: GravityAffected
- type: Tag
tags:
- CanPilot
- type: UserInterface
- type: CombatMode
";
protected static PoolSettings Default => new()
{
Connected = true,
Dirty = true
};
public override PoolSettings PoolSettings => Default;
///
/// This is here for backward-compatibility.
/// You probably should switch to .
///
public virtual async Task Setup()
{
//
}
public override async Task DoSetup()
{
await base.DoSetup();
// Setup map.
if (TestMapPath == null)
await Pair.CreateTestMap();
else
await Pair.LoadTestMap(TestMapPath.Value);
PlayerCoords = SEntMan.GetNetCoordinates(Transform.WithEntityId(MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)), MapData.MapUid));
TargetCoords = SEntMan.GetNetCoordinates(Transform.WithEntityId(MapData.GridCoords.Offset(new Vector2(1.5f, 0.5f)), MapData.MapUid));
await SetTile(Plating, grid: MapData.Grid);
// Get player data
var sPlayerMan = Server.ResolveDependency();
var cPlayerMan = Client.ResolveDependency();
if (Client.Session == null)
Assert.Fail("No player");
ClientSession = Client.Session!;
SLogger = SLogMan.RootSawmill;
CLogger = CLogMan.RootSawmill;
// Spawn player entity & attach
NetEntity? old = default;
await Server.WaitPost(() =>
{
// Fuck you mind system I want an hour of my life back
// Mind system is a time vampire
SEntMan.System().WipeMind(ServerSession!.ContentData()?.Mind);
CEntMan.TryGetNetEntity(cPlayerMan.LocalEntity, out old);
SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords));
Player = SEntMan.GetNetEntity(SPlayer);
Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer);
Hands = SEntMan.GetComponentOrNull(SPlayer);
DoAfters = SEntMan.GetComponentOrNull(SPlayer);
});
// Check player got attached.
await RunTicks(5);
CPlayer = ToClient(Player);
Assert.That(cPlayerMan.LocalEntity, Is.EqualTo(CPlayer));
// Delete old player entity.
await Server.WaitPost(() =>
{
if (SEntMan.TryGetEntity(old, out var uid))
SEntMan.DeleteEntity(uid);
});
// Change UI state to in-game.
var state = Client.ResolveDependency();
await Client.WaitPost(() => state.RequestStateChange());
// Final player asserts/checks.
await Pair.ReallyBeIdle(5);
using (Assert.EnterMultipleScope())
{
Assert.That(CEntMan.GetNetEntity(cPlayerMan.LocalEntity), Is.EqualTo(Player));
Assert.That(sPlayerMan.GetSessionById(ClientSession.UserId).AttachedEntity, Is.EqualTo(SEntMan.GetEntity(Player)));
}
}
public override async Task DoTeardown()
{
await Server.WaitPost(() => MapSystem.DeleteMap(MapId));
await TearDown();
await base.DoTeardown();
}
protected virtual Task TearDown()
{
return Task.CompletedTask;
}
}