HandsSystem.cs 17 KB

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