ToggleableClothingSystem.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. using Content.Shared.Actions;
  2. using Content.Shared.Clothing.Components;
  3. using Content.Shared.DoAfter;
  4. using Content.Shared.IdentityManagement;
  5. using Content.Shared.Interaction;
  6. using Content.Shared.Inventory;
  7. using Content.Shared.Inventory.Events;
  8. using Content.Shared.Popups;
  9. using Content.Shared.Strip;
  10. using Content.Shared.Verbs;
  11. using Robust.Shared.Containers;
  12. using Robust.Shared.Network;
  13. using Robust.Shared.Serialization;
  14. using Robust.Shared.Timing;
  15. using Robust.Shared.Utility;
  16. namespace Content.Shared.Clothing.EntitySystems;
  17. public sealed class ToggleableClothingSystem : EntitySystem
  18. {
  19. [Dependency] private readonly IGameTiming _timing = default!;
  20. [Dependency] private readonly INetManager _netMan = default!;
  21. [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
  22. [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
  23. [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
  24. [Dependency] private readonly InventorySystem _inventorySystem = default!;
  25. [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
  26. [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
  27. [Dependency] private readonly SharedStrippableSystem _strippable = default!;
  28. public override void Initialize()
  29. {
  30. base.Initialize();
  31. SubscribeLocalEvent<ToggleableClothingComponent, ComponentInit>(OnInit);
  32. SubscribeLocalEvent<ToggleableClothingComponent, MapInitEvent>(OnMapInit);
  33. SubscribeLocalEvent<ToggleableClothingComponent, ToggleClothingEvent>(OnToggleClothing);
  34. SubscribeLocalEvent<ToggleableClothingComponent, GetItemActionsEvent>(OnGetActions);
  35. SubscribeLocalEvent<ToggleableClothingComponent, ComponentRemove>(OnRemoveToggleable);
  36. SubscribeLocalEvent<ToggleableClothingComponent, GotUnequippedEvent>(OnToggleableUnequip);
  37. SubscribeLocalEvent<AttachedClothingComponent, InteractHandEvent>(OnInteractHand);
  38. SubscribeLocalEvent<AttachedClothingComponent, GotUnequippedEvent>(OnAttachedUnequip);
  39. SubscribeLocalEvent<AttachedClothingComponent, ComponentRemove>(OnRemoveAttached);
  40. SubscribeLocalEvent<AttachedClothingComponent, BeingUnequippedAttemptEvent>(OnAttachedUnequipAttempt);
  41. SubscribeLocalEvent<ToggleableClothingComponent, InventoryRelayedEvent<GetVerbsEvent<EquipmentVerb>>>(GetRelayedVerbs);
  42. SubscribeLocalEvent<ToggleableClothingComponent, GetVerbsEvent<EquipmentVerb>>(OnGetVerbs);
  43. SubscribeLocalEvent<AttachedClothingComponent, GetVerbsEvent<EquipmentVerb>>(OnGetAttachedStripVerbsEvent);
  44. SubscribeLocalEvent<ToggleableClothingComponent, ToggleClothingDoAfterEvent>(OnDoAfterComplete);
  45. }
  46. private void GetRelayedVerbs(EntityUid uid, ToggleableClothingComponent component, InventoryRelayedEvent<GetVerbsEvent<EquipmentVerb>> args)
  47. {
  48. OnGetVerbs(uid, component, args.Args);
  49. }
  50. private void OnGetVerbs(EntityUid uid, ToggleableClothingComponent component, GetVerbsEvent<EquipmentVerb> args)
  51. {
  52. if (!args.CanAccess || !args.CanInteract || args.Hands == null || component.ClothingUid == null || component.Container == null)
  53. return;
  54. var text = component.VerbText ?? (component.ActionEntity == null ? null : Name(component.ActionEntity.Value));
  55. if (text == null)
  56. return;
  57. if (!_inventorySystem.InSlotWithFlags(uid, component.RequiredFlags))
  58. return;
  59. var wearer = Transform(uid).ParentUid;
  60. if (args.User != wearer && component.StripDelay == null)
  61. return;
  62. var verb = new EquipmentVerb()
  63. {
  64. Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
  65. Text = Loc.GetString(text),
  66. };
  67. if (args.User == wearer)
  68. {
  69. verb.EventTarget = uid;
  70. verb.ExecutionEventArgs = new ToggleClothingEvent() { Performer = args.User };
  71. }
  72. else
  73. {
  74. verb.Act = () => StartDoAfter(args.User, uid, Transform(uid).ParentUid, component);
  75. }
  76. args.Verbs.Add(verb);
  77. }
  78. private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, ToggleableClothingComponent component)
  79. {
  80. if (component.StripDelay == null)
  81. return;
  82. var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, item, component.StripDelay.Value);
  83. var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), item, wearer, item)
  84. {
  85. BreakOnDamage = true,
  86. BreakOnMove = true,
  87. // This should just re-use the BUI range checks & cancel the do after if the BUI closes. But that is all
  88. // server-side at the moment.
  89. // TODO BUI REFACTOR.
  90. DistanceThreshold = 2,
  91. };
  92. if (!_doAfter.TryStartDoAfter(args))
  93. return;
  94. if (!stealth)
  95. {
  96. var popup = Loc.GetString("strippable-component-alert-owner-interact", ("user", Identity.Entity(user, EntityManager)), ("item", item));
  97. _popupSystem.PopupEntity(popup, wearer, wearer, PopupType.Large);
  98. }
  99. }
  100. private void OnGetAttachedStripVerbsEvent(EntityUid uid, AttachedClothingComponent component, GetVerbsEvent<EquipmentVerb> args)
  101. {
  102. // redirect to the attached entity.
  103. OnGetVerbs(component.AttachedUid, Comp<ToggleableClothingComponent>(component.AttachedUid), args);
  104. }
  105. private void OnDoAfterComplete(EntityUid uid, ToggleableClothingComponent component, ToggleClothingDoAfterEvent args)
  106. {
  107. if (args.Cancelled)
  108. return;
  109. ToggleClothing(args.User, uid, component);
  110. }
  111. private void OnInteractHand(EntityUid uid, AttachedClothingComponent component, InteractHandEvent args)
  112. {
  113. if (args.Handled)
  114. return;
  115. if (!TryComp(component.AttachedUid, out ToggleableClothingComponent? toggleCom)
  116. || toggleCom.Container == null)
  117. return;
  118. if (!_inventorySystem.TryUnequip(Transform(uid).ParentUid, toggleCom.Slot, force: true))
  119. return;
  120. _containerSystem.Insert(uid, toggleCom.Container);
  121. args.Handled = true;
  122. }
  123. /// <summary>
  124. /// Called when the suit is unequipped, to ensure that the helmet also gets unequipped.
  125. /// </summary>
  126. private void OnToggleableUnequip(EntityUid uid, ToggleableClothingComponent component, GotUnequippedEvent args)
  127. {
  128. // If it's a part of PVS departure then don't handle it.
  129. if (_timing.ApplyingState)
  130. return;
  131. // If the attached clothing is not currently in the container, this just assumes that it is currently equipped.
  132. // This should maybe double check that the entity currently in the slot is actually the attached clothing, but
  133. // if its not, then something else has gone wrong already...
  134. if (component.Container != null && component.Container.ContainedEntity == null && component.ClothingUid != null)
  135. _inventorySystem.TryUnequip(args.Equipee, component.Slot, force: true);
  136. }
  137. private void OnRemoveToggleable(EntityUid uid, ToggleableClothingComponent component, ComponentRemove args)
  138. {
  139. // If the parent/owner component of the attached clothing is being removed (entity getting deleted?) we will
  140. // delete the attached entity. We do this regardless of whether or not the attached entity is currently
  141. // "outside" of the container or not. This means that if a hardsuit takes too much damage, the helmet will also
  142. // automatically be deleted.
  143. _actionsSystem.RemoveAction(component.ActionEntity);
  144. if (component.ClothingUid != null && !_netMan.IsClient)
  145. QueueDel(component.ClothingUid.Value);
  146. }
  147. private void OnAttachedUnequipAttempt(EntityUid uid, AttachedClothingComponent component, BeingUnequippedAttemptEvent args)
  148. {
  149. args.Cancel();
  150. }
  151. private void OnRemoveAttached(EntityUid uid, AttachedClothingComponent component, ComponentRemove args)
  152. {
  153. // if the attached component is being removed (maybe entity is being deleted?) we will just remove the
  154. // toggleable clothing component. This means if you had a hard-suit helmet that took too much damage, you would
  155. // still be left with a suit that was simply missing a helmet. There is currently no way to fix a partially
  156. // broken suit like this.
  157. if (!TryComp(component.AttachedUid, out ToggleableClothingComponent? toggleComp))
  158. return;
  159. if (toggleComp.LifeStage > ComponentLifeStage.Running)
  160. return;
  161. _actionsSystem.RemoveAction(toggleComp.ActionEntity);
  162. RemComp(component.AttachedUid, toggleComp);
  163. }
  164. /// <summary>
  165. /// Called if the helmet was unequipped, to ensure that it gets moved into the suit's container.
  166. /// </summary>
  167. private void OnAttachedUnequip(EntityUid uid, AttachedClothingComponent component, GotUnequippedEvent args)
  168. {
  169. // Let containers worry about it.
  170. if (_timing.ApplyingState)
  171. return;
  172. if (component.LifeStage > ComponentLifeStage.Running)
  173. return;
  174. if (!TryComp(component.AttachedUid, out ToggleableClothingComponent? toggleComp))
  175. return;
  176. if (toggleComp.LifeStage > ComponentLifeStage.Running)
  177. return;
  178. // As unequipped gets called in the middle of container removal, we cannot call a container-insert without causing issues.
  179. // So we delay it and process it during a system update:
  180. if (toggleComp.ClothingUid != null && toggleComp.Container != null)
  181. _containerSystem.Insert(toggleComp.ClothingUid.Value, toggleComp.Container);
  182. }
  183. /// <summary>
  184. /// Equip or unequip the toggleable clothing.
  185. /// </summary>
  186. private void OnToggleClothing(EntityUid uid, ToggleableClothingComponent component, ToggleClothingEvent args)
  187. {
  188. if (args.Handled)
  189. return;
  190. args.Handled = true;
  191. ToggleClothing(args.Performer, uid, component);
  192. }
  193. private void ToggleClothing(EntityUid user, EntityUid target, ToggleableClothingComponent component)
  194. {
  195. if (component.Container == null || component.ClothingUid == null)
  196. return;
  197. var parent = Transform(target).ParentUid;
  198. if (component.Container.ContainedEntity == null)
  199. _inventorySystem.TryUnequip(user, parent, component.Slot, force: true);
  200. else if (_inventorySystem.TryGetSlotEntity(parent, component.Slot, out var existing))
  201. {
  202. _popupSystem.PopupClient(Loc.GetString("toggleable-clothing-remove-first", ("entity", existing)),
  203. user, user);
  204. }
  205. else
  206. _inventorySystem.TryEquip(user, parent, component.ClothingUid.Value, component.Slot);
  207. }
  208. private void OnGetActions(EntityUid uid, ToggleableClothingComponent component, GetItemActionsEvent args)
  209. {
  210. if (component.ClothingUid != null
  211. && component.ActionEntity != null
  212. && (args.SlotFlags & component.RequiredFlags) == component.RequiredFlags)
  213. {
  214. args.AddAction(component.ActionEntity.Value);
  215. }
  216. }
  217. private void OnInit(EntityUid uid, ToggleableClothingComponent component, ComponentInit args)
  218. {
  219. component.Container = _containerSystem.EnsureContainer<ContainerSlot>(uid, component.ContainerId);
  220. }
  221. /// <summary>
  222. /// On map init, either spawn the appropriate entity into the suit slot, or if it already exists, perform some
  223. /// sanity checks. Also updates the action icon to show the toggled-entity.
  224. /// </summary>
  225. private void OnMapInit(EntityUid uid, ToggleableClothingComponent component, MapInitEvent args)
  226. {
  227. if (component.Container!.ContainedEntity is {} ent)
  228. {
  229. DebugTools.Assert(component.ClothingUid == ent, "Unexpected entity present inside of a toggleable clothing container.");
  230. return;
  231. }
  232. if (component.ClothingUid != null && component.ActionEntity != null)
  233. {
  234. DebugTools.Assert(Exists(component.ClothingUid), "Toggleable clothing is missing expected entity.");
  235. DebugTools.Assert(TryComp(component.ClothingUid, out AttachedClothingComponent? comp), "Toggleable clothing is missing an attached component");
  236. DebugTools.Assert(comp?.AttachedUid == uid, "Toggleable clothing uid mismatch");
  237. }
  238. else
  239. {
  240. var xform = Transform(uid);
  241. component.ClothingUid = Spawn(component.ClothingPrototype, xform.Coordinates);
  242. var attachedClothing = EnsureComp<AttachedClothingComponent>(component.ClothingUid.Value);
  243. attachedClothing.AttachedUid = uid;
  244. Dirty(component.ClothingUid.Value, attachedClothing);
  245. _containerSystem.Insert(component.ClothingUid.Value, component.Container, containerXform: xform);
  246. Dirty(uid, component);
  247. }
  248. if (_actionContainer.EnsureAction(uid, ref component.ActionEntity, out var action, component.Action))
  249. _actionsSystem.SetEntityIcon(component.ActionEntity.Value, component.ClothingUid, action);
  250. }
  251. }
  252. public sealed partial class ToggleClothingEvent : InstantActionEvent
  253. {
  254. }
  255. [Serializable, NetSerializable]
  256. public sealed partial class ToggleClothingDoAfterEvent : SimpleDoAfterEvent
  257. {
  258. }