#nullable enable using System.Linq; using System.Numerics; using Content.Client.Construction; using Content.Client.Examine; using Content.IntegrationTests.Pair; using Content.Server.Body.Systems; using Content.Server.Players; 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.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Mind; using Robust.Client.Input; using Robust.Client.UserInterface; using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.GameObjects; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.UnitTesting; 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 { protected virtual string PlayerPrototype => "InteractionTestMob"; protected TestPair Pair = default!; protected TestMapData MapData => Pair.TestMap!; protected RobustIntegrationTest.ServerIntegrationInstance Server => Pair.Server; protected RobustIntegrationTest.ClientIntegrationInstance Client => Pair.Client; 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 ICommonSession ClientSession = default!; protected IPlayerSession ServerSession = default!; public EntityUid? ClientTarget; /// /// 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; /// /// When attempting to start construction, this is the client-side ID of the construction ghost. /// protected int ConstructionGhostId; // SERVER dependencies protected IEntityManager SEntMan = default!; protected ITileDefinitionManager TileMan = default!; protected IMapManager MapMan = default!; protected IPrototypeManager ProtoMan = default!; protected IGameTiming STiming = default!; protected IComponentFactory Factory = default!; protected SharedHandsSystem HandSys = default!; protected StackSystem Stack = default!; protected SharedInteractionSystem InteractSys = default!; protected Content.Server.Construction.ConstructionSystem SConstruction = default!; protected SharedDoAfterSystem DoAfterSys = default!; protected ToolSystem ToolSys = default!; protected InteractionTestSystem STestSystem = default!; protected SharedTransformSystem Transform = default!; protected ActorSystem Actor = default!; protected ISawmill SLogger = default!; // CLIENT dependencies protected IEntityManager CEntMan = default!; protected IGameTiming CTiming = default!; protected IUserInterfaceManager UiMan = default!; protected IInputManager InputManager = default!; protected Robust.Client.GameObjects.InputSystem InputSystem = default!; protected ConstructionSystem CConSys = default!; protected ExamineSystem ExamineSys = default!; protected InteractionTestSystem CTestSystem = default!; protected ISawmill CLogger = default!; // player components protected HandsComponent Hands = default!; protected DoAfterComponent DoAfters = default!; 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: Body prototype: Aghost - type: DoAfter - type: Hands - type: MindContainer - type: Stripping - type: Tag tags: - CanPilot - type: UserInterface "; [SetUp] public virtual async Task Setup() { Pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); // server dependencies SEntMan = Server.ResolveDependency(); TileMan = Server.ResolveDependency(); MapMan = Server.ResolveDependency(); ProtoMan = Server.ResolveDependency(); Factory = Server.ResolveDependency(); STiming = Server.ResolveDependency(); HandSys = SEntMan.System(); InteractSys = SEntMan.System(); ToolSys = SEntMan.System(); DoAfterSys = SEntMan.System(); Transform = SEntMan.System(); SConstruction = SEntMan.System(); STestSystem = SEntMan.System(); Stack = SEntMan.System(); Actor = SEntMan.System(); SLogger = Server.ResolveDependency().RootSawmill; // client dependencies CEntMan = Client.ResolveDependency(); UiMan = Client.ResolveDependency(); CTiming = Client.ResolveDependency(); InputManager = Client.ResolveDependency(); InputSystem = CEntMan.System(); CTestSystem = CEntMan.System(); CConSys = CEntMan.System(); ExamineSys = CEntMan.System(); CLogger = Client.ResolveDependency().RootSawmill; // Setup map. await Pair.CreateTestMap(); PlayerCoords = SEntMan.GetNetCoordinates(MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)).WithEntityId(MapData.MapUid, Transform, SEntMan)); TargetCoords = SEntMan.GetNetCoordinates(MapData.GridCoords.Offset(new Vector2(1.5f, 0.5f)).WithEntityId(MapData.MapUid, Transform, SEntMan)); await SetTile(Plating, grid: MapData.MapGrid); // Get player data var sPlayerMan = Server.ResolveDependency(); var cPlayerMan = Client.ResolveDependency(); if (cPlayerMan.LocalPlayer?.Session == null) Assert.Fail("No player"); ClientSession = cPlayerMan.LocalPlayer!.Session!; ServerSession = sPlayerMan.GetSessionByUserId(ClientSession.UserId); // Spawn player entity & attach EntityUid? 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); old = cPlayerMan.LocalPlayer.ControlledEntity; Player = SEntMan.GetNetEntity(SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords))); var serverPlayerEnt = SEntMan.GetEntity(Player); Actor.Attach(serverPlayerEnt, ServerSession); Hands = SEntMan.GetComponent(serverPlayerEnt); DoAfters = SEntMan.GetComponent(serverPlayerEnt); }); // Check player got attached. await RunTicks(5); Assert.That(CEntMan.GetNetEntity(cPlayerMan.LocalPlayer.ControlledEntity), Is.EqualTo(Player)); // Delete old player entity. await Server.WaitPost(() => { if (old != null) 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(); var hands = bodySystem.GetBodyChildrenOfType(SEntMan.GetEntity(Player), BodyPartType.Hand).ToArray(); for (var i = 1; i < hands.Length; i++) { SEntMan.DeleteEntity(hands[i].Id); } }); // Final player asserts/checks. await Pair.ReallyBeIdle(5); Assert.Multiple(() => { Assert.That(CEntMan.GetNetEntity(cPlayerMan.LocalPlayer.ControlledEntity), Is.EqualTo(Player)); Assert.That(sPlayerMan.GetSessionByUserId(ClientSession.UserId).AttachedEntity, Is.EqualTo(SEntMan.GetEntity(Player))); }); } [TearDown] public async Task TearDownInternal() { await Server.WaitPost(() => MapMan.DeleteMap(MapId)); await Pair.CleanReturnAsync(); await TearDown(); } protected virtual async Task TearDown() { } }