Files
crystall-punk-14/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs

1297 lines
44 KiB
C#
Raw Normal View History

2023-04-15 07:41:25 +12:00
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
2023-04-15 07:41:25 +12:00
using System.Linq;
using System.Numerics;
using System.Reflection;
2023-04-15 07:41:25 +12:00
using Content.Client.Construction;
2023-06-28 21:22:03 +10:00
using Content.Server.Atmos.EntitySystems;
2023-04-15 07:41:25 +12:00
using Content.Server.Construction.Components;
2023-04-17 18:07:03 +12:00
using Content.Server.Gravity;
using Content.Server.Power.Components;
2023-04-17 18:07:03 +12:00
using Content.Shared.Atmos;
2023-04-15 07:41:25 +12:00
using Content.Shared.Construction.Prototypes;
2023-04-17 18:07:03 +12:00
using Content.Shared.Gravity;
2023-04-15 07:41:25 +12:00
using Content.Shared.Item;
using Robust.Client.UserInterface;
2024-06-04 09:05:51 +12:00
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
2023-04-15 07:41:25 +12:00
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
2023-04-15 07:41:25 +12:00
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent;
2023-04-15 07:41:25 +12:00
namespace Content.IntegrationTests.Tests.Interaction;
// This partial class defines various methods that are useful for performing & validating interactions
public abstract partial class InteractionTest
{
/// <summary>
/// Begin constructing an entity.
/// </summary>
protected async Task StartConstruction(string prototype, bool shouldSucceed = true)
{
var proto = ProtoMan.Index<ConstructionPrototype>(prototype);
Assert.That(proto.Type, Is.EqualTo(ConstructionType.Structure));
await Client.WaitPost(() =>
{
Assert.That(CConSys.TrySpawnGhost(proto, CEntMan.GetCoordinates(TargetCoords), Direction.South, out var clientTarget),
2023-04-15 07:41:25 +12:00
Is.EqualTo(shouldSucceed));
if (!shouldSucceed)
return;
var comp = CEntMan.GetComponent<ConstructionGhostComponent>(clientTarget!.Value);
2024-06-04 09:05:51 +12:00
Target = CEntMan.GetNetEntity(clientTarget.Value);
Assert.That(Target.Value.IsClientSide());
ConstructionGhostId = clientTarget.Value.GetHashCode();
2023-04-15 07:41:25 +12:00
});
await RunTicks(1);
}
/// <summary>
/// Craft an item.
/// </summary>
protected async Task CraftItem(string prototype, bool shouldSucceed = true)
{
Assert.That(ProtoMan.Index<ConstructionPrototype>(prototype).Type, Is.EqualTo(ConstructionType.Item));
// Please someone purge async construction code
Task<bool> task = default!;
2025-03-08 14:49:13 +11:00
await Server.WaitPost(() =>
{
task = SConstruction.TryStartItemConstruction(prototype, SEntMan.GetEntity(Player));
});
2023-04-15 07:41:25 +12:00
Task? tickTask = null;
while (!task.IsCompleted)
{
tickTask = Pair.RunTicksSync(1);
2023-04-15 07:41:25 +12:00
await Task.WhenAny(task, tickTask);
}
if (tickTask != null)
await tickTask;
#pragma warning disable RA0004
Assert.That(task.Result, Is.EqualTo(shouldSucceed));
#pragma warning restore RA0004
await RunTicks(5);
}
/// <summary>
/// Spawn an entity entity and set it as the target.
/// </summary>
[MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
protected async Task<NetEntity> SpawnTarget(string prototype)
2023-04-15 07:41:25 +12:00
{
Target = NetEntity.Invalid;
2023-04-15 07:41:25 +12:00
await Server.WaitPost(() =>
{
Target = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
2023-04-15 07:41:25 +12:00
});
await RunTicks(5);
AssertPrototype(prototype);
return Target!.Value;
2023-04-15 07:41:25 +12:00
}
#pragma warning restore CS8774 // Member must have a non-null value when exiting.
2023-04-15 07:41:25 +12:00
/// <summary>
/// Spawn an entity in preparation for deconstruction
/// </summary>
protected async Task StartDeconstruction(string prototype)
{
await SpawnTarget(prototype);
var serverTarget = SEntMan.GetEntity(Target);
Assert.That(SEntMan.TryGetComponent(serverTarget, out ConstructionComponent? comp));
await Server.WaitPost(() => SConstruction.SetPathfindingTarget(serverTarget!.Value, comp!.DeconstructionNode, comp));
2023-04-15 07:41:25 +12:00
await RunTicks(5);
}
/// <summary>
/// Drops and deletes the currently held entity.
/// </summary>
protected async Task DeleteHeldEntity()
{
if (HandSys.GetActiveItem((ToServer(Player), Hands)) is { } held)
2023-04-15 07:41:25 +12:00
{
await Server.WaitPost(() =>
{
Assert.That(HandSys.TryDrop((SEntMan.GetEntity(Player), Hands), null, false, true));
2023-04-15 07:41:25 +12:00
SEntMan.DeleteEntity(held);
SLogger.Debug($"Deleting held entity");
2023-04-15 07:41:25 +12:00
});
}
await RunTicks(1);
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.Null);
2023-04-15 07:41:25 +12:00
}
/// <summary>
/// Place an entity prototype into the players hand. Deletes any currently held entity.
/// </summary>
2024-06-04 09:05:51 +12:00
/// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
/// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
/// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
protected async Task<NetEntity> PlaceInHands(string id, int quantity = 1, bool enableToggleable = true)
{
2024-06-04 09:05:51 +12:00
return await PlaceInHands((id, quantity), enableToggleable);
}
2023-04-15 07:41:25 +12:00
/// <summary>
/// Place an entity prototype into the players hand. Deletes any currently held entity.
/// </summary>
2024-06-04 09:05:51 +12:00
/// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
/// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableToggleable = true)
2023-04-15 07:41:25 +12:00
{
if (Hands.ActiveHandId == null)
2023-04-15 07:41:25 +12:00
{
Assert.Fail("No active hand");
return default;
}
Assert.That(!string.IsNullOrWhiteSpace(entity.Prototype));
2023-04-15 07:41:25 +12:00
await DeleteHeldEntity();
// spawn and pick up the new item
var item = await SpawnEntity(entity, SEntMan.GetCoordinates(PlayerCoords));
ItemToggle system expansion (#22369) * Fixed EnergySword and variants having incorrect sound on attacking when in their Off state. * Removed the unused ItemToggle from the serverside and created a new shared ItemToggleComponent and System, now used for the e-blade family of items. Also added e-blade hum and swing sounds. Thanks Sloth for the initial code! * Changing Stunbaton system to include the itemToggle system. * Adapted changes that have come up in the meantime. * Changed damagespecifier to be serializable and autoNetworked in melee weapon components. Fixes a bug that makes it so client-side, damage values are not updated on toggle. * Made the ItemToggleSystem have both a shared and a server component. Ported the Stun Baton and Stun Prod to the new toggleable system. Added a failure to activate noise component. * Ported the welders to the new item toggle system. Set it so deactivated damage and item size default to the item's regular options. * Removed unnecessary usings. * Small modification to the stun prod. * Made the integration test use the new method to turn the welders on. * Fixed a few testing issues, applied a few changes requested by Delta. * Updated Stunbaton code for consistentcy when it comes to calling the itemToggle component. * Removed a redundant return; as per Delta. Made examining the stun baton for charge rely on the battery component instead. * Removed the welder visualizer system, now using the generic one. Removed some unused usings. Removed the welder visuals and layers. Ported lighters to the new system. Added zippi (sic) lighters. * Renamed variables used to make them less generic. * Simplified the light update code. * Fixed the unit test to use the itemToggle system for welders now. * Made the name shorter. I can't tell if the welding damage when interacted with actually does anything though. I can't figure out how to trigger it. * Fixed some YML issues. * Added a client side item toggle system just to make the shared code run on local UID's too. * Fixed some more Yaml. * Made the Zippi lighter have its own parent item, so it doesnt' conflict with the random pattern on the regular lighter. * Made the zippi lighter its own in-hand sprites. * Added a summary for the activated property in itemtoggle component. * Fixed a typo in the itemToggle Component. * Fixed a typo. * Added to the remarks for the ItemToggleComponent. * Fixed up the lighter yaml to make it use a generic term instead of a toggle layer enum for the random skin. * Fixed a bug I introduced accidentally with the humming sound. * Removed 2 unnecessary events from the ItemToggleSystem and component. * Fixed a bug by only making the server run the item activation code, since the client cannot predict whether or not the activation will be cancelled. * Cleaned up some names and functions getting called. * Renamed a couple of variables and removed the explicit datafields from the component. Removed "activated: false" from yml since they're already deactivated by default. * Added an IsActivated function, used it in the welder and stun baton systems code. Refactored welder code to remove the WelderToggle event, now using the ItemToggleActivatedEvent instead for eye protection check. * Fixed a typo. Added some comments. * Split the ItemToggle into smaller components. Changed the items that used the toggle system to work with the smaller components. Made the mirror shield reflect energy shots with a 95% chance. * Fixed the namespaces for the server components and whatnot. * Fixed a doubled deactivation sound from using activated wieldable items (like the double Esword). Fixed wrong yml with the e-dagger. Fixed the disarm malus code. * Added the zippo lighter to the detective's trench coat. * Removed the default hit sound for the double e-sword since it was unnecessary. * Changed e-sword damage numbers to be in line with the changes made by Emisse. * Made no damage sounds be autoNetworked, so it changes can be changed on activation/deactivation of items. Made Welders and Eswords sound like themselves but quieter if they hit for 0 damage, instead of taps. You can choose what sound to play when a weapon does 0 damage when activated now. Fixed a bug with swing sounds. * Typo. * Fixed a bug where the welder would blind you if you used it while it was off. * Created a single abstract method called when an item has completed its toggle. * Update Content.Server/Eye/Blinding/EyeProtection/EyeProtectionSystem.cs Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Fixed a comment. * Made most component variables readOnly for ItemToggle. There is no need to be able to change them from within the variable viewer. * Removed trailing white spaces. * Made the Use a field instead of a property in the itemToggleActivation/Deactivation attempt events. * Small fixes. * Removed ForceToggle, just use the toggle method instead. * Fixed a bug with item sharpness staying even after getting deactivated, if the item gained sharpness that way (esword). * Used ProtoId in the welder component. * Made damage NetSerializable as well. * Added networking and data fields to a couple of components. * Made component variables autonetworked. Added some comments. * Moved the events that modify item components on toggle to events, handled (where possible) in the systems linked to said components. * Made all the component variables readWrite again. * Added the component get to the WelderStatus. * Added a predictable bool to the item toggle component. * Replaced the Activated/Deactivated events with ToggleDone, with an Activated argument. Used that to simplify some systems. * Added a reflect update raise event. * Removed the Zippo changes. To add in a later PR. * Removed the zippo from meta.json too. * Small fix. * Another small fix. * Fixed the wieldable system thing in ItemToggle. --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2023-12-24 08:11:05 +02:00
ItemToggleComponent? itemToggle = null;
2023-04-15 07:41:25 +12:00
await Server.WaitPost(() =>
{
var playerEnt = SEntMan.GetEntity(Player);
Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHandId, false, false, false, Hands));
2023-04-15 07:41:25 +12:00
// turn on welders
2024-06-04 09:05:51 +12:00
if (enableToggleable && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
ItemToggle system expansion (#22369) * Fixed EnergySword and variants having incorrect sound on attacking when in their Off state. * Removed the unused ItemToggle from the serverside and created a new shared ItemToggleComponent and System, now used for the e-blade family of items. Also added e-blade hum and swing sounds. Thanks Sloth for the initial code! * Changing Stunbaton system to include the itemToggle system. * Adapted changes that have come up in the meantime. * Changed damagespecifier to be serializable and autoNetworked in melee weapon components. Fixes a bug that makes it so client-side, damage values are not updated on toggle. * Made the ItemToggleSystem have both a shared and a server component. Ported the Stun Baton and Stun Prod to the new toggleable system. Added a failure to activate noise component. * Ported the welders to the new item toggle system. Set it so deactivated damage and item size default to the item's regular options. * Removed unnecessary usings. * Small modification to the stun prod. * Made the integration test use the new method to turn the welders on. * Fixed a few testing issues, applied a few changes requested by Delta. * Updated Stunbaton code for consistentcy when it comes to calling the itemToggle component. * Removed a redundant return; as per Delta. Made examining the stun baton for charge rely on the battery component instead. * Removed the welder visualizer system, now using the generic one. Removed some unused usings. Removed the welder visuals and layers. Ported lighters to the new system. Added zippi (sic) lighters. * Renamed variables used to make them less generic. * Simplified the light update code. * Fixed the unit test to use the itemToggle system for welders now. * Made the name shorter. I can't tell if the welding damage when interacted with actually does anything though. I can't figure out how to trigger it. * Fixed some YML issues. * Added a client side item toggle system just to make the shared code run on local UID's too. * Fixed some more Yaml. * Made the Zippi lighter have its own parent item, so it doesnt' conflict with the random pattern on the regular lighter. * Made the zippi lighter its own in-hand sprites. * Added a summary for the activated property in itemtoggle component. * Fixed a typo in the itemToggle Component. * Fixed a typo. * Added to the remarks for the ItemToggleComponent. * Fixed up the lighter yaml to make it use a generic term instead of a toggle layer enum for the random skin. * Fixed a bug I introduced accidentally with the humming sound. * Removed 2 unnecessary events from the ItemToggleSystem and component. * Fixed a bug by only making the server run the item activation code, since the client cannot predict whether or not the activation will be cancelled. * Cleaned up some names and functions getting called. * Renamed a couple of variables and removed the explicit datafields from the component. Removed "activated: false" from yml since they're already deactivated by default. * Added an IsActivated function, used it in the welder and stun baton systems code. Refactored welder code to remove the WelderToggle event, now using the ItemToggleActivatedEvent instead for eye protection check. * Fixed a typo. Added some comments. * Split the ItemToggle into smaller components. Changed the items that used the toggle system to work with the smaller components. Made the mirror shield reflect energy shots with a 95% chance. * Fixed the namespaces for the server components and whatnot. * Fixed a doubled deactivation sound from using activated wieldable items (like the double Esword). Fixed wrong yml with the e-dagger. Fixed the disarm malus code. * Added the zippo lighter to the detective's trench coat. * Removed the default hit sound for the double e-sword since it was unnecessary. * Changed e-sword damage numbers to be in line with the changes made by Emisse. * Made no damage sounds be autoNetworked, so it changes can be changed on activation/deactivation of items. Made Welders and Eswords sound like themselves but quieter if they hit for 0 damage, instead of taps. You can choose what sound to play when a weapon does 0 damage when activated now. Fixed a bug with swing sounds. * Typo. * Fixed a bug where the welder would blind you if you used it while it was off. * Created a single abstract method called when an item has completed its toggle. * Update Content.Server/Eye/Blinding/EyeProtection/EyeProtectionSystem.cs Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Fixed a comment. * Made most component variables readOnly for ItemToggle. There is no need to be able to change them from within the variable viewer. * Removed trailing white spaces. * Made the Use a field instead of a property in the itemToggleActivation/Deactivation attempt events. * Small fixes. * Removed ForceToggle, just use the toggle method instead. * Fixed a bug with item sharpness staying even after getting deactivated, if the item gained sharpness that way (esword). * Used ProtoId in the welder component. * Made damage NetSerializable as well. * Added networking and data fields to a couple of components. * Made component variables autonetworked. Added some comments. * Moved the events that modify item components on toggle to events, handled (where possible) in the systems linked to said components. * Made all the component variables readWrite again. * Added the component get to the WelderStatus. * Added a predictable bool to the item toggle component. * Replaced the Activated/Deactivated events with ToggleDone, with an Activated argument. Used that to simplify some systems. * Added a reflect update raise event. * Removed the Zippo changes. To add in a later PR. * Removed the zippo from meta.json too. * Small fix. * Another small fix. * Fixed the wieldable system thing in ItemToggle. --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2023-12-24 08:11:05 +02:00
{
item toggling giga rework + full ninja refactor (#28039) * item toggle refactoring and some new systems * add ToggleClothing component/system * unhardcode magboots gravity logic * make magboots and speedboots use ItemToggle and stuff * remove now useless clothing components * update client/server magboots systems * add note to use ItemToggledEvent in ToggleActionEvent doc * refactor PowerCellDraw to use ItemToggle for ui open/close control * add TryUseCharges, refactor charges system * update magboot trigger code * make borg use ItemToggle, network SelectedModule instead of now removed Activated * add AccessToggle for borg * the giga ninja refactor * update ninja yml * update ItemToggle usage for some stuff * fix activatableui requires power * random fixing * yaml fixing * nuke ItemToggleDisarmMalus * make defib use ItemToggle * make things that use power not turn on if missing use charge * pro * fix sound prediction * bruh * proximity detector use ItemToggle * oop * big idiot syndrome * fix ninja spawn rule and make it generic * fix ninja spawn rule yml * move loading profiles into AntagLoadProfileRule * more ninja refactor * ninja yml fixes * the dreaded copy paste ops * remove useless NinjaRuleComponent and ue AntagSelection for greeting * fix invisibility * move IsCompleted to SharedObjectivesSystem * ability fixes * oop fix powercell instantly draining itself * sentient speedboots gaming * make reflect use ItemToggle * fix other test * loadprofilerule moved into its own pr * remove conflict with dragon refactor * remove all GenericAntag code from ninja * ) * probably * remove old enabled * great language bravo vince * GREAT LANGUAGE * who made this language * because it stinks * reparent blood-red magboots to magboots probbbly works * most of the review stuff * hasGrav doesnt mean what i thought it did * make health analyzer use itemtoggle, not fail test * fix mag/speed boots being wacky * UNTROLL * add ItemToggle to the random health analyzers * a * remove unused obsolete borg func * untrolling * :trollface: * fix test * fix * g * untroll --------- Co-authored-by: deltanedas <@deltanedas:kde.org>
2024-07-11 05:55:56 +00:00
Assert.That(ItemToggleSys.TryActivate((item, itemToggle), user: playerEnt));
ItemToggle system expansion (#22369) * Fixed EnergySword and variants having incorrect sound on attacking when in their Off state. * Removed the unused ItemToggle from the serverside and created a new shared ItemToggleComponent and System, now used for the e-blade family of items. Also added e-blade hum and swing sounds. Thanks Sloth for the initial code! * Changing Stunbaton system to include the itemToggle system. * Adapted changes that have come up in the meantime. * Changed damagespecifier to be serializable and autoNetworked in melee weapon components. Fixes a bug that makes it so client-side, damage values are not updated on toggle. * Made the ItemToggleSystem have both a shared and a server component. Ported the Stun Baton and Stun Prod to the new toggleable system. Added a failure to activate noise component. * Ported the welders to the new item toggle system. Set it so deactivated damage and item size default to the item's regular options. * Removed unnecessary usings. * Small modification to the stun prod. * Made the integration test use the new method to turn the welders on. * Fixed a few testing issues, applied a few changes requested by Delta. * Updated Stunbaton code for consistentcy when it comes to calling the itemToggle component. * Removed a redundant return; as per Delta. Made examining the stun baton for charge rely on the battery component instead. * Removed the welder visualizer system, now using the generic one. Removed some unused usings. Removed the welder visuals and layers. Ported lighters to the new system. Added zippi (sic) lighters. * Renamed variables used to make them less generic. * Simplified the light update code. * Fixed the unit test to use the itemToggle system for welders now. * Made the name shorter. I can't tell if the welding damage when interacted with actually does anything though. I can't figure out how to trigger it. * Fixed some YML issues. * Added a client side item toggle system just to make the shared code run on local UID's too. * Fixed some more Yaml. * Made the Zippi lighter have its own parent item, so it doesnt' conflict with the random pattern on the regular lighter. * Made the zippi lighter its own in-hand sprites. * Added a summary for the activated property in itemtoggle component. * Fixed a typo in the itemToggle Component. * Fixed a typo. * Added to the remarks for the ItemToggleComponent. * Fixed up the lighter yaml to make it use a generic term instead of a toggle layer enum for the random skin. * Fixed a bug I introduced accidentally with the humming sound. * Removed 2 unnecessary events from the ItemToggleSystem and component. * Fixed a bug by only making the server run the item activation code, since the client cannot predict whether or not the activation will be cancelled. * Cleaned up some names and functions getting called. * Renamed a couple of variables and removed the explicit datafields from the component. Removed "activated: false" from yml since they're already deactivated by default. * Added an IsActivated function, used it in the welder and stun baton systems code. Refactored welder code to remove the WelderToggle event, now using the ItemToggleActivatedEvent instead for eye protection check. * Fixed a typo. Added some comments. * Split the ItemToggle into smaller components. Changed the items that used the toggle system to work with the smaller components. Made the mirror shield reflect energy shots with a 95% chance. * Fixed the namespaces for the server components and whatnot. * Fixed a doubled deactivation sound from using activated wieldable items (like the double Esword). Fixed wrong yml with the e-dagger. Fixed the disarm malus code. * Added the zippo lighter to the detective's trench coat. * Removed the default hit sound for the double e-sword since it was unnecessary. * Changed e-sword damage numbers to be in line with the changes made by Emisse. * Made no damage sounds be autoNetworked, so it changes can be changed on activation/deactivation of items. Made Welders and Eswords sound like themselves but quieter if they hit for 0 damage, instead of taps. You can choose what sound to play when a weapon does 0 damage when activated now. Fixed a bug with swing sounds. * Typo. * Fixed a bug where the welder would blind you if you used it while it was off. * Created a single abstract method called when an item has completed its toggle. * Update Content.Server/Eye/Blinding/EyeProtection/EyeProtectionSystem.cs Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Fixed a comment. * Made most component variables readOnly for ItemToggle. There is no need to be able to change them from within the variable viewer. * Removed trailing white spaces. * Made the Use a field instead of a property in the itemToggleActivation/Deactivation attempt events. * Small fixes. * Removed ForceToggle, just use the toggle method instead. * Fixed a bug with item sharpness staying even after getting deactivated, if the item gained sharpness that way (esword). * Used ProtoId in the welder component. * Made damage NetSerializable as well. * Added networking and data fields to a couple of components. * Made component variables autonetworked. Added some comments. * Moved the events that modify item components on toggle to events, handled (where possible) in the systems linked to said components. * Made all the component variables readWrite again. * Added the component get to the WelderStatus. * Added a predictable bool to the item toggle component. * Replaced the Activated/Deactivated events with ToggleDone, with an Activated argument. Used that to simplify some systems. * Added a reflect update raise event. * Removed the Zippo changes. To add in a later PR. * Removed the zippo from meta.json too. * Small fix. * Another small fix. * Fixed the wieldable system thing in ItemToggle. --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2023-12-24 08:11:05 +02:00
}
2023-04-15 07:41:25 +12:00
});
await RunTicks(1);
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.EqualTo(item));
2024-06-04 09:05:51 +12:00
if (enableToggleable && itemToggle != null)
ItemToggle system expansion (#22369) * Fixed EnergySword and variants having incorrect sound on attacking when in their Off state. * Removed the unused ItemToggle from the serverside and created a new shared ItemToggleComponent and System, now used for the e-blade family of items. Also added e-blade hum and swing sounds. Thanks Sloth for the initial code! * Changing Stunbaton system to include the itemToggle system. * Adapted changes that have come up in the meantime. * Changed damagespecifier to be serializable and autoNetworked in melee weapon components. Fixes a bug that makes it so client-side, damage values are not updated on toggle. * Made the ItemToggleSystem have both a shared and a server component. Ported the Stun Baton and Stun Prod to the new toggleable system. Added a failure to activate noise component. * Ported the welders to the new item toggle system. Set it so deactivated damage and item size default to the item's regular options. * Removed unnecessary usings. * Small modification to the stun prod. * Made the integration test use the new method to turn the welders on. * Fixed a few testing issues, applied a few changes requested by Delta. * Updated Stunbaton code for consistentcy when it comes to calling the itemToggle component. * Removed a redundant return; as per Delta. Made examining the stun baton for charge rely on the battery component instead. * Removed the welder visualizer system, now using the generic one. Removed some unused usings. Removed the welder visuals and layers. Ported lighters to the new system. Added zippi (sic) lighters. * Renamed variables used to make them less generic. * Simplified the light update code. * Fixed the unit test to use the itemToggle system for welders now. * Made the name shorter. I can't tell if the welding damage when interacted with actually does anything though. I can't figure out how to trigger it. * Fixed some YML issues. * Added a client side item toggle system just to make the shared code run on local UID's too. * Fixed some more Yaml. * Made the Zippi lighter have its own parent item, so it doesnt' conflict with the random pattern on the regular lighter. * Made the zippi lighter its own in-hand sprites. * Added a summary for the activated property in itemtoggle component. * Fixed a typo in the itemToggle Component. * Fixed a typo. * Added to the remarks for the ItemToggleComponent. * Fixed up the lighter yaml to make it use a generic term instead of a toggle layer enum for the random skin. * Fixed a bug I introduced accidentally with the humming sound. * Removed 2 unnecessary events from the ItemToggleSystem and component. * Fixed a bug by only making the server run the item activation code, since the client cannot predict whether or not the activation will be cancelled. * Cleaned up some names and functions getting called. * Renamed a couple of variables and removed the explicit datafields from the component. Removed "activated: false" from yml since they're already deactivated by default. * Added an IsActivated function, used it in the welder and stun baton systems code. Refactored welder code to remove the WelderToggle event, now using the ItemToggleActivatedEvent instead for eye protection check. * Fixed a typo. Added some comments. * Split the ItemToggle into smaller components. Changed the items that used the toggle system to work with the smaller components. Made the mirror shield reflect energy shots with a 95% chance. * Fixed the namespaces for the server components and whatnot. * Fixed a doubled deactivation sound from using activated wieldable items (like the double Esword). Fixed wrong yml with the e-dagger. Fixed the disarm malus code. * Added the zippo lighter to the detective's trench coat. * Removed the default hit sound for the double e-sword since it was unnecessary. * Changed e-sword damage numbers to be in line with the changes made by Emisse. * Made no damage sounds be autoNetworked, so it changes can be changed on activation/deactivation of items. Made Welders and Eswords sound like themselves but quieter if they hit for 0 damage, instead of taps. You can choose what sound to play when a weapon does 0 damage when activated now. Fixed a bug with swing sounds. * Typo. * Fixed a bug where the welder would blind you if you used it while it was off. * Created a single abstract method called when an item has completed its toggle. * Update Content.Server/Eye/Blinding/EyeProtection/EyeProtectionSystem.cs Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Fixed a comment. * Made most component variables readOnly for ItemToggle. There is no need to be able to change them from within the variable viewer. * Removed trailing white spaces. * Made the Use a field instead of a property in the itemToggleActivation/Deactivation attempt events. * Small fixes. * Removed ForceToggle, just use the toggle method instead. * Fixed a bug with item sharpness staying even after getting deactivated, if the item gained sharpness that way (esword). * Used ProtoId in the welder component. * Made damage NetSerializable as well. * Added networking and data fields to a couple of components. * Made component variables autonetworked. Added some comments. * Moved the events that modify item components on toggle to events, handled (where possible) in the systems linked to said components. * Made all the component variables readWrite again. * Added the component get to the WelderStatus. * Added a predictable bool to the item toggle component. * Replaced the Activated/Deactivated events with ToggleDone, with an Activated argument. Used that to simplify some systems. * Added a reflect update raise event. * Removed the Zippo changes. To add in a later PR. * Removed the zippo from meta.json too. * Small fix. * Another small fix. * Fixed the wieldable system thing in ItemToggle. --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2023-12-24 08:11:05 +02:00
Assert.That(itemToggle.Activated);
2023-04-15 07:41:25 +12:00
return SEntMan.GetNetEntity(item);
2023-04-15 07:41:25 +12:00
}
/// <summary>
/// Pick up an entity. Defaults to just deleting the previously held entity.
/// </summary>
protected async Task Pickup(NetEntity? entity = null, bool deleteHeld = true)
2023-04-15 07:41:25 +12:00
{
entity ??= Target;
2023-04-15 07:41:25 +12:00
if (Hands.ActiveHandId == null)
2023-04-15 07:41:25 +12:00
{
Assert.Fail("No active hand");
return;
}
if (deleteHeld)
await DeleteHeldEntity();
var uid = SEntMan.GetEntity(entity);
2023-04-15 07:41:25 +12:00
if (!SEntMan.TryGetComponent(uid, out ItemComponent? item))
{
Assert.Fail($"Entity {entity} is not an item");
2023-04-15 07:41:25 +12:00
return;
}
await Server.WaitPost(() =>
{
Assert.That(HandSys.TryPickup(ToServer(Player), uid.Value, Hands.ActiveHandId, false, false, false, Hands, item));
2023-04-15 07:41:25 +12:00
});
await RunTicks(1);
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.EqualTo(uid));
2023-04-15 07:41:25 +12:00
}
/// <summary>
/// Drops the currently held entity.
/// </summary>
protected async Task Drop()
{
if (HandSys.GetActiveItem((ToServer(Player), Hands)) == null)
2023-04-15 07:41:25 +12:00
{
Assert.Fail("Not holding any entity to drop");
return;
}
await Server.WaitPost(() =>
{
Assert.That(HandSys.TryDrop((ToServer(Player), Hands)));
2023-04-15 07:41:25 +12:00
});
await RunTicks(1);
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.Null);
2023-04-15 07:41:25 +12:00
}
#region Interact
2023-04-15 07:41:25 +12:00
/// <summary>
/// Use the currently held entity.
/// </summary>
protected async Task UseInHand()
{
if (HandSys.GetActiveItem((ToServer(Player), Hands)) is not { } target)
2023-04-15 07:41:25 +12:00
{
Assert.Fail("Not holding any entity");
return;
}
await Server.WaitPost(() =>
{
InteractSys.UserInteraction(SEntMan.GetEntity(Player), SEntMan.GetComponent<TransformComponent>(target).Coordinates, target);
2023-04-15 07:41:25 +12:00
});
}
/// <summary>
/// Place an entity prototype into the players hand and interact with the given entity (or target position)
/// </summary>
2024-06-04 09:05:51 +12:00
/// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
/// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
/// <param name="altInteract">If true, perform an alternate interaction instead of a standard one.
protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true, bool altInteract = false)
{
await InteractUsing((id, quantity), awaitDoAfters, altInteract);
}
2023-04-15 07:41:25 +12:00
/// <summary>
2024-06-04 09:05:51 +12:00
/// Place an entity prototype into the players hand and interact with the given entity (or target position).
2023-04-15 07:41:25 +12:00
/// </summary>
2024-06-04 09:05:51 +12:00
/// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
/// <param name="altInteract">If true, perform an alternate interaction instead of a standard one.
protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true, bool altInteract = false)
2023-04-15 07:41:25 +12:00
{
// For every interaction, we will also examine the entity, just in case this breaks something, somehow.
// (e.g., servers attempt to assemble construction examine hints).
if (Target != null)
{
await Client.WaitPost(() => ExamineSys.DoExamine(CEntMan.GetEntity(Target.Value)));
2023-04-15 07:41:25 +12:00
}
await PlaceInHands(entity);
await Interact(awaitDoAfters, altInteract);
}
2023-04-15 07:41:25 +12:00
/// <summary>
/// Interact with an entity using the currently held entity.
/// </summary>
2024-06-04 09:05:51 +12:00
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
/// <param name="altInteract">If true, performs an alternate interaction instead of a standard one.
protected async Task Interact(bool awaitDoAfters = true, bool altInteract = false)
{
2024-06-04 09:05:51 +12:00
if (Target == null || !Target.Value.IsClientSide())
2023-04-15 07:41:25 +12:00
{
await Interact(Target, TargetCoords, awaitDoAfters, altInteract);
2024-06-04 09:05:51 +12:00
return;
2023-04-15 07:41:25 +12:00
}
2024-06-04 09:05:51 +12:00
// The target is a client-side entity, so we will just attempt to start construction under the assumption that
// it is a construction ghost.
await Client.WaitPost(() => CConSys.TryStartConstruction(CTarget!.Value));
await RunTicks(5);
if (awaitDoAfters)
await AwaitDoAfters();
await CheckTargetChange();
}
/// <inheritdoc cref="Interact(EntityUid?,EntityCoordinates,bool,bool)"/>
protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true, bool altInteract = false)
2024-06-04 09:05:51 +12:00
{
Assert.That(SEntMan.TryGetEntity(target, out var sTarget) || target == null);
var coords = SEntMan.GetCoordinates(coordinates);
Assert.That(coords.IsValid(SEntMan));
await Interact(sTarget, coords, awaitDoAfters, altInteract);
2024-06-04 09:05:51 +12:00
}
/// <summary>
/// Interact with an entity using the currently held entity.
/// </summary>
protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true, bool altInteract = false)
2024-06-04 09:05:51 +12:00
{
Assert.That(SEntMan.TryGetEntity(Player, out var player));
await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target, altInteract: altInteract));
2024-06-04 09:05:51 +12:00
await RunTicks(1);
if (awaitDoAfters)
await AwaitDoAfters();
await CheckTargetChange();
}
/// <summary>
/// Activate an entity.
/// </summary>
protected async Task Activate(NetEntity? target = null, bool awaitDoAfters = true)
{
target ??= Target;
Assert.That(target, Is.Not.Null);
Assert.That(SEntMan.TryGetEntity(target!.Value, out var sTarget));
Assert.That(SEntMan.TryGetEntity(Player, out var player));
await Server.WaitPost(() => InteractSys.InteractionActivate(player!.Value, sTarget!.Value));
await RunTicks(1);
2023-04-15 07:41:25 +12:00
if (awaitDoAfters)
2024-06-04 09:05:51 +12:00
await AwaitDoAfters();
2023-04-15 07:41:25 +12:00
2024-06-04 09:05:51 +12:00
await CheckTargetChange();
2023-04-15 07:41:25 +12:00
}
/// <summary>
2024-06-04 09:05:51 +12:00
/// Variant of <see cref="InteractUsing(string,int,bool)"/> that performs several interactions using different entities.
/// Useful for quickly finishing multiple construction steps.
/// </summary>
/// <remarks>
/// Empty strings imply empty hands.
/// </remarks>
protected async Task Interact(params EntitySpecifier[] specifiers)
{
foreach (var spec in specifiers)
{
2024-06-04 09:05:51 +12:00
await InteractUsing(spec);
}
}
/// <summary>
/// Throw the currently held entity. Defaults to targeting the current <see cref="TargetCoords"/>
/// </summary>
protected async Task<bool> ThrowItem(NetCoordinates? target = null, float minDistance = 4)
{
var actualTarget = SEntMan.GetCoordinates(target ?? TargetCoords);
var result = false;
await Server.WaitPost(() => result = HandSys.ThrowHeldItem(SEntMan.GetEntity(Player), actualTarget, minDistance));
return result;
}
#endregion
2023-04-15 07:41:25 +12:00
/// <summary>
/// Wait for any currently active DoAfters to finish.
/// </summary>
2024-06-04 09:05:51 +12:00
protected async Task AwaitDoAfters(int maxExpected = 1)
2023-04-15 07:41:25 +12:00
{
if (!ActiveDoAfters.Any())
return;
// Generally expect interactions to only start one DoAfter.
Assert.That(ActiveDoAfters.Count(), Is.LessThanOrEqualTo(maxExpected));
// wait out the DoAfters.
var doAfters = ActiveDoAfters.ToList();
while (ActiveDoAfters.Any())
{
await RunTicks(10);
}
foreach (var doAfter in doAfters)
{
Assert.That(!doAfter.Cancelled);
}
2024-06-04 09:05:51 +12:00
await RunTicks(5);
2023-04-15 07:41:25 +12:00
}
/// <summary>
/// Cancel any currently active DoAfters. Default arguments are such that it also checks that there is at least one
/// active DoAfter to cancel.
/// </summary>
protected async Task CancelDoAfters(int minExpected = 1, int maxExpected = 1)
{
Assert.That(ActiveDoAfters.Count(), Is.GreaterThanOrEqualTo(minExpected));
Assert.That(ActiveDoAfters.Count(), Is.LessThanOrEqualTo(maxExpected));
if (!ActiveDoAfters.Any())
return;
// Cancel all the do-afters
var doAfters = ActiveDoAfters.ToList();
await Server.WaitPost(() =>
{
foreach (var doAfter in doAfters)
{
DoAfterSys.Cancel(SEntMan.GetEntity(Player), doAfter.Index, DoAfters);
2023-04-15 07:41:25 +12:00
}
});
await RunTicks(1);
foreach (var doAfter in doAfters)
{
Assert.That(doAfter.Cancelled);
}
Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
}
/// <summary>
/// Check if the test's target entity has changed. E.g., construction interactions will swap out entities while
/// a structure is being built.
/// </summary>
2024-06-04 09:05:51 +12:00
protected async Task CheckTargetChange()
2023-04-15 07:41:25 +12:00
{
if (Target == null)
return;
2024-06-04 09:05:51 +12:00
var originalTarget = Target.Value;
2023-04-15 07:41:25 +12:00
await RunTicks(5);
2024-06-04 09:05:51 +12:00
if (Target.Value.IsClientSide() && CTestSystem.Ghosts.TryGetValue(ConstructionGhostId, out var newWeh))
2023-04-15 07:41:25 +12:00
{
2024-06-04 09:05:51 +12:00
CLogger.Debug($"Construction ghost {ConstructionGhostId} became entity {newWeh}");
Target = newWeh;
2023-04-15 07:41:25 +12:00
}
if (STestSystem.EntChanges.TryGetValue(Target.Value, out var newServerWeh))
2023-04-15 07:41:25 +12:00
{
2024-06-04 09:05:51 +12:00
SLogger.Debug($"Construction entity {Target.Value} changed to {newServerWeh}");
Target = newServerWeh;
2023-04-15 07:41:25 +12:00
}
2024-06-04 09:05:51 +12:00
if (Target != originalTarget)
await CheckTargetChange();
2023-04-15 07:41:25 +12:00
}
#region Asserts
protected void ClientAssertPrototype(string? prototype, NetEntity? target = null)
2023-04-15 07:41:25 +12:00
{
target ??= Target;
if (target == null)
{
Assert.Fail("No target specified");
return;
}
2024-06-04 09:05:51 +12:00
var meta = CEntMan.GetComponent<MetaDataComponent>(CEntMan.GetEntity(target.Value));
2023-04-15 07:41:25 +12:00
Assert.That(meta.EntityPrototype?.ID, Is.EqualTo(prototype));
}
protected void AssertPrototype(string? prototype, NetEntity? target = null)
2023-04-15 07:41:25 +12:00
{
target ??= Target;
if (target == null)
{
Assert.Fail("No target specified");
return;
}
var meta = SEntMan.GetComponent<MetaDataComponent>(SEntMan.GetEntity(target.Value));
Assert.That(meta.EntityPrototype?.ID, Is.EqualTo(prototype));
}
protected void AssertAnchored(bool anchored = true, NetEntity? target = null)
{
target ??= Target;
if (target == null)
{
Assert.Fail("No target specified");
return;
}
var sXform = SEntMan.GetComponent<TransformComponent>(SEntMan.GetEntity(target.Value));
var cXform = CEntMan.GetComponent<TransformComponent>(CEntMan.GetEntity(target.Value));
Assert.Multiple(() =>
{
Assert.That(sXform.Anchored, Is.EqualTo(anchored));
Assert.That(cXform.Anchored, Is.EqualTo(anchored));
});
2023-04-15 07:41:25 +12:00
}
protected void AssertDeleted(NetEntity? target = null)
2023-04-15 07:41:25 +12:00
{
target ??= Target;
if (target == null)
{
Assert.Fail("No target specified");
return;
}
Assert.Multiple(() =>
{
Assert.That(SEntMan.Deleted(SEntMan.GetEntity(target)));
Assert.That(CEntMan.Deleted(CEntMan.GetEntity(target)));
});
}
protected void AssertExists(NetEntity? target = null)
{
target ??= Target;
if (target == null)
{
Assert.Fail("No target specified");
return;
}
Assert.Multiple(() =>
{
Assert.That(SEntMan.EntityExists(SEntMan.GetEntity(target)));
Assert.That(CEntMan.EntityExists(CEntMan.GetEntity(target)));
});
2023-04-15 07:41:25 +12:00
}
/// <summary>
/// Assert whether or not the target has the given component.
/// </summary>
protected void AssertComp<T>(bool hasComp = true, NetEntity? target = null) where T : IComponent
2023-04-15 07:41:25 +12:00
{
target ??= Target;
if (target == null)
{
Assert.Fail("No target specified");
return;
}
Assert.That(SEntMan.HasComponent<T>(SEntMan.GetEntity(target)), Is.EqualTo(hasComp));
2023-04-15 07:41:25 +12:00
}
/// <summary>
/// Check that the tile at the target position matches some prototype.
/// </summary>
protected async Task AssertTile(string? proto, NetCoordinates? coords = null)
2023-04-15 07:41:25 +12:00
{
var targetTile = proto == null
? Tile.Empty
: new Tile(TileMan[proto].TileId);
var tile = Tile.Empty;
var serverCoords = SEntMan.GetCoordinates(coords ?? TargetCoords);
var pos = Transform.ToMapCoordinates(serverCoords);
2023-04-15 07:41:25 +12:00
await Server.WaitPost(() =>
{
if (MapMan.TryFindGridAt(pos, out var gridUid, out var grid))
tile = MapSystem.GetTileRef(gridUid, grid, serverCoords).Tile;
2023-04-15 07:41:25 +12:00
});
Assert.That(tile.TypeId, Is.EqualTo(targetTile.TypeId));
}
2023-04-17 18:07:03 +12:00
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));
}
2023-04-15 07:41:25 +12:00
#endregion
#region Entity lookups
/// <summary>
/// Returns entities in an area around the target. Ignores the map, grid, player, target, and contained entities.
/// </summary>
protected async Task<HashSet<EntityUid>> DoEntityLookup(LookupFlags flags = LookupFlags.Uncontained)
{
var lookup = SEntMan.System<EntityLookupSystem>();
HashSet<EntityUid> entities = default!;
await Server.WaitPost(() =>
{
// Get all entities left behind by deconstruction
entities = lookup.GetEntitiesIntersecting(MapId, Box2.CentredAroundZero(new Vector2(10, 10)), flags);
2023-04-15 07:41:25 +12:00
var xformQuery = SEntMan.GetEntityQuery<TransformComponent>();
HashSet<EntityUid> toRemove = new();
foreach (var ent in entities)
{
var transform = xformQuery.GetComponent(ent);
var netEnt = SEntMan.GetNetEntity(ent);
2023-04-15 07:41:25 +12:00
if (ent == transform.MapUid
|| ent == transform.GridUid
|| netEnt == Player
|| netEnt == Target)
2023-04-15 07:41:25 +12:00
{
toRemove.Add(ent);
}
}
entities.ExceptWith(toRemove);
});
return entities;
}
/// <summary>
/// Performs an entity lookup and asserts that only the listed entities exist and that they are all present.
/// Ignores the grid, map, player, target and contained entities.
/// </summary>
protected async Task AssertEntityLookup(params EntitySpecifier[] entities)
{
var collection = new EntitySpecifierCollection(entities);
await AssertEntityLookup(collection);
}
/// <summary>
/// Performs an entity lookup and asserts that only the listed entities exist and that they are all present.
/// Ignores the grid, map, player, target, contained entities, and entities with null prototypes.
2023-04-15 07:41:25 +12:00
/// </summary>
protected async Task AssertEntityLookup(
EntitySpecifierCollection collection,
bool failOnMissing = true,
bool failOnExcess = true,
LookupFlags flags = LookupFlags.Uncontained)
{
var expected = collection.Clone();
var entities = await DoEntityLookup(flags);
var found = ToEntityCollection(entities);
expected.Remove(found);
await expected.ConvertToStacks(ProtoMan, Factory, Server);
2023-04-15 07:41:25 +12:00
if (expected.Entities.Count == 0)
return;
Assert.Multiple(() =>
{
foreach (var (proto, quantity) in expected.Entities)
{
if (proto == "Audio")
continue;
2023-04-15 07:41:25 +12:00
if (quantity < 0 && failOnExcess)
Assert.Fail($"Unexpected entity/stack: {proto}, quantity: {-quantity}");
if (quantity > 0 && failOnMissing)
Assert.Fail($"Missing entity/stack: {proto}, quantity: {quantity}");
if (quantity == 0)
throw new Exception("Error in entity collection math.");
}
});
}
/// <summary>
/// Performs an entity lookup and attempts to find an entity matching the given entity specifier.
/// </summary>
/// <remarks>
/// This is used to check that an item-crafting attempt was successful. Ideally crafting items would just return the
/// entity or raise an event or something.
/// </remarks>
protected async Task<EntityUid> FindEntity(
EntitySpecifier spec,
LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Contained,
bool shouldSucceed = true)
{
await spec.ConvertToStack(ProtoMan, Factory, Server);
2023-04-15 07:41:25 +12:00
var entities = await DoEntityLookup(flags);
foreach (var uid in entities)
{
var found = ToEntitySpecifier(uid);
if (found is null)
continue;
2023-04-15 07:41:25 +12:00
if (spec.Prototype != found.Prototype)
continue;
if (found.Quantity >= spec.Quantity)
return uid;
// TODO combine stacks?
}
if (shouldSucceed)
Assert.Fail($"Could not find stack/entity with prototype {spec.Prototype}");
return default;
}
#endregion
/// <summary>
/// List of currently active DoAfters on the player.
/// </summary>
protected IEnumerable<Shared.DoAfter.DoAfter> ActiveDoAfters
=> DoAfters.DoAfters.Values.Where(x => !x.Cancelled && !x.Completed);
2024-06-04 09:05:51 +12:00
#region Component
2023-04-15 07:41:25 +12:00
/// <summary>
/// Convenience method to get components on the target. Returns SERVER-SIDE components.
/// </summary>
protected T Comp<T>(NetEntity? target = null) where T : IComponent
{
target ??= Target;
if (target == null)
Assert.Fail("No target specified");
2024-06-04 09:05:51 +12:00
return SEntMan.GetComponent<T>(ToServer(target!.Value));
}
/// <inheritdoc cref="Comp{T}"/>
protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent
{
return SEntMan.TryGetComponent(ToServer(target), out comp);
}
/// <inheritdoc cref="Comp{T}"/>
protected bool TryComp<T>([NotNullWhen(true)] out T? comp) where T : IComponent
{
return SEntMan.TryGetComponent(STarget, out comp);
}
2023-04-15 07:41:25 +12:00
2024-06-04 09:05:51 +12:00
#endregion
2023-04-15 07:41:25 +12:00
/// <summary>
/// Set the tile at the target position to some prototype.
/// </summary>
protected async Task SetTile(string? proto, NetCoordinates? coords = null, Entity<MapGridComponent>? grid = null)
2023-04-15 07:41:25 +12:00
{
var tile = proto == null
? Tile.Empty
: new Tile(TileMan[proto].TileId);
var pos = Transform.ToMapCoordinates(SEntMan.GetCoordinates(coords ?? TargetCoords));
2023-04-15 07:41:25 +12:00
EntityUid gridUid;
MapGridComponent? gridComp;
2023-04-15 07:41:25 +12:00
await Server.WaitPost(() =>
{
if (grid is { } gridEnt)
2023-04-15 07:41:25 +12:00
{
MapSystem.SetTile(gridEnt, SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
return;
}
else if (MapMan.TryFindGridAt(pos, out var gUid, out var gComp))
{
MapSystem.SetTile(gUid, gComp, SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
2023-04-15 07:41:25 +12:00
return;
}
if (proto == null)
return;
gridEnt = MapMan.CreateGridEntity(MapData.MapId);
grid = gridEnt;
gridUid = gridEnt;
gridComp = gridEnt.Comp;
var gridXform = SEntMan.GetComponent<TransformComponent>(gridUid);
Transform.SetWorldPosition((gridUid, gridXform), pos.Position);
MapSystem.SetTile((gridUid, gridComp), SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
2023-04-15 07:41:25 +12:00
if (!MapMan.TryFindGridAt(pos, out _, out _))
2023-04-15 07:41:25 +12:00
Assert.Fail("Failed to create grid?");
});
await AssertTile(proto, coords);
}
protected async Task Delete(EntityUid uid)
2023-04-15 07:41:25 +12:00
{
await Server.WaitPost(() => SEntMan.DeleteEntity(uid));
await RunTicks(5);
}
protected Task Delete(NetEntity nuid)
{
return Delete(SEntMan.GetEntity(nuid));
}
2023-04-17 18:07:03 +12:00
#region Time/Tick managment
2023-04-15 07:41:25 +12:00
protected async Task RunTicks(int ticks)
{
await Pair.RunTicksSync(ticks);
2023-04-15 07:41:25 +12:00
}
protected async Task RunSeconds(float seconds)
{
await Pair.RunSeconds(seconds);
}
2023-04-17 18:07:03 +12:00
#endregion
#region BUI
/// <summary>
/// Sends a bui message using the given bui key.
/// </summary>
protected async Task SendBui(Enum key, BoundUserInterfaceMessage msg, EntityUid? _ = null)
{
if (!TryGetBui(key, out var bui))
return;
await Client.WaitPost(() => bui.SendMessage(msg));
// allow for client -> server and server -> client messages to be sent.
await RunTicks(15);
}
/// <summary>
/// Sends a bui message using the given bui key.
/// </summary>
protected async Task CloseBui(Enum key, EntityUid? _ = null)
{
if (!TryGetBui(key, out var bui))
return;
await Client.WaitPost(() => bui.Close());
// allow for client -> server and server -> client messages to be sent.
await RunTicks(15);
}
protected bool TryGetBui(Enum key, [NotNullWhen(true)] out BoundUserInterface? bui, NetEntity? target = null, bool shouldSucceed = true)
{
bui = null;
target ??= Target;
if (target == null)
{
Assert.Fail("No target specified");
return false;
}
2023-09-11 21:20:46 +10:00
if (!CEntMan.TryGetComponent<UserInterfaceComponent>(CEntMan.GetEntity(target), out var ui))
{
if (shouldSucceed)
Assert.Fail($"Entity {SEntMan.ToPrettyString(SEntMan.GetEntity(target.Value))} does not have a bui component");
return false;
}
if (!ui.ClientOpenInterfaces.TryGetValue(key, out bui))
{
if (shouldSucceed)
Assert.Fail($"Entity {SEntMan.ToPrettyString(SEntMan.GetEntity(target.Value))} does not have an open bui with key {key.GetType()}.{key}.");
return false;
}
2023-07-08 09:02:17 -07:00
var bui2 = bui;
Assert.Multiple(() =>
{
Assert.That(bui2.UiKey, Is.EqualTo(key), $"Bound user interface {bui2} is indexed by a key other than the one assigned to it somehow. {bui2.UiKey} != {key}");
Assert.That(shouldSucceed, Is.True);
});
return true;
}
2024-06-04 09:05:51 +12:00
protected bool IsUiOpen(Enum key)
{
if (!TryComp(Player, out UserInterfaceUserComponent? user))
return false;
foreach (var keys in user.OpenInterfaces.Values)
{
if (keys.Contains(key))
return true;
}
return false;
}
#endregion
#region UI
/// <summary>
2024-06-04 09:05:51 +12:00
/// Attempts to find, and then presses and releases a control on some client-side window.
/// Will fail if the control cannot be found.
/// </summary>
protected async Task ClickControl<TWindow, TControl>(string name, BoundKeyFunction? function = null)
where TWindow : BaseWindow
where TControl : Control
{
var window = GetWindow<TWindow>();
var control = GetControlFromField<TControl>(name, window);
await ClickControl(control, function);
}
/// <summary>
/// Attempts to find, and then presses and releases a control on some client-side widget.
/// Will fail if the control cannot be found.
/// </summary>
2024-06-04 09:05:51 +12:00
protected async Task ClickWidgetControl<TWidget, TControl>(string name, BoundKeyFunction? function = null)
where TWidget : UIWidget, new()
where TControl : Control
{
var widget = GetWidget<TWidget>();
var control = GetControlFromField<TControl>(name, widget);
await ClickControl(control, function);
}
/// <inheritdoc cref="ClickControl{TWindow,TControl}"/>
protected async Task ClickControl<TWindow>(string name, BoundKeyFunction? function = null)
where TWindow : BaseWindow
{
2024-06-04 09:05:51 +12:00
await ClickControl<TWindow, Control>(name, function);
}
/// <inheritdoc cref="ClickWidgetControl{TWidget,TControl}"/>
protected async Task ClickWidgetControl<TWidget>(string name, BoundKeyFunction? function = null)
where TWidget : UIWidget, new()
{
await ClickWidgetControl<TWidget, Control>(name, function);
}
/// <summary>
2024-06-04 09:05:51 +12:00
/// Simulates a click and release at the center of some UI control.
/// </summary>
2024-06-04 09:05:51 +12:00
protected async Task ClickControl(Control control, BoundKeyFunction? function = null)
{
2024-06-04 09:05:51 +12:00
function ??= EngineKeyFunctions.UIClick;
var screenCoords = new ScreenCoordinates(
control.GlobalPixelPosition + control.PixelSize / 2,
control.Window?.Id ?? default);
var relativePos = screenCoords.Position / control.UIScale - control.GlobalPosition;
var relativePixelPos = screenCoords.Position - control.GlobalPixelPosition;
var args = new GUIBoundKeyEventArgs(
2024-06-04 09:05:51 +12:00
function.Value,
BoundKeyState.Down,
screenCoords,
default,
relativePos,
relativePixelPos);
await Client.DoGuiEvent(control, args);
await RunTicks(1);
args = new GUIBoundKeyEventArgs(
2024-06-04 09:05:51 +12:00
function.Value,
BoundKeyState.Up,
screenCoords,
default,
relativePos,
relativePixelPos);
await Client.DoGuiEvent(control, args);
await RunTicks(1);
}
/// <summary>
2024-06-04 09:05:51 +12:00
/// Attempt to retrieve a control by looking for a field on some other control.
/// </summary>
2024-06-04 09:05:51 +12:00
/// <remarks>
/// Will fail if the control cannot be found.
/// </remarks>
protected TControl GetControlFromField<TControl>(string name, Control parent)
where TControl : Control
{
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
2024-06-04 09:05:51 +12:00
var parentType = parent.GetType();
var field = parentType.GetField(name, flags);
var prop = parentType.GetProperty(name, flags);
if (field == null && prop == null)
{
2024-06-04 09:05:51 +12:00
Assert.Fail($"Window {parentType.Name} does not have a field or property named {name}");
return default!;
}
2024-06-04 09:05:51 +12:00
var fieldOrProp = field?.GetValue(parent) ?? prop?.GetValue(parent);
if (fieldOrProp is not Control control)
{
Assert.Fail($"{name} was null or was not a control.");
return default!;
}
2024-06-04 09:05:51 +12:00
Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
return (TControl) control;
}
/// <summary>
/// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
/// </summary>
/// <remarks>
/// Will fail if the control cannot be found.
/// </remarks>
protected TControl GetControlFromChildren<TControl>(Func<TControl, bool> predicate, Control parent, bool recursive = true)
where TControl : Control
{
if (TryGetControlFromChildren(predicate, parent, out var control, recursive))
return control;
Assert.Fail($"Failed to find a {nameof(TControl)} that satisfies the predicate in {parent.Name}");
return default!;
}
/// <summary>
/// Attempt to retrieve a control of a given type by iterating through a control's children.
/// </summary>
protected TControl GetControlFromChildren<TControl>(Control parent, bool recursive = false)
where TControl : Control
{
return GetControlFromChildren<TControl>(static _ => true, parent, recursive);
}
/// <summary>
/// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
/// </summary>
protected bool TryGetControlFromChildren<TControl>(
Func<TControl, bool> predicate,
Control parent,
[NotNullWhen(true)] out TControl? control,
bool recursive = true)
where TControl : Control
{
foreach (var ctrl in parent.Children)
{
if (ctrl is TControl cast && predicate(cast))
{
control = cast;
return true;
}
if (recursive && TryGetControlFromChildren(predicate, ctrl, out control))
return true;
}
control = null;
return false;
}
/// <summary>
/// Attempts to find a currently open client-side window. Will fail if the window cannot be found.
/// </summary>
/// <remarks>
/// Note that this just returns the very first open window of this type that is found.
/// </remarks>
protected TWindow GetWindow<TWindow>() where TWindow : BaseWindow
{
if (TryFindWindow(out TWindow? window))
return window;
Assert.Fail($"Could not find a window assignable to {nameof(TWindow)}");
return default!;
}
/// <summary>
/// Attempts to find a currently open client-side window.
/// </summary>
/// <remarks>
/// Note that this just returns the very first open window of this type that is found.
/// </remarks>
protected bool TryFindWindow<TWindow>([NotNullWhen(true)] out TWindow? window) where TWindow : BaseWindow
{
TryFindWindow(typeof(TWindow), out var control);
window = control as TWindow;
return window != null;
}
/// <summary>
/// Attempts to find a currently open client-side window.
/// </summary>
/// <remarks>
/// Note that this just returns the very first open window of this type that is found.
/// </remarks>
protected bool TryFindWindow(Type type, [NotNullWhen(true)] out BaseWindow? window)
{
Assert.That(type.IsAssignableTo(typeof(BaseWindow)));
window = UiMan.WindowRoot.Children
.OfType<BaseWindow>()
.Where(x => x.IsOpen)
.FirstOrDefault(x => x.GetType().IsAssignableTo(type));
return window != null;
}
2024-06-04 09:05:51 +12:00
/// <summary>
/// Attempts to find client-side UI widget.
/// </summary>
protected UIWidget GetWidget<TWidget>()
where TWidget : UIWidget, new()
{
if (TryFindWidget(out TWidget? widget))
return widget;
Assert.Fail($"Could not find a {typeof(TWidget).Name} widget");
return default!;
}
/// <summary>
/// Attempts to find client-side UI widget.
/// </summary>
private bool TryFindWidget<TWidget>([NotNullWhen(true)] out TWidget? uiWidget)
where TWidget : UIWidget, new()
{
uiWidget = null;
var screen = UiMan.ActiveScreen;
if (screen == null)
return false;
return screen.TryGetWidget(out uiWidget);
}
#endregion
#region Power
protected void ToggleNeedPower(NetEntity? target = null)
{
var comp = Comp<ApcPowerReceiverComponent>(target);
comp.NeedsPower = !comp.NeedsPower;
}
#endregion
2023-04-17 18:07:03 +12:00
#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)
{
2024-04-20 17:01:15 -04:00
var target = uid ?? MapData.Grid;
2023-04-17 18:07:03 +12:00
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(() =>
{
2023-06-28 21:22:03 +10:00
var atmosSystem = SEntMan.System<AtmosphereSystem>();
2023-04-17 18:07:03 +12:00
var moles = new float[Atmospherics.AdjustedNumberOfGases];
moles[(int) Gas.Oxygen] = 21.824779f;
moles[(int) Gas.Nitrogen] = 82.10312f;
atmosSystem.SetMapAtmosphere(target, false, new GasMixture(moles, Atmospherics.T20C));
2023-04-17 18:07:03 +12:00
});
}
#endregion
#region Inputs
2023-04-17 18:07:03 +12:00
/// <summary>
/// Make the client press and then release a key. This assumes the key is currently released.
/// This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
2023-04-17 18:07:03 +12:00
/// </summary>
protected async Task PressKey(
BoundKeyFunction key,
int ticks = 1,
NetCoordinates? coordinates = null,
NetEntity? cursorEntity = null)
2023-04-17 18:07:03 +12:00
{
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.
/// This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
2023-04-17 18:07:03 +12:00
/// </summary>
protected async Task SetKey(
BoundKeyFunction key,
BoundKeyState state,
NetCoordinates? coordinates = null,
NetEntity? cursorEntity = null,
ScreenCoordinates? screenCoordinates = null)
2023-04-17 18:07:03 +12:00
{
var coords = coordinates ?? TargetCoords;
var target = cursorEntity ?? Target ?? default;
var screen = screenCoordinates ?? default;
2023-04-17 18:07:03 +12:00
var funcId = InputManager.NetworkBindMap.KeyFunctionID(key);
var message = new ClientFullInputCmdMessage(CTiming.CurTick, CTiming.TickFraction, funcId)
{
State = state,
Coordinates = CEntMan.GetCoordinates(coords),
ScreenCoordinates = screen,
Uid = CEntMan.GetEntity(target),
};
2023-04-17 18:07:03 +12:00
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
#region Networking
protected EntityUid ToServer(NetEntity nent) => SEntMan.GetEntity(nent);
protected EntityUid ToClient(NetEntity nent) => CEntMan.GetEntity(nent);
protected EntityUid? ToServer(NetEntity? nent) => SEntMan.GetEntity(nent);
protected EntityUid? ToClient(NetEntity? nent) => CEntMan.GetEntity(nent);
protected EntityUid ToServer(EntityUid cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid));
protected EntityUid ToClient(EntityUid cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid));
protected EntityUid? ToServer(EntityUid? cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid));
protected EntityUid? ToClient(EntityUid? cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid));
protected EntityCoordinates ToServer(NetCoordinates coords) => SEntMan.GetCoordinates(coords);
protected EntityCoordinates ToClient(NetCoordinates coords) => CEntMan.GetCoordinates(coords);
protected EntityCoordinates? ToServer(NetCoordinates? coords) => SEntMan.GetCoordinates(coords);
protected EntityCoordinates? ToClient(NetCoordinates? coords) => CEntMan.GetCoordinates(coords);
#endregion
#region Metadata & Transforms
protected MetaDataComponent Meta(NetEntity uid) => Meta(ToServer(uid));
protected MetaDataComponent Meta(EntityUid uid) => SEntMan.GetComponent<MetaDataComponent>(uid);
protected TransformComponent Xform(NetEntity uid) => Xform(ToServer(uid));
protected TransformComponent Xform(EntityUid uid) => SEntMan.GetComponent<TransformComponent>(uid);
protected EntityCoordinates Position(NetEntity uid) => Position(ToServer(uid));
protected EntityCoordinates Position(EntityUid uid) => Xform(uid).Coordinates;
#endregion
2023-04-15 07:41:25 +12:00
}