InteractionTest.Helpers.cs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293
  1. #nullable enable
  2. using System.Collections.Generic;
  3. using System.Diagnostics.CodeAnalysis;
  4. using System.Linq;
  5. using System.Numerics;
  6. using System.Reflection;
  7. using Content.Client.Construction;
  8. using Content.Server.Atmos.EntitySystems;
  9. using Content.Server.Construction.Components;
  10. using Content.Server.Gravity;
  11. using Content.Server.Power.Components;
  12. using Content.Shared.Atmos;
  13. using Content.Shared.Construction.Prototypes;
  14. using Content.Shared.Gravity;
  15. using Content.Shared.Item;
  16. using Robust.Client.UserInterface;
  17. using Robust.Client.UserInterface.Controls;
  18. using Robust.Client.UserInterface.CustomControls;
  19. using Robust.Shared.GameObjects;
  20. using Robust.Shared.Input;
  21. using Robust.Shared.Map;
  22. using Robust.Shared.Map.Components;
  23. using Robust.Shared.Maths;
  24. using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent;
  25. namespace Content.IntegrationTests.Tests.Interaction;
  26. // This partial class defines various methods that are useful for performing & validating interactions
  27. public abstract partial class InteractionTest
  28. {
  29. /// <summary>
  30. /// Begin constructing an entity.
  31. /// </summary>
  32. protected async Task StartConstruction(string prototype, bool shouldSucceed = true)
  33. {
  34. var proto = ProtoMan.Index<ConstructionPrototype>(prototype);
  35. Assert.That(proto.Type, Is.EqualTo(ConstructionType.Structure));
  36. await Client.WaitPost(() =>
  37. {
  38. Assert.That(CConSys.TrySpawnGhost(proto, CEntMan.GetCoordinates(TargetCoords), Direction.South, out var clientTarget),
  39. Is.EqualTo(shouldSucceed));
  40. if (!shouldSucceed)
  41. return;
  42. var comp = CEntMan.GetComponent<ConstructionGhostComponent>(clientTarget!.Value);
  43. Target = CEntMan.GetNetEntity(clientTarget.Value);
  44. Assert.That(Target.Value.IsClientSide());
  45. ConstructionGhostId = clientTarget.Value.GetHashCode();
  46. });
  47. await RunTicks(1);
  48. }
  49. /// <summary>
  50. /// Craft an item.
  51. /// </summary>
  52. protected async Task CraftItem(string prototype, bool shouldSucceed = true)
  53. {
  54. Assert.That(ProtoMan.Index<ConstructionPrototype>(prototype).Type, Is.EqualTo(ConstructionType.Item));
  55. // Please someone purge async construction code
  56. Task<bool> task = default!;
  57. await Server.WaitPost(() =>
  58. {
  59. task = SConstruction.TryStartItemConstruction(prototype, SEntMan.GetEntity(Player));
  60. });
  61. Task? tickTask = null;
  62. while (!task.IsCompleted)
  63. {
  64. tickTask = Pair.RunTicksSync(1);
  65. await Task.WhenAny(task, tickTask);
  66. }
  67. if (tickTask != null)
  68. await tickTask;
  69. #pragma warning disable RA0004
  70. Assert.That(task.Result, Is.EqualTo(shouldSucceed));
  71. #pragma warning restore RA0004
  72. await RunTicks(5);
  73. }
  74. /// <summary>
  75. /// Spawn an entity entity and set it as the target.
  76. /// </summary>
  77. [MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
  78. #pragma warning disable CS8774 // Member must have a non-null value when exiting.
  79. protected async Task<NetEntity> SpawnTarget(string prototype)
  80. {
  81. Target = NetEntity.Invalid;
  82. await Server.WaitPost(() =>
  83. {
  84. Target = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
  85. });
  86. await RunTicks(5);
  87. AssertPrototype(prototype);
  88. return Target!.Value;
  89. }
  90. #pragma warning restore CS8774 // Member must have a non-null value when exiting.
  91. /// <summary>
  92. /// Spawn an entity in preparation for deconstruction
  93. /// </summary>
  94. protected async Task StartDeconstruction(string prototype)
  95. {
  96. await SpawnTarget(prototype);
  97. var serverTarget = SEntMan.GetEntity(Target);
  98. Assert.That(SEntMan.TryGetComponent(serverTarget, out ConstructionComponent? comp));
  99. await Server.WaitPost(() => SConstruction.SetPathfindingTarget(serverTarget!.Value, comp!.DeconstructionNode, comp));
  100. await RunTicks(5);
  101. }
  102. /// <summary>
  103. /// Drops and deletes the currently held entity.
  104. /// </summary>
  105. protected async Task DeleteHeldEntity()
  106. {
  107. if (Hands.ActiveHandEntity is { } held)
  108. {
  109. await Server.WaitPost(() =>
  110. {
  111. Assert.That(HandSys.TryDrop(SEntMan.GetEntity(Player), null, false, true, Hands));
  112. SEntMan.DeleteEntity(held);
  113. SLogger.Debug($"Deleting held entity");
  114. });
  115. }
  116. await RunTicks(1);
  117. Assert.That(Hands.ActiveHandEntity, Is.Null);
  118. }
  119. /// <summary>
  120. /// Place an entity prototype into the players hand. Deletes any currently held entity.
  121. /// </summary>
  122. /// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
  123. /// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
  124. /// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
  125. protected async Task<NetEntity> PlaceInHands(string id, int quantity = 1, bool enableToggleable = true)
  126. {
  127. return await PlaceInHands((id, quantity), enableToggleable);
  128. }
  129. /// <summary>
  130. /// Place an entity prototype into the players hand. Deletes any currently held entity.
  131. /// </summary>
  132. /// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
  133. /// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
  134. protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableToggleable = true)
  135. {
  136. if (Hands.ActiveHand == null)
  137. {
  138. Assert.Fail("No active hand");
  139. return default;
  140. }
  141. Assert.That(!string.IsNullOrWhiteSpace(entity.Prototype));
  142. await DeleteHeldEntity();
  143. // spawn and pick up the new item
  144. var item = await SpawnEntity(entity, SEntMan.GetCoordinates(PlayerCoords));
  145. ItemToggleComponent? itemToggle = null;
  146. await Server.WaitPost(() =>
  147. {
  148. var playerEnt = SEntMan.GetEntity(Player);
  149. Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHand, false, false, Hands));
  150. // turn on welders
  151. if (enableToggleable && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
  152. {
  153. Assert.That(ItemToggleSys.TryActivate((item, itemToggle), user: playerEnt));
  154. }
  155. });
  156. await RunTicks(1);
  157. Assert.That(Hands.ActiveHandEntity, Is.EqualTo(item));
  158. if (enableToggleable && itemToggle != null)
  159. Assert.That(itemToggle.Activated);
  160. return SEntMan.GetNetEntity(item);
  161. }
  162. /// <summary>
  163. /// Pick up an entity. Defaults to just deleting the previously held entity.
  164. /// </summary>
  165. protected async Task Pickup(NetEntity? entity = null, bool deleteHeld = true)
  166. {
  167. entity ??= Target;
  168. if (Hands.ActiveHand == null)
  169. {
  170. Assert.Fail("No active hand");
  171. return;
  172. }
  173. if (deleteHeld)
  174. await DeleteHeldEntity();
  175. var uid = SEntMan.GetEntity(entity);
  176. if (!SEntMan.TryGetComponent(uid, out ItemComponent? item))
  177. {
  178. Assert.Fail($"Entity {entity} is not an item");
  179. return;
  180. }
  181. await Server.WaitPost(() =>
  182. {
  183. Assert.That(HandSys.TryPickup(SEntMan.GetEntity(Player), uid.Value, Hands.ActiveHand, false, false, Hands, item));
  184. });
  185. await RunTicks(1);
  186. Assert.That(Hands.ActiveHandEntity, Is.EqualTo(uid));
  187. }
  188. /// <summary>
  189. /// Drops the currently held entity.
  190. /// </summary>
  191. protected async Task Drop()
  192. {
  193. if (Hands.ActiveHandEntity == null)
  194. {
  195. Assert.Fail("Not holding any entity to drop");
  196. return;
  197. }
  198. await Server.WaitPost(() =>
  199. {
  200. Assert.That(HandSys.TryDrop(SEntMan.GetEntity(Player), handsComp: Hands));
  201. });
  202. await RunTicks(1);
  203. Assert.That(Hands.ActiveHandEntity, Is.Null);
  204. }
  205. #region Interact
  206. /// <summary>
  207. /// Use the currently held entity.
  208. /// </summary>
  209. protected async Task UseInHand()
  210. {
  211. if (Hands.ActiveHandEntity is not { } target)
  212. {
  213. Assert.Fail("Not holding any entity");
  214. return;
  215. }
  216. await Server.WaitPost(() =>
  217. {
  218. InteractSys.UserInteraction(SEntMan.GetEntity(Player), SEntMan.GetComponent<TransformComponent>(target).Coordinates, target);
  219. });
  220. }
  221. /// <summary>
  222. /// Place an entity prototype into the players hand and interact with the given entity (or target position)
  223. /// </summary>
  224. /// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
  225. /// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
  226. /// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
  227. protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true)
  228. {
  229. await InteractUsing((id, quantity), awaitDoAfters);
  230. }
  231. /// <summary>
  232. /// Place an entity prototype into the players hand and interact with the given entity (or target position).
  233. /// </summary>
  234. /// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
  235. /// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
  236. protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true)
  237. {
  238. // For every interaction, we will also examine the entity, just in case this breaks something, somehow.
  239. // (e.g., servers attempt to assemble construction examine hints).
  240. if (Target != null)
  241. {
  242. await Client.WaitPost(() => ExamineSys.DoExamine(CEntMan.GetEntity(Target.Value)));
  243. }
  244. await PlaceInHands(entity);
  245. await Interact(awaitDoAfters);
  246. }
  247. /// <summary>
  248. /// Interact with an entity using the currently held entity.
  249. /// </summary>
  250. /// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
  251. protected async Task Interact(bool awaitDoAfters = true)
  252. {
  253. if (Target == null || !Target.Value.IsClientSide())
  254. {
  255. await Interact(Target, TargetCoords, awaitDoAfters);
  256. return;
  257. }
  258. // The target is a client-side entity, so we will just attempt to start construction under the assumption that
  259. // it is a construction ghost.
  260. await Client.WaitPost(() => CConSys.TryStartConstruction(CTarget!.Value));
  261. await RunTicks(5);
  262. if (awaitDoAfters)
  263. await AwaitDoAfters();
  264. await CheckTargetChange();
  265. }
  266. /// <inheritdoc cref="Interact(EntityUid?,EntityCoordinates,bool)"/>
  267. protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true)
  268. {
  269. Assert.That(SEntMan.TryGetEntity(target, out var sTarget) || target == null);
  270. var coords = SEntMan.GetCoordinates(coordinates);
  271. Assert.That(coords.IsValid(SEntMan));
  272. await Interact(sTarget, coords, awaitDoAfters);
  273. }
  274. /// <summary>
  275. /// Interact with an entity using the currently held entity.
  276. /// </summary>
  277. protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true)
  278. {
  279. Assert.That(SEntMan.TryGetEntity(Player, out var player));
  280. await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target));
  281. await RunTicks(1);
  282. if (awaitDoAfters)
  283. await AwaitDoAfters();
  284. await CheckTargetChange();
  285. }
  286. /// <summary>
  287. /// Activate an entity.
  288. /// </summary>
  289. protected async Task Activate(NetEntity? target = null, bool awaitDoAfters = true)
  290. {
  291. target ??= Target;
  292. Assert.That(target, Is.Not.Null);
  293. Assert.That(SEntMan.TryGetEntity(target!.Value, out var sTarget));
  294. Assert.That(SEntMan.TryGetEntity(Player, out var player));
  295. await Server.WaitPost(() => InteractSys.InteractionActivate(player!.Value, sTarget!.Value));
  296. await RunTicks(1);
  297. if (awaitDoAfters)
  298. await AwaitDoAfters();
  299. await CheckTargetChange();
  300. }
  301. /// <summary>
  302. /// Variant of <see cref="InteractUsing(string,int,bool)"/> that performs several interactions using different entities.
  303. /// Useful for quickly finishing multiple construction steps.
  304. /// </summary>
  305. /// <remarks>
  306. /// Empty strings imply empty hands.
  307. /// </remarks>
  308. protected async Task Interact(params EntitySpecifier[] specifiers)
  309. {
  310. foreach (var spec in specifiers)
  311. {
  312. await InteractUsing(spec);
  313. }
  314. }
  315. /// <summary>
  316. /// Throw the currently held entity. Defaults to targeting the current <see cref="TargetCoords"/>
  317. /// </summary>
  318. protected async Task<bool> ThrowItem(NetCoordinates? target = null, float minDistance = 4)
  319. {
  320. var actualTarget = SEntMan.GetCoordinates(target ?? TargetCoords);
  321. var result = false;
  322. await Server.WaitPost(() => result = HandSys.ThrowHeldItem(SEntMan.GetEntity(Player), actualTarget, minDistance));
  323. return result;
  324. }
  325. #endregion
  326. /// <summary>
  327. /// Wait for any currently active DoAfters to finish.
  328. /// </summary>
  329. protected async Task AwaitDoAfters(int maxExpected = 1)
  330. {
  331. if (!ActiveDoAfters.Any())
  332. return;
  333. // Generally expect interactions to only start one DoAfter.
  334. Assert.That(ActiveDoAfters.Count(), Is.LessThanOrEqualTo(maxExpected));
  335. // wait out the DoAfters.
  336. var doAfters = ActiveDoAfters.ToList();
  337. while (ActiveDoAfters.Any())
  338. {
  339. await RunTicks(10);
  340. }
  341. foreach (var doAfter in doAfters)
  342. {
  343. Assert.That(!doAfter.Cancelled);
  344. }
  345. await RunTicks(5);
  346. }
  347. /// <summary>
  348. /// Cancel any currently active DoAfters. Default arguments are such that it also checks that there is at least one
  349. /// active DoAfter to cancel.
  350. /// </summary>
  351. protected async Task CancelDoAfters(int minExpected = 1, int maxExpected = 1)
  352. {
  353. Assert.That(ActiveDoAfters.Count(), Is.GreaterThanOrEqualTo(minExpected));
  354. Assert.That(ActiveDoAfters.Count(), Is.LessThanOrEqualTo(maxExpected));
  355. if (!ActiveDoAfters.Any())
  356. return;
  357. // Cancel all the do-afters
  358. var doAfters = ActiveDoAfters.ToList();
  359. await Server.WaitPost(() =>
  360. {
  361. foreach (var doAfter in doAfters)
  362. {
  363. DoAfterSys.Cancel(SEntMan.GetEntity(Player), doAfter.Index, DoAfters);
  364. }
  365. });
  366. await RunTicks(1);
  367. foreach (var doAfter in doAfters)
  368. {
  369. Assert.That(doAfter.Cancelled);
  370. }
  371. Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
  372. }
  373. /// <summary>
  374. /// Check if the test's target entity has changed. E.g., construction interactions will swap out entities while
  375. /// a structure is being built.
  376. /// </summary>
  377. protected async Task CheckTargetChange()
  378. {
  379. if (Target == null)
  380. return;
  381. var originalTarget = Target.Value;
  382. await RunTicks(5);
  383. if (Target.Value.IsClientSide() && CTestSystem.Ghosts.TryGetValue(ConstructionGhostId, out var newWeh))
  384. {
  385. CLogger.Debug($"Construction ghost {ConstructionGhostId} became entity {newWeh}");
  386. Target = newWeh;
  387. }
  388. if (STestSystem.EntChanges.TryGetValue(Target.Value, out var newServerWeh))
  389. {
  390. SLogger.Debug($"Construction entity {Target.Value} changed to {newServerWeh}");
  391. Target = newServerWeh;
  392. }
  393. if (Target != originalTarget)
  394. await CheckTargetChange();
  395. }
  396. #region Asserts
  397. protected void ClientAssertPrototype(string? prototype, NetEntity? target = null)
  398. {
  399. target ??= Target;
  400. if (target == null)
  401. {
  402. Assert.Fail("No target specified");
  403. return;
  404. }
  405. var meta = CEntMan.GetComponent<MetaDataComponent>(CEntMan.GetEntity(target.Value));
  406. Assert.That(meta.EntityPrototype?.ID, Is.EqualTo(prototype));
  407. }
  408. protected void AssertPrototype(string? prototype, NetEntity? target = null)
  409. {
  410. target ??= Target;
  411. if (target == null)
  412. {
  413. Assert.Fail("No target specified");
  414. return;
  415. }
  416. var meta = SEntMan.GetComponent<MetaDataComponent>(SEntMan.GetEntity(target.Value));
  417. Assert.That(meta.EntityPrototype?.ID, Is.EqualTo(prototype));
  418. }
  419. protected void AssertAnchored(bool anchored = true, NetEntity? target = null)
  420. {
  421. target ??= Target;
  422. if (target == null)
  423. {
  424. Assert.Fail("No target specified");
  425. return;
  426. }
  427. var sXform = SEntMan.GetComponent<TransformComponent>(SEntMan.GetEntity(target.Value));
  428. var cXform = CEntMan.GetComponent<TransformComponent>(CEntMan.GetEntity(target.Value));
  429. Assert.Multiple(() =>
  430. {
  431. Assert.That(sXform.Anchored, Is.EqualTo(anchored));
  432. Assert.That(cXform.Anchored, Is.EqualTo(anchored));
  433. });
  434. }
  435. protected void AssertDeleted(NetEntity? target = null)
  436. {
  437. target ??= Target;
  438. if (target == null)
  439. {
  440. Assert.Fail("No target specified");
  441. return;
  442. }
  443. Assert.Multiple(() =>
  444. {
  445. Assert.That(SEntMan.Deleted(SEntMan.GetEntity(target)));
  446. Assert.That(CEntMan.Deleted(CEntMan.GetEntity(target)));
  447. });
  448. }
  449. protected void AssertExists(NetEntity? target = null)
  450. {
  451. target ??= Target;
  452. if (target == null)
  453. {
  454. Assert.Fail("No target specified");
  455. return;
  456. }
  457. Assert.Multiple(() =>
  458. {
  459. Assert.That(SEntMan.EntityExists(SEntMan.GetEntity(target)));
  460. Assert.That(CEntMan.EntityExists(CEntMan.GetEntity(target)));
  461. });
  462. }
  463. /// <summary>
  464. /// Assert whether or not the target has the given component.
  465. /// </summary>
  466. protected void AssertComp<T>(bool hasComp = true, NetEntity? target = null) where T : IComponent
  467. {
  468. target ??= Target;
  469. if (target == null)
  470. {
  471. Assert.Fail("No target specified");
  472. return;
  473. }
  474. Assert.That(SEntMan.HasComponent<T>(SEntMan.GetEntity(target)), Is.EqualTo(hasComp));
  475. }
  476. /// <summary>
  477. /// Check that the tile at the target position matches some prototype.
  478. /// </summary>
  479. protected async Task AssertTile(string? proto, NetCoordinates? coords = null)
  480. {
  481. var targetTile = proto == null
  482. ? Tile.Empty
  483. : new Tile(TileMan[proto].TileId);
  484. var tile = Tile.Empty;
  485. var serverCoords = SEntMan.GetCoordinates(coords ?? TargetCoords);
  486. var pos = Transform.ToMapCoordinates(serverCoords);
  487. await Server.WaitPost(() =>
  488. {
  489. if (MapMan.TryFindGridAt(pos, out var gridUid, out var grid))
  490. tile = MapSystem.GetTileRef(gridUid, grid, serverCoords).Tile;
  491. });
  492. Assert.That(tile.TypeId, Is.EqualTo(targetTile.TypeId));
  493. }
  494. protected void AssertGridCount(int value)
  495. {
  496. var count = 0;
  497. var query = SEntMan.AllEntityQueryEnumerator<MapGridComponent, TransformComponent>();
  498. while (query.MoveNext(out _, out var xform))
  499. {
  500. if (xform.MapUid == MapData.MapUid)
  501. count++;
  502. }
  503. Assert.That(count, Is.EqualTo(value));
  504. }
  505. #endregion
  506. #region Entity lookups
  507. /// <summary>
  508. /// Returns entities in an area around the target. Ignores the map, grid, player, target, and contained entities.
  509. /// </summary>
  510. protected async Task<HashSet<EntityUid>> DoEntityLookup(LookupFlags flags = LookupFlags.Uncontained)
  511. {
  512. var lookup = SEntMan.System<EntityLookupSystem>();
  513. HashSet<EntityUid> entities = default!;
  514. await Server.WaitPost(() =>
  515. {
  516. // Get all entities left behind by deconstruction
  517. entities = lookup.GetEntitiesIntersecting(MapId, Box2.CentredAroundZero(new Vector2(10, 10)), flags);
  518. var xformQuery = SEntMan.GetEntityQuery<TransformComponent>();
  519. HashSet<EntityUid> toRemove = new();
  520. foreach (var ent in entities)
  521. {
  522. var transform = xformQuery.GetComponent(ent);
  523. var netEnt = SEntMan.GetNetEntity(ent);
  524. if (ent == transform.MapUid
  525. || ent == transform.GridUid
  526. || netEnt == Player
  527. || netEnt == Target)
  528. {
  529. toRemove.Add(ent);
  530. }
  531. }
  532. entities.ExceptWith(toRemove);
  533. });
  534. return entities;
  535. }
  536. /// <summary>
  537. /// Performs an entity lookup and asserts that only the listed entities exist and that they are all present.
  538. /// Ignores the grid, map, player, target and contained entities.
  539. /// </summary>
  540. protected async Task AssertEntityLookup(params EntitySpecifier[] entities)
  541. {
  542. var collection = new EntitySpecifierCollection(entities);
  543. await AssertEntityLookup(collection);
  544. }
  545. /// <summary>
  546. /// Performs an entity lookup and asserts that only the listed entities exist and that they are all present.
  547. /// Ignores the grid, map, player, target, contained entities, and entities with null prototypes.
  548. /// </summary>
  549. protected async Task AssertEntityLookup(
  550. EntitySpecifierCollection collection,
  551. bool failOnMissing = true,
  552. bool failOnExcess = true,
  553. LookupFlags flags = LookupFlags.Uncontained)
  554. {
  555. var expected = collection.Clone();
  556. var entities = await DoEntityLookup(flags);
  557. var found = ToEntityCollection(entities);
  558. expected.Remove(found);
  559. await expected.ConvertToStacks(ProtoMan, Factory, Server);
  560. if (expected.Entities.Count == 0)
  561. return;
  562. Assert.Multiple(() =>
  563. {
  564. foreach (var (proto, quantity) in expected.Entities)
  565. {
  566. if (proto == "Audio")
  567. continue;
  568. if (quantity < 0 && failOnExcess)
  569. Assert.Fail($"Unexpected entity/stack: {proto}, quantity: {-quantity}");
  570. if (quantity > 0 && failOnMissing)
  571. Assert.Fail($"Missing entity/stack: {proto}, quantity: {quantity}");
  572. if (quantity == 0)
  573. throw new Exception("Error in entity collection math.");
  574. }
  575. });
  576. }
  577. /// <summary>
  578. /// Performs an entity lookup and attempts to find an entity matching the given entity specifier.
  579. /// </summary>
  580. /// <remarks>
  581. /// This is used to check that an item-crafting attempt was successful. Ideally crafting items would just return the
  582. /// entity or raise an event or something.
  583. /// </remarks>
  584. protected async Task<EntityUid> FindEntity(
  585. EntitySpecifier spec,
  586. LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Contained,
  587. bool shouldSucceed = true)
  588. {
  589. await spec.ConvertToStack(ProtoMan, Factory, Server);
  590. var entities = await DoEntityLookup(flags);
  591. foreach (var uid in entities)
  592. {
  593. var found = ToEntitySpecifier(uid);
  594. if (found is null)
  595. continue;
  596. if (spec.Prototype != found.Prototype)
  597. continue;
  598. if (found.Quantity >= spec.Quantity)
  599. return uid;
  600. // TODO combine stacks?
  601. }
  602. if (shouldSucceed)
  603. Assert.Fail($"Could not find stack/entity with prototype {spec.Prototype}");
  604. return default;
  605. }
  606. #endregion
  607. /// <summary>
  608. /// List of currently active DoAfters on the player.
  609. /// </summary>
  610. protected IEnumerable<Shared.DoAfter.DoAfter> ActiveDoAfters
  611. => DoAfters.DoAfters.Values.Where(x => !x.Cancelled && !x.Completed);
  612. #region Component
  613. /// <summary>
  614. /// Convenience method to get components on the target. Returns SERVER-SIDE components.
  615. /// </summary>
  616. protected T Comp<T>(NetEntity? target = null) where T : IComponent
  617. {
  618. target ??= Target;
  619. if (target == null)
  620. Assert.Fail("No target specified");
  621. return SEntMan.GetComponent<T>(ToServer(target!.Value));
  622. }
  623. /// <inheritdoc cref="Comp{T}"/>
  624. protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent
  625. {
  626. return SEntMan.TryGetComponent(ToServer(target), out comp);
  627. }
  628. /// <inheritdoc cref="Comp{T}"/>
  629. protected bool TryComp<T>([NotNullWhen(true)] out T? comp) where T : IComponent
  630. {
  631. return SEntMan.TryGetComponent(STarget, out comp);
  632. }
  633. #endregion
  634. /// <summary>
  635. /// Set the tile at the target position to some prototype.
  636. /// </summary>
  637. protected async Task SetTile(string? proto, NetCoordinates? coords = null, Entity<MapGridComponent>? grid = null)
  638. {
  639. var tile = proto == null
  640. ? Tile.Empty
  641. : new Tile(TileMan[proto].TileId);
  642. var pos = Transform.ToMapCoordinates(SEntMan.GetCoordinates(coords ?? TargetCoords));
  643. EntityUid gridUid;
  644. MapGridComponent? gridComp;
  645. await Server.WaitPost(() =>
  646. {
  647. if (grid is { } gridEnt)
  648. {
  649. MapSystem.SetTile(gridEnt, SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
  650. return;
  651. }
  652. else if (MapMan.TryFindGridAt(pos, out var gUid, out var gComp))
  653. {
  654. MapSystem.SetTile(gUid, gComp, SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
  655. return;
  656. }
  657. if (proto == null)
  658. return;
  659. gridEnt = MapMan.CreateGridEntity(MapData.MapId);
  660. grid = gridEnt;
  661. gridUid = gridEnt;
  662. gridComp = gridEnt.Comp;
  663. var gridXform = SEntMan.GetComponent<TransformComponent>(gridUid);
  664. Transform.SetWorldPosition(gridXform, pos.Position);
  665. MapSystem.SetTile((gridUid, gridComp), SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
  666. if (!MapMan.TryFindGridAt(pos, out _, out _))
  667. Assert.Fail("Failed to create grid?");
  668. });
  669. await AssertTile(proto, coords);
  670. }
  671. protected async Task Delete(EntityUid uid)
  672. {
  673. await Server.WaitPost(() => SEntMan.DeleteEntity(uid));
  674. await RunTicks(5);
  675. }
  676. protected Task Delete(NetEntity nuid)
  677. {
  678. return Delete(SEntMan.GetEntity(nuid));
  679. }
  680. #region Time/Tick managment
  681. protected async Task RunTicks(int ticks)
  682. {
  683. await Pair.RunTicksSync(ticks);
  684. }
  685. protected async Task RunSeconds(float seconds)
  686. {
  687. await Pair.RunSeconds(seconds);
  688. }
  689. #endregion
  690. #region BUI
  691. /// <summary>
  692. /// Sends a bui message using the given bui key.
  693. /// </summary>
  694. protected async Task SendBui(Enum key, BoundUserInterfaceMessage msg, EntityUid? _ = null)
  695. {
  696. if (!TryGetBui(key, out var bui))
  697. return;
  698. await Client.WaitPost(() => bui.SendMessage(msg));
  699. // allow for client -> server and server -> client messages to be sent.
  700. await RunTicks(15);
  701. }
  702. /// <summary>
  703. /// Sends a bui message using the given bui key.
  704. /// </summary>
  705. protected async Task CloseBui(Enum key, EntityUid? _ = null)
  706. {
  707. if (!TryGetBui(key, out var bui))
  708. return;
  709. await Client.WaitPost(() => bui.Close());
  710. // allow for client -> server and server -> client messages to be sent.
  711. await RunTicks(15);
  712. }
  713. protected bool TryGetBui(Enum key, [NotNullWhen(true)] out BoundUserInterface? bui, NetEntity? target = null, bool shouldSucceed = true)
  714. {
  715. bui = null;
  716. target ??= Target;
  717. if (target == null)
  718. {
  719. Assert.Fail("No target specified");
  720. return false;
  721. }
  722. if (!CEntMan.TryGetComponent<UserInterfaceComponent>(CEntMan.GetEntity(target), out var ui))
  723. {
  724. if (shouldSucceed)
  725. Assert.Fail($"Entity {SEntMan.ToPrettyString(SEntMan.GetEntity(target.Value))} does not have a bui component");
  726. return false;
  727. }
  728. if (!ui.ClientOpenInterfaces.TryGetValue(key, out bui))
  729. {
  730. if (shouldSucceed)
  731. Assert.Fail($"Entity {SEntMan.ToPrettyString(SEntMan.GetEntity(target.Value))} does not have an open bui with key {key.GetType()}.{key}.");
  732. return false;
  733. }
  734. var bui2 = bui;
  735. Assert.Multiple(() =>
  736. {
  737. 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}");
  738. Assert.That(shouldSucceed, Is.True);
  739. });
  740. return true;
  741. }
  742. protected bool IsUiOpen(Enum key)
  743. {
  744. if (!TryComp(Player, out UserInterfaceUserComponent? user))
  745. return false;
  746. foreach (var keys in user.OpenInterfaces.Values)
  747. {
  748. if (keys.Contains(key))
  749. return true;
  750. }
  751. return false;
  752. }
  753. #endregion
  754. #region UI
  755. /// <summary>
  756. /// Attempts to find, and then presses and releases a control on some client-side window.
  757. /// Will fail if the control cannot be found.
  758. /// </summary>
  759. protected async Task ClickControl<TWindow, TControl>(string name, BoundKeyFunction? function = null)
  760. where TWindow : BaseWindow
  761. where TControl : Control
  762. {
  763. var window = GetWindow<TWindow>();
  764. var control = GetControlFromField<TControl>(name, window);
  765. await ClickControl(control, function);
  766. }
  767. /// <summary>
  768. /// Attempts to find, and then presses and releases a control on some client-side widget.
  769. /// Will fail if the control cannot be found.
  770. /// </summary>
  771. protected async Task ClickWidgetControl<TWidget, TControl>(string name, BoundKeyFunction? function = null)
  772. where TWidget : UIWidget, new()
  773. where TControl : Control
  774. {
  775. var widget = GetWidget<TWidget>();
  776. var control = GetControlFromField<TControl>(name, widget);
  777. await ClickControl(control, function);
  778. }
  779. /// <inheritdoc cref="ClickControl{TWindow,TControl}"/>
  780. protected async Task ClickControl<TWindow>(string name, BoundKeyFunction? function = null)
  781. where TWindow : BaseWindow
  782. {
  783. await ClickControl<TWindow, Control>(name, function);
  784. }
  785. /// <inheritdoc cref="ClickWidgetControl{TWidget,TControl}"/>
  786. protected async Task ClickWidgetControl<TWidget>(string name, BoundKeyFunction? function = null)
  787. where TWidget : UIWidget, new()
  788. {
  789. await ClickWidgetControl<TWidget, Control>(name, function);
  790. }
  791. /// <summary>
  792. /// Simulates a click and release at the center of some UI control.
  793. /// </summary>
  794. protected async Task ClickControl(Control control, BoundKeyFunction? function = null)
  795. {
  796. function ??= EngineKeyFunctions.UIClick;
  797. var screenCoords = new ScreenCoordinates(
  798. control.GlobalPixelPosition + control.PixelSize / 2,
  799. control.Window?.Id ?? default);
  800. var relativePos = screenCoords.Position / control.UIScale - control.GlobalPosition;
  801. var relativePixelPos = screenCoords.Position - control.GlobalPixelPosition;
  802. var args = new GUIBoundKeyEventArgs(
  803. function.Value,
  804. BoundKeyState.Down,
  805. screenCoords,
  806. default,
  807. relativePos,
  808. relativePixelPos);
  809. await Client.DoGuiEvent(control, args);
  810. await RunTicks(1);
  811. args = new GUIBoundKeyEventArgs(
  812. function.Value,
  813. BoundKeyState.Up,
  814. screenCoords,
  815. default,
  816. relativePos,
  817. relativePixelPos);
  818. await Client.DoGuiEvent(control, args);
  819. await RunTicks(1);
  820. }
  821. /// <summary>
  822. /// Attempt to retrieve a control by looking for a field on some other control.
  823. /// </summary>
  824. /// <remarks>
  825. /// Will fail if the control cannot be found.
  826. /// </remarks>
  827. protected TControl GetControlFromField<TControl>(string name, Control parent)
  828. where TControl : Control
  829. {
  830. const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
  831. var parentType = parent.GetType();
  832. var field = parentType.GetField(name, flags);
  833. var prop = parentType.GetProperty(name, flags);
  834. if (field == null && prop == null)
  835. {
  836. Assert.Fail($"Window {parentType.Name} does not have a field or property named {name}");
  837. return default!;
  838. }
  839. var fieldOrProp = field?.GetValue(parent) ?? prop?.GetValue(parent);
  840. if (fieldOrProp is not Control control)
  841. {
  842. Assert.Fail($"{name} was null or was not a control.");
  843. return default!;
  844. }
  845. Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
  846. return (TControl) control;
  847. }
  848. /// <summary>
  849. /// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
  850. /// </summary>
  851. /// <remarks>
  852. /// Will fail if the control cannot be found.
  853. /// </remarks>
  854. protected TControl GetControlFromChildren<TControl>(Func<TControl, bool> predicate, Control parent, bool recursive = true)
  855. where TControl : Control
  856. {
  857. if (TryGetControlFromChildren(predicate, parent, out var control, recursive))
  858. return control;
  859. Assert.Fail($"Failed to find a {nameof(TControl)} that satisfies the predicate in {parent.Name}");
  860. return default!;
  861. }
  862. /// <summary>
  863. /// Attempt to retrieve a control of a given type by iterating through a control's children.
  864. /// </summary>
  865. protected TControl GetControlFromChildren<TControl>(Control parent, bool recursive = false)
  866. where TControl : Control
  867. {
  868. return GetControlFromChildren<TControl>(static _ => true, parent, recursive);
  869. }
  870. /// <summary>
  871. /// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
  872. /// </summary>
  873. protected bool TryGetControlFromChildren<TControl>(
  874. Func<TControl, bool> predicate,
  875. Control parent,
  876. [NotNullWhen(true)] out TControl? control,
  877. bool recursive = true)
  878. where TControl : Control
  879. {
  880. foreach (var ctrl in parent.Children)
  881. {
  882. if (ctrl is TControl cast && predicate(cast))
  883. {
  884. control = cast;
  885. return true;
  886. }
  887. if (recursive && TryGetControlFromChildren(predicate, ctrl, out control))
  888. return true;
  889. }
  890. control = null;
  891. return false;
  892. }
  893. /// <summary>
  894. /// Attempts to find a currently open client-side window. Will fail if the window cannot be found.
  895. /// </summary>
  896. /// <remarks>
  897. /// Note that this just returns the very first open window of this type that is found.
  898. /// </remarks>
  899. protected TWindow GetWindow<TWindow>() where TWindow : BaseWindow
  900. {
  901. if (TryFindWindow(out TWindow? window))
  902. return window;
  903. Assert.Fail($"Could not find a window assignable to {nameof(TWindow)}");
  904. return default!;
  905. }
  906. /// <summary>
  907. /// Attempts to find a currently open client-side window.
  908. /// </summary>
  909. /// <remarks>
  910. /// Note that this just returns the very first open window of this type that is found.
  911. /// </remarks>
  912. protected bool TryFindWindow<TWindow>([NotNullWhen(true)] out TWindow? window) where TWindow : BaseWindow
  913. {
  914. TryFindWindow(typeof(TWindow), out var control);
  915. window = control as TWindow;
  916. return window != null;
  917. }
  918. /// <summary>
  919. /// Attempts to find a currently open client-side window.
  920. /// </summary>
  921. /// <remarks>
  922. /// Note that this just returns the very first open window of this type that is found.
  923. /// </remarks>
  924. protected bool TryFindWindow(Type type, [NotNullWhen(true)] out BaseWindow? window)
  925. {
  926. Assert.That(type.IsAssignableTo(typeof(BaseWindow)));
  927. window = UiMan.WindowRoot.Children
  928. .OfType<BaseWindow>()
  929. .Where(x => x.IsOpen)
  930. .FirstOrDefault(x => x.GetType().IsAssignableTo(type));
  931. return window != null;
  932. }
  933. /// <summary>
  934. /// Attempts to find client-side UI widget.
  935. /// </summary>
  936. protected UIWidget GetWidget<TWidget>()
  937. where TWidget : UIWidget, new()
  938. {
  939. if (TryFindWidget(out TWidget? widget))
  940. return widget;
  941. Assert.Fail($"Could not find a {typeof(TWidget).Name} widget");
  942. return default!;
  943. }
  944. /// <summary>
  945. /// Attempts to find client-side UI widget.
  946. /// </summary>
  947. private bool TryFindWidget<TWidget>([NotNullWhen(true)] out TWidget? uiWidget)
  948. where TWidget : UIWidget, new()
  949. {
  950. uiWidget = null;
  951. var screen = UiMan.ActiveScreen;
  952. if (screen == null)
  953. return false;
  954. return screen.TryGetWidget(out uiWidget);
  955. }
  956. #endregion
  957. #region Power
  958. protected void ToggleNeedPower(NetEntity? target = null)
  959. {
  960. var comp = Comp<ApcPowerReceiverComponent>(target);
  961. comp.NeedsPower = !comp.NeedsPower;
  962. }
  963. #endregion
  964. #region Map Setup
  965. /// <summary>
  966. /// Adds gravity to a given entity. Defaults to the grid if no entity is specified.
  967. /// </summary>
  968. protected async Task AddGravity(EntityUid? uid = null)
  969. {
  970. var target = uid ?? MapData.Grid;
  971. await Server.WaitPost(() =>
  972. {
  973. var gravity = SEntMan.EnsureComponent<GravityComponent>(target);
  974. SEntMan.System<GravitySystem>().EnableGravity(target, gravity);
  975. });
  976. }
  977. /// <summary>
  978. /// Adds a default atmosphere to the test map.
  979. /// </summary>
  980. protected async Task AddAtmosphere(EntityUid? uid = null)
  981. {
  982. var target = uid ?? MapData.MapUid;
  983. await Server.WaitPost(() =>
  984. {
  985. var atmosSystem = SEntMan.System<AtmosphereSystem>();
  986. var moles = new float[Atmospherics.AdjustedNumberOfGases];
  987. moles[(int) Gas.Oxygen] = 21.824779f;
  988. moles[(int) Gas.Nitrogen] = 82.10312f;
  989. atmosSystem.SetMapAtmosphere(target, false, new GasMixture(moles, Atmospherics.T20C));
  990. });
  991. }
  992. #endregion
  993. #region Inputs
  994. /// <summary>
  995. /// Make the client press and then release a key. This assumes the key is currently released.
  996. /// This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
  997. /// </summary>
  998. protected async Task PressKey(
  999. BoundKeyFunction key,
  1000. int ticks = 1,
  1001. NetCoordinates? coordinates = null,
  1002. NetEntity? cursorEntity = null)
  1003. {
  1004. await SetKey(key, BoundKeyState.Down, coordinates, cursorEntity);
  1005. await RunTicks(ticks);
  1006. await SetKey(key, BoundKeyState.Up, coordinates, cursorEntity);
  1007. await RunTicks(1);
  1008. }
  1009. /// <summary>
  1010. /// Make the client press or release a key.
  1011. /// This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
  1012. /// </summary>
  1013. protected async Task SetKey(
  1014. BoundKeyFunction key,
  1015. BoundKeyState state,
  1016. NetCoordinates? coordinates = null,
  1017. NetEntity? cursorEntity = null,
  1018. ScreenCoordinates? screenCoordinates = null)
  1019. {
  1020. var coords = coordinates ?? TargetCoords;
  1021. var target = cursorEntity ?? Target ?? default;
  1022. var screen = screenCoordinates ?? default;
  1023. var funcId = InputManager.NetworkBindMap.KeyFunctionID(key);
  1024. var message = new ClientFullInputCmdMessage(CTiming.CurTick, CTiming.TickFraction, funcId)
  1025. {
  1026. State = state,
  1027. Coordinates = CEntMan.GetCoordinates(coords),
  1028. ScreenCoordinates = screen,
  1029. Uid = CEntMan.GetEntity(target),
  1030. };
  1031. await Client.WaitPost(() => InputSystem.HandleInputCommand(ClientSession, key, message));
  1032. }
  1033. /// <summary>
  1034. /// Variant of <see cref="SetKey"/> for setting movement keys.
  1035. /// </summary>
  1036. protected async Task SetMovementKey(DirectionFlag dir, BoundKeyState state)
  1037. {
  1038. if ((dir & DirectionFlag.South) != 0)
  1039. await SetKey(EngineKeyFunctions.MoveDown, state);
  1040. if ((dir & DirectionFlag.East) != 0)
  1041. await SetKey(EngineKeyFunctions.MoveRight, state);
  1042. if ((dir & DirectionFlag.North) != 0)
  1043. await SetKey(EngineKeyFunctions.MoveUp, state);
  1044. if ((dir & DirectionFlag.West) != 0)
  1045. await SetKey(EngineKeyFunctions.MoveLeft, state);
  1046. }
  1047. /// <summary>
  1048. /// Make the client hold the move key in some direction for some amount of time.
  1049. /// </summary>
  1050. protected async Task Move(DirectionFlag dir, float seconds)
  1051. {
  1052. await SetMovementKey(dir, BoundKeyState.Down);
  1053. await RunSeconds(seconds);
  1054. await SetMovementKey(dir, BoundKeyState.Up);
  1055. await RunTicks(1);
  1056. }
  1057. #endregion
  1058. #region Networking
  1059. protected EntityUid ToServer(NetEntity nent) => SEntMan.GetEntity(nent);
  1060. protected EntityUid ToClient(NetEntity nent) => CEntMan.GetEntity(nent);
  1061. protected EntityUid? ToServer(NetEntity? nent) => SEntMan.GetEntity(nent);
  1062. protected EntityUid? ToClient(NetEntity? nent) => CEntMan.GetEntity(nent);
  1063. protected EntityUid ToServer(EntityUid cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid));
  1064. protected EntityUid ToClient(EntityUid cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid));
  1065. protected EntityUid? ToServer(EntityUid? cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid));
  1066. protected EntityUid? ToClient(EntityUid? cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid));
  1067. protected EntityCoordinates ToServer(NetCoordinates coords) => SEntMan.GetCoordinates(coords);
  1068. protected EntityCoordinates ToClient(NetCoordinates coords) => CEntMan.GetCoordinates(coords);
  1069. protected EntityCoordinates? ToServer(NetCoordinates? coords) => SEntMan.GetCoordinates(coords);
  1070. protected EntityCoordinates? ToClient(NetCoordinates? coords) => CEntMan.GetCoordinates(coords);
  1071. #endregion
  1072. #region Metadata & Transforms
  1073. protected MetaDataComponent Meta(NetEntity uid) => Meta(ToServer(uid));
  1074. protected MetaDataComponent Meta(EntityUid uid) => SEntMan.GetComponent<MetaDataComponent>(uid);
  1075. protected TransformComponent Xform(NetEntity uid) => Xform(ToServer(uid));
  1076. protected TransformComponent Xform(EntityUid uid) => SEntMan.GetComponent<TransformComponent>(uid);
  1077. protected EntityCoordinates Position(NetEntity uid) => Position(ToServer(uid));
  1078. protected EntityCoordinates Position(EntityUid uid) => Xform(uid).Coordinates;
  1079. #endregion
  1080. }