HandsSystem.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. // SPDX-FileCopyrightText: 2021 Swept <sweptwastaken@protonmail.com>
  2. // SPDX-FileCopyrightText: 2021 Vera Aguilera Puerto <gradientvera@outlook.com>
  3. // SPDX-FileCopyrightText: 2022 Flipp Syder <76629141+vulppine@users.noreply.github.com>
  4. // SPDX-FileCopyrightText: 2022 Jacob Tong <10494922+ShadowCommander@users.noreply.github.com>
  5. // SPDX-FileCopyrightText: 2022 Jezithyr <Jezithyr.@gmail.com>
  6. // SPDX-FileCopyrightText: 2022 Jezithyr <Jezithyr@gmail.com>
  7. // SPDX-FileCopyrightText: 2022 Jezithyr <jmaster9999@gmail.com>
  8. // SPDX-FileCopyrightText: 2022 Paul <ritter.paul1@googlemail.com>
  9. // SPDX-FileCopyrightText: 2022 Paul Ritter <ritter.paul1@googlemail.com>
  10. // SPDX-FileCopyrightText: 2022 ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
  11. // SPDX-FileCopyrightText: 2022 Visne <39844191+Visne@users.noreply.github.com>
  12. // SPDX-FileCopyrightText: 2022 wrexbe <81056464+wrexbe@users.noreply.github.com>
  13. // SPDX-FileCopyrightText: 2022 wrexbe <wrexbe@protonmail.com>
  14. // SPDX-FileCopyrightText: 2023 DrSmugleaf <DrSmugleaf@users.noreply.github.com>
  15. // SPDX-FileCopyrightText: 2023 Kara <lunarautomaton6@gmail.com>
  16. // SPDX-FileCopyrightText: 2023 Menshin <Menshin@users.noreply.github.com>
  17. // SPDX-FileCopyrightText: 2023 Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
  18. // SPDX-FileCopyrightText: 2023 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
  19. // SPDX-FileCopyrightText: 2024 0x6273 <0x40@keemail.me>
  20. // SPDX-FileCopyrightText: 2024 AJCM-git <60196617+AJCM-git@users.noreply.github.com>
  21. // SPDX-FileCopyrightText: 2024 Ed <96445749+TheShuEd@users.noreply.github.com>
  22. // SPDX-FileCopyrightText: 2024 Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
  23. // SPDX-FileCopyrightText: 2024 Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
  24. // SPDX-FileCopyrightText: 2024 Piras314 <p1r4s@proton.me>
  25. // SPDX-FileCopyrightText: 2024 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
  26. // SPDX-FileCopyrightText: 2024 metalgearsloth <comedian_vs_clown@hotmail.com>
  27. // SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
  28. //
  29. // SPDX-License-Identifier: AGPL-3.0-or-later
  30. using System.Diagnostics.CodeAnalysis;
  31. using System.Linq;
  32. using Content.Shared._Shitmed.Body.Events; // Shitmed Change
  33. using Content.Client.DisplacementMap;
  34. using Content.Client.Examine;
  35. using Content.Client.Strip;
  36. using Content.Client.Verbs.UI;
  37. using Content.Shared.Body.Part; // Shitmed Change
  38. using Content.Shared.Hands;
  39. using Content.Shared.Hands.Components;
  40. using Content.Shared.Hands.EntitySystems;
  41. using Content.Shared.Inventory.VirtualItem;
  42. using Content.Shared.Item;
  43. using JetBrains.Annotations;
  44. using Robust.Client.GameObjects;
  45. using Robust.Client.Player;
  46. using Robust.Client.UserInterface;
  47. using Robust.Shared.Containers;
  48. using Robust.Shared.GameStates;
  49. using Robust.Shared.Player;
  50. namespace Content.Client.Hands.Systems
  51. {
  52. [UsedImplicitly]
  53. public sealed class HandsSystem : SharedHandsSystem
  54. {
  55. [Dependency] private readonly IPlayerManager _playerManager = default!;
  56. [Dependency] private readonly IUserInterfaceManager _ui = default!;
  57. [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
  58. [Dependency] private readonly StrippableSystem _stripSys = default!;
  59. [Dependency] private readonly ExamineSystem _examine = default!;
  60. [Dependency] private readonly DisplacementMapSystem _displacement = default!;
  61. public event Action<string, HandLocation>? OnPlayerAddHand;
  62. public event Action<string>? OnPlayerRemoveHand;
  63. public event Action<string?>? OnPlayerSetActiveHand;
  64. public event Action<HandsComponent>? OnPlayerHandsAdded;
  65. public event Action? OnPlayerHandsRemoved;
  66. public event Action<string, EntityUid>? OnPlayerItemAdded;
  67. public event Action<string, EntityUid>? OnPlayerItemRemoved;
  68. public event Action<string>? OnPlayerHandBlocked;
  69. public event Action<string>? OnPlayerHandUnblocked;
  70. public override void Initialize()
  71. {
  72. base.Initialize();
  73. SubscribeLocalEvent<HandsComponent, LocalPlayerAttachedEvent>(HandlePlayerAttached);
  74. SubscribeLocalEvent<HandsComponent, LocalPlayerDetachedEvent>(HandlePlayerDetached);
  75. SubscribeLocalEvent<HandsComponent, ComponentStartup>(OnHandsStartup);
  76. SubscribeLocalEvent<HandsComponent, ComponentShutdown>(OnHandsShutdown);
  77. SubscribeLocalEvent<HandsComponent, ComponentHandleState>(HandleComponentState);
  78. SubscribeLocalEvent<HandsComponent, VisualsChangedEvent>(OnVisualsChanged);
  79. SubscribeLocalEvent<HandsComponent, BodyPartRemovedEvent>(HandleBodyPartRemoved); // Shitmed Change
  80. SubscribeLocalEvent<HandsComponent, BodyPartDisabledEvent>(HandleBodyPartDisabled); // Shitmed Change
  81. OnHandSetActive += OnHandActivated;
  82. }
  83. #region StateHandling
  84. private void HandleComponentState(EntityUid uid, HandsComponent component, ref ComponentHandleState args)
  85. {
  86. if (args.Current is not HandsComponentState state)
  87. return;
  88. var handsModified = component.Hands.Count != state.Hands.Count;
  89. // we need to check that, even if we have the same amount, that the individual hands didn't change.
  90. if (!handsModified)
  91. {
  92. foreach (var hand in component.Hands.Values)
  93. {
  94. if (state.Hands.Contains(hand))
  95. continue;
  96. handsModified = true;
  97. break;
  98. }
  99. }
  100. var manager = EnsureComp<ContainerManagerComponent>(uid);
  101. if (handsModified)
  102. {
  103. List<Hand> addedHands = new();
  104. foreach (var hand in state.Hands)
  105. {
  106. if (component.Hands.ContainsKey(hand.Name))
  107. continue;
  108. var container = _containerSystem.EnsureContainer<ContainerSlot>(uid, hand.Name, manager);
  109. var newHand = new Hand(hand.Name, hand.Location, container);
  110. component.Hands.Add(hand.Name, newHand);
  111. addedHands.Add(newHand);
  112. }
  113. foreach (var name in component.Hands.Keys)
  114. {
  115. if (!state.HandNames.Contains(name))
  116. {
  117. RemoveHand(uid, name, component);
  118. }
  119. }
  120. component.SortedHands.Clear();
  121. component.SortedHands.AddRange(state.HandNames);
  122. var sorted = addedHands.OrderBy(hand => component.SortedHands.IndexOf(hand.Name));
  123. foreach (var hand in sorted)
  124. {
  125. AddHand(uid, hand, component);
  126. }
  127. }
  128. _stripSys.UpdateUi(uid);
  129. if (component.ActiveHand == null && state.ActiveHand == null)
  130. return; //edge case
  131. if (component.ActiveHand != null && state.ActiveHand != component.ActiveHand.Name)
  132. {
  133. SetActiveHand(uid, component.Hands[state.ActiveHand!], component);
  134. }
  135. }
  136. #endregion
  137. public void ReloadHandButtons()
  138. {
  139. if (!TryGetPlayerHands(out var hands))
  140. {
  141. return;
  142. }
  143. OnPlayerHandsAdded?.Invoke(hands);
  144. }
  145. public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? hands = null, bool log = true)
  146. {
  147. base.DoDrop(uid, hand, doDropInteraction, hands, log);
  148. if (TryComp(hand.HeldEntity, out SpriteComponent? sprite))
  149. sprite.RenderOrder = EntityManager.CurrentTick.Value;
  150. }
  151. public EntityUid? GetActiveHandEntity()
  152. {
  153. return TryGetPlayerHands(out var hands) ? hands.ActiveHandEntity : null;
  154. }
  155. /// <summary>
  156. /// Get the hands component of the local player
  157. /// </summary>
  158. public bool TryGetPlayerHands([NotNullWhen(true)] out HandsComponent? hands)
  159. {
  160. var player = _playerManager.LocalEntity;
  161. hands = null;
  162. return player != null && TryComp(player.Value, out hands);
  163. }
  164. /// <summary>
  165. /// Called when a user clicked on their hands GUI
  166. /// </summary>
  167. public void UIHandClick(HandsComponent hands, string handName)
  168. {
  169. if (!hands.Hands.TryGetValue(handName, out var pressedHand))
  170. return;
  171. if (hands.ActiveHand == null)
  172. return;
  173. var pressedEntity = pressedHand.HeldEntity;
  174. var activeEntity = hands.ActiveHand.HeldEntity;
  175. if (pressedHand == hands.ActiveHand && activeEntity != null)
  176. {
  177. // use item in hand
  178. // it will always be attack_self() in my heart.
  179. EntityManager.RaisePredictiveEvent(new RequestUseInHandEvent());
  180. return;
  181. }
  182. if (pressedHand != hands.ActiveHand && pressedEntity == null)
  183. {
  184. // change active hand
  185. EntityManager.RaisePredictiveEvent(new RequestSetHandEvent(handName));
  186. return;
  187. }
  188. if (pressedHand != hands.ActiveHand && pressedEntity != null && activeEntity != null)
  189. {
  190. // use active item on held item
  191. EntityManager.RaisePredictiveEvent(new RequestHandInteractUsingEvent(pressedHand.Name));
  192. return;
  193. }
  194. if (pressedHand != hands.ActiveHand && pressedEntity != null && activeEntity == null)
  195. {
  196. // move the item to the active hand
  197. EntityManager.RaisePredictiveEvent(new RequestMoveHandItemEvent(pressedHand.Name));
  198. }
  199. }
  200. /// <summary>
  201. /// Called when a user clicks on the little "activation" icon in the hands GUI. This is currently only used
  202. /// by storage (backpacks, etc).
  203. /// </summary>
  204. public void UIHandActivate(string handName)
  205. {
  206. EntityManager.RaisePredictiveEvent(new RequestActivateInHandEvent(handName));
  207. }
  208. public void UIInventoryExamine(string handName)
  209. {
  210. if (!TryGetPlayerHands(out var hands) ||
  211. !hands.Hands.TryGetValue(handName, out var hand) ||
  212. hand.HeldEntity is not { Valid: true } entity)
  213. {
  214. return;
  215. }
  216. _examine.DoExamine(entity);
  217. }
  218. /// <summary>
  219. /// Called when a user clicks on the little "activation" icon in the hands GUI. This is currently only used
  220. /// by storage (backpacks, etc).
  221. /// </summary>
  222. public void UIHandOpenContextMenu(string handName)
  223. {
  224. if (!TryGetPlayerHands(out var hands) ||
  225. !hands.Hands.TryGetValue(handName, out var hand) ||
  226. hand.HeldEntity is not { Valid: true } entity)
  227. {
  228. return;
  229. }
  230. _ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(entity);
  231. }
  232. public void UIHandAltActivateItem(string handName)
  233. {
  234. RaisePredictiveEvent(new RequestHandAltInteractEvent(handName));
  235. }
  236. #region visuals
  237. // Shitmed Change Start
  238. private void HideLayers(EntityUid uid, HandsComponent component, Entity<BodyPartComponent> part, SpriteComponent? sprite = null)
  239. {
  240. if (part.Comp.PartType != BodyPartType.Hand || !Resolve(uid, ref sprite, logMissing: false))
  241. return;
  242. var location = part.Comp.Symmetry switch
  243. {
  244. BodyPartSymmetry.None => HandLocation.Middle,
  245. BodyPartSymmetry.Left => HandLocation.Left,
  246. BodyPartSymmetry.Right => HandLocation.Right,
  247. _ => throw new ArgumentOutOfRangeException(nameof(part.Comp.Symmetry))
  248. };
  249. if (component.RevealedLayers.TryGetValue(location, out var revealedLayers))
  250. {
  251. foreach (var key in revealedLayers)
  252. sprite.RemoveLayer(key);
  253. revealedLayers.Clear();
  254. }
  255. }
  256. private void HandleBodyPartRemoved(EntityUid uid, HandsComponent component, ref BodyPartRemovedEvent args) => HideLayers(uid, component, args.Part);
  257. private void HandleBodyPartDisabled(EntityUid uid, HandsComponent component, ref BodyPartDisabledEvent args) => HideLayers(uid, component, args.Part);
  258. // Shitmed Change End
  259. protected override void HandleEntityInserted(EntityUid uid, HandsComponent hands, EntInsertedIntoContainerMessage args)
  260. {
  261. base.HandleEntityInserted(uid, hands, args);
  262. if (!hands.Hands.TryGetValue(args.Container.ID, out var hand))
  263. return;
  264. UpdateHandVisuals(uid, args.Entity, hand);
  265. _stripSys.UpdateUi(uid);
  266. if (uid != _playerManager.LocalEntity)
  267. return;
  268. OnPlayerItemAdded?.Invoke(hand.Name, args.Entity);
  269. if (HasComp<VirtualItemComponent>(args.Entity))
  270. OnPlayerHandBlocked?.Invoke(hand.Name);
  271. }
  272. protected override void HandleEntityRemoved(EntityUid uid, HandsComponent hands, EntRemovedFromContainerMessage args)
  273. {
  274. base.HandleEntityRemoved(uid, hands, args);
  275. if (!hands.Hands.TryGetValue(args.Container.ID, out var hand))
  276. return;
  277. UpdateHandVisuals(uid, args.Entity, hand);
  278. _stripSys.UpdateUi(uid);
  279. if (uid != _playerManager.LocalEntity)
  280. return;
  281. OnPlayerItemRemoved?.Invoke(hand.Name, args.Entity);
  282. if (HasComp<VirtualItemComponent>(args.Entity))
  283. OnPlayerHandUnblocked?.Invoke(hand.Name);
  284. }
  285. /// <summary>
  286. /// Update the players sprite with new in-hand visuals.
  287. /// </summary>
  288. private void UpdateHandVisuals(EntityUid uid, EntityUid held, Hand hand, HandsComponent? handComp = null, SpriteComponent? sprite = null)
  289. {
  290. if (!Resolve(uid, ref handComp, ref sprite, false))
  291. return;
  292. // visual update might involve changes to the entity's effective sprite -> need to update hands GUI.
  293. if (uid == _playerManager.LocalEntity)
  294. OnPlayerItemAdded?.Invoke(hand.Name, held);
  295. if (!handComp.ShowInHands)
  296. return;
  297. // Remove old layers. We could also just set them to invisible, but as items may add arbitrary layers, this
  298. // may eventually bloat the player with lots of layers.
  299. if (handComp.RevealedLayers.TryGetValue(hand.Location, out var revealedLayers))
  300. {
  301. foreach (var key in revealedLayers)
  302. {
  303. sprite.RemoveLayer(key);
  304. }
  305. revealedLayers.Clear();
  306. }
  307. else
  308. {
  309. revealedLayers = new();
  310. handComp.RevealedLayers[hand.Location] = revealedLayers;
  311. }
  312. if (hand.HeldEntity == null)
  313. {
  314. // the held item was removed.
  315. RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true);
  316. return;
  317. }
  318. var ev = new GetInhandVisualsEvent(uid, hand.Location);
  319. RaiseLocalEvent(held, ev);
  320. if (ev.Layers.Count == 0)
  321. {
  322. RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true);
  323. return;
  324. }
  325. // add the new layers
  326. foreach (var (key, layerData) in ev.Layers)
  327. {
  328. if (!revealedLayers.Add(key))
  329. {
  330. Log.Warning($"Duplicate key for in-hand visuals: {key}. Are multiple components attempting to modify the same layer? Entity: {ToPrettyString(held)}");
  331. continue;
  332. }
  333. var index = sprite.LayerMapReserveBlank(key);
  334. // In case no RSI is given, use the item's base RSI as a default. This cuts down on a lot of unnecessary yaml entries.
  335. if (layerData.RsiPath == null
  336. && layerData.TexturePath == null
  337. && sprite[index].Rsi == null)
  338. {
  339. if (TryComp<ItemComponent>(held, out var itemComponent) && itemComponent.RsiPath != null)
  340. sprite.LayerSetRSI(index, itemComponent.RsiPath);
  341. else if (TryComp(held, out SpriteComponent? clothingSprite))
  342. sprite.LayerSetRSI(index, clothingSprite.BaseRSI);
  343. }
  344. sprite.LayerSetData(index, layerData);
  345. //Add displacement maps
  346. if (handComp.HandDisplacement is not null)
  347. _displacement.TryAddDisplacement(handComp.HandDisplacement, sprite, index, key, revealedLayers);
  348. }
  349. RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true);
  350. }
  351. private void OnVisualsChanged(EntityUid uid, HandsComponent component, VisualsChangedEvent args)
  352. {
  353. // update hands visuals if this item is in a hand (rather then inventory or other container).
  354. if (component.Hands.TryGetValue(args.ContainerId, out var hand))
  355. {
  356. UpdateHandVisuals(uid, GetEntity(args.Item), hand, component);
  357. }
  358. }
  359. #endregion
  360. #region Gui
  361. private void HandlePlayerAttached(EntityUid uid, HandsComponent component, LocalPlayerAttachedEvent args)
  362. {
  363. OnPlayerHandsAdded?.Invoke(component);
  364. }
  365. private void HandlePlayerDetached(EntityUid uid, HandsComponent component, LocalPlayerDetachedEvent args)
  366. {
  367. OnPlayerHandsRemoved?.Invoke();
  368. }
  369. private void OnHandsStartup(EntityUid uid, HandsComponent component, ComponentStartup args)
  370. {
  371. if (_playerManager.LocalEntity == uid)
  372. OnPlayerHandsAdded?.Invoke(component);
  373. }
  374. private void OnHandsShutdown(EntityUid uid, HandsComponent component, ComponentShutdown args)
  375. {
  376. if (_playerManager.LocalEntity == uid)
  377. OnPlayerHandsRemoved?.Invoke();
  378. }
  379. #endregion
  380. private void AddHand(EntityUid uid, Hand newHand, HandsComponent? handsComp = null)
  381. {
  382. AddHand(uid, newHand.Name, newHand.Location, handsComp);
  383. }
  384. public override void AddHand(EntityUid uid, string handName, HandLocation handLocation, HandsComponent? handsComp = null)
  385. {
  386. base.AddHand(uid, handName, handLocation, handsComp);
  387. if (uid == _playerManager.LocalEntity)
  388. OnPlayerAddHand?.Invoke(handName, handLocation);
  389. if (handsComp == null)
  390. return;
  391. if (handsComp.ActiveHand == null)
  392. SetActiveHand(uid, handsComp.Hands[handName], handsComp);
  393. }
  394. public override void RemoveHand(EntityUid uid, string handName, HandsComponent? handsComp = null)
  395. {
  396. if (uid == _playerManager.LocalEntity && handsComp != null &&
  397. handsComp.Hands.ContainsKey(handName) && uid ==
  398. _playerManager.LocalEntity)
  399. {
  400. OnPlayerRemoveHand?.Invoke(handName);
  401. }
  402. base.RemoveHand(uid, handName, handsComp);
  403. }
  404. private void OnHandActivated(Entity<HandsComponent>? ent)
  405. {
  406. if (ent is not { } hand)
  407. return;
  408. if (_playerManager.LocalEntity != hand.Owner)
  409. return;
  410. if (hand.Comp.ActiveHand == null)
  411. {
  412. OnPlayerSetActiveHand?.Invoke(null);
  413. return;
  414. }
  415. OnPlayerSetActiveHand?.Invoke(hand.Comp.ActiveHand.Name);
  416. }
  417. }
  418. }