#nullable enable using System.Numerics; using Content.Client.Construction; using Content.Client.Examine; using Content.Client.Gameplay; using Content.IntegrationTests.Pair; 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.Ranged.Systems; using Robust.Client.Input; using Robust.Client.State; 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.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 EntityUid SPlayer; protected EntityUid CPlayer; protected ICommonSession ClientSession = default!; protected ICommonSession ServerSession = 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 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 HandsSystem 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 ItemToggleSystem ItemToggleSys = default!; protected InteractionTestSystem STestSystem = default!; protected SharedTransformSystem Transform = default!; protected SharedMapSystem MapSystem = default!; protected ISawmill SLogger = default!; protected SharedUserInterfaceSystem SUiSys = default!; protected SharedCombatModeSystem SCombatMode = default!; protected SharedGunSystem SGun = 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!; protected SharedUserInterfaceSystem CUiSys = 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 }; protected virtual PoolSettings Settings => Default; [SetUp] public virtual async Task Setup() { Pair = await PoolManager.GetServerClient(Settings); // server dependencies SEntMan = Server.ResolveDependency(); TileMan = Server.ResolveDependency(); MapMan = Server.ResolveDependency(); ProtoMan = Server.ResolveDependency(); Factory = Server.ResolveDependency(); STiming = Server.ResolveDependency(); SLogger = Server.ResolveDependency().RootSawmill; HandSys = SEntMan.System(); InteractSys = SEntMan.System(); ToolSys = SEntMan.System(); ItemToggleSys = SEntMan.System(); DoAfterSys = SEntMan.System(); Transform = SEntMan.System(); MapSystem = SEntMan.System(); SConstruction = SEntMan.System(); STestSystem = SEntMan.System(); Stack = SEntMan.System(); SUiSys = SEntMan.System(); SCombatMode = SEntMan.System(); SGun = SEntMan.System(); // client dependencies CEntMan = Client.ResolveDependency(); UiMan = Client.ResolveDependency(); CTiming = Client.ResolveDependency(); InputManager = Client.ResolveDependency(); CLogger = Client.ResolveDependency().RootSawmill; InputSystem = CEntMan.System(); CTestSystem = CEntMan.System(); CConSys = CEntMan.System(); ExamineSys = CEntMan.System(); CUiSys = CEntMan.System(); // Setup map. await Pair.CreateTestMap(); 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!; ServerSession = sPlayerMan.GetSessionById(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.LocalEntity; 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 (old != null) SEntMan.DeleteEntity(old.Value); }); // Change UI state to in-game. var state = Client.ResolveDependency(); await Client.WaitPost(() => state.RequestStateChange()); // Final player asserts/checks. await Pair.ReallyBeIdle(5); Assert.Multiple(() => { Assert.That(CEntMan.GetNetEntity(cPlayerMan.LocalEntity), Is.EqualTo(Player)); Assert.That(sPlayerMan.GetSessionById(ClientSession.UserId).AttachedEntity, Is.EqualTo(SEntMan.GetEntity(Player))); }); } [TearDown] public async Task TearDownInternal() { await Server.WaitPost(() => MapSystem.DeleteMap(MapId)); await Pair.CleanReturnAsync(); await TearDown(); } protected virtual Task TearDown() { return Task.CompletedTask; } }