ExamineSystem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. using System.Linq;
  2. using System.Numerics;
  3. using System.Threading;
  4. using Content.Client.Verbs;
  5. using Content.Shared.Examine;
  6. using Content.Shared.IdentityManagement;
  7. using Content.Shared.Input;
  8. using Content.Shared.Interaction.Events;
  9. using Content.Shared.Item;
  10. using Content.Shared.Verbs;
  11. using JetBrains.Annotations;
  12. using Robust.Client.GameObjects;
  13. using Robust.Client.Graphics;
  14. using Robust.Client.Player;
  15. using Robust.Client.UserInterface;
  16. using Robust.Client.UserInterface.Controls;
  17. using Robust.Shared.Input.Binding;
  18. using Robust.Shared.Map;
  19. using Robust.Shared.Utility;
  20. using static Content.Shared.Interaction.SharedInteractionSystem;
  21. using static Robust.Client.UserInterface.Controls.BoxContainer;
  22. using Direction = Robust.Shared.Maths.Direction;
  23. namespace Content.Client.Examine
  24. {
  25. [UsedImplicitly]
  26. public sealed class ExamineSystem : ExamineSystemShared
  27. {
  28. [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
  29. [Dependency] private readonly IPlayerManager _playerManager = default!;
  30. [Dependency] private readonly IEyeManager _eyeManager = default!;
  31. [Dependency] private readonly VerbSystem _verbSystem = default!;
  32. public const string StyleClassEntityTooltip = "entity-tooltip";
  33. private EntityUid _examinedEntity;
  34. private EntityUid _lastExaminedEntity;
  35. private Popup? _examineTooltipOpen;
  36. private ScreenCoordinates _popupPos;
  37. private CancellationTokenSource? _requestCancelTokenSource;
  38. private int _idCounter;
  39. public override void Initialize()
  40. {
  41. base.Initialize();
  42. UpdatesOutsidePrediction = true;
  43. SubscribeLocalEvent<GetVerbsEvent<ExamineVerb>>(AddExamineVerb);
  44. SubscribeNetworkEvent<ExamineSystemMessages.ExamineInfoResponseMessage>(OnExamineInfoResponse);
  45. SubscribeLocalEvent<ItemComponent, DroppedEvent>(OnExaminedItemDropped);
  46. CommandBinds.Builder
  47. .Bind(ContentKeyFunctions.ExamineEntity, new PointerInputCmdHandler(HandleExamine, outsidePrediction: true))
  48. .Register<ExamineSystem>();
  49. _idCounter = 0;
  50. }
  51. private void OnExaminedItemDropped(EntityUid item, ItemComponent comp, DroppedEvent args)
  52. {
  53. if (!args.User.Valid)
  54. return;
  55. if (_examineTooltipOpen == null)
  56. return;
  57. if (item == _examinedEntity && args.User == _playerManager.LocalEntity)
  58. CloseTooltip();
  59. }
  60. public override void Update(float frameTime)
  61. {
  62. if (_examineTooltipOpen is not {Visible: true}) return;
  63. if (!_examinedEntity.Valid || _playerManager.LocalEntity is not { } player) return;
  64. if (!CanExamine(player, _examinedEntity))
  65. CloseTooltip();
  66. }
  67. public override void Shutdown()
  68. {
  69. CommandBinds.Unregister<ExamineSystem>();
  70. base.Shutdown();
  71. }
  72. public override bool CanExamine(EntityUid examiner, MapCoordinates target, Ignored? predicate = null, EntityUid? examined = null, ExaminerComponent? examinerComp = null)
  73. {
  74. if (!Resolve(examiner, ref examinerComp, false))
  75. return false;
  76. if (examinerComp.SkipChecks)
  77. return true;
  78. if (examinerComp.CheckInRangeUnOccluded)
  79. {
  80. // TODO fix this. This should be using the examiner's eye component, not eye manager.
  81. var b = _eyeManager.GetWorldViewbounds();
  82. if (!b.Contains(target.Position))
  83. return false;
  84. }
  85. return base.CanExamine(examiner, target, predicate, examined, examinerComp);
  86. }
  87. private bool HandleExamine(in PointerInputCmdHandler.PointerInputCmdArgs args)
  88. {
  89. var entity = args.EntityUid;
  90. if (!args.EntityUid.IsValid() || !EntityManager.EntityExists(entity))
  91. {
  92. return false;
  93. }
  94. if (_playerManager.LocalEntity is not { } player ||
  95. !CanExamine(player, entity))
  96. {
  97. return false;
  98. }
  99. DoExamine(entity);
  100. return true;
  101. }
  102. private void AddExamineVerb(GetVerbsEvent<ExamineVerb> args)
  103. {
  104. if (!CanExamine(args.User, args.Target))
  105. return;
  106. // Basic examine verb.
  107. ExamineVerb verb = new();
  108. verb.Category = VerbCategory.Examine;
  109. verb.Priority = 10;
  110. // Center it on the entity if they use the verb instead.
  111. verb.Act = () => DoExamine(args.Target, false);
  112. verb.Text = Loc.GetString("examine-verb-name");
  113. verb.Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/examine.svg.192dpi.png"));
  114. verb.ShowOnExamineTooltip = false;
  115. verb.ClientExclusive = true;
  116. args.Verbs.Add(verb);
  117. }
  118. private void OnExamineInfoResponse(ExamineSystemMessages.ExamineInfoResponseMessage ev)
  119. {
  120. var player = _playerManager.LocalEntity;
  121. if (player == null)
  122. return;
  123. // Prevent updating a new tooltip.
  124. if (ev.Id != 0 && ev.Id != _idCounter)
  125. return;
  126. // Tooltips coming in from the server generally prioritize
  127. // opening at the old tooltip rather than the cursor/another entity,
  128. // since there's probably one open already if it's coming in from the server.
  129. var entity = GetEntity(ev.EntityUid);
  130. OpenTooltip(player.Value, entity, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget);
  131. UpdateTooltipInfo(player.Value, entity, ev.Message, ev.Verbs);
  132. }
  133. public override void SendExamineTooltip(EntityUid player, EntityUid target, FormattedMessage message, bool getVerbs, bool centerAtCursor)
  134. {
  135. OpenTooltip(player, target, centerAtCursor, false);
  136. UpdateTooltipInfo(player, target, message);
  137. }
  138. /// <summary>
  139. /// Opens the tooltip window and sets spriteview/name/etc, but does
  140. /// not fill it with information. This is done when the server sends examine info/verbs,
  141. /// or immediately if it's entirely clientside.
  142. /// </summary>
  143. public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCursor=true, bool openAtOldTooltip=true, bool knowTarget = true)
  144. {
  145. // Close any examine tooltip that might already be opened
  146. // Before we do that, save its position. We'll prioritize opening any new popups there if
  147. // openAtOldTooltip is true.
  148. ScreenCoordinates? oldTooltipPos = _examineTooltipOpen != null ? _popupPos : null;
  149. CloseTooltip();
  150. // cache entity for Update function
  151. _examinedEntity = target;
  152. const float minWidth = 300;
  153. if (openAtOldTooltip && oldTooltipPos != null)
  154. {
  155. _popupPos = oldTooltipPos.Value;
  156. }
  157. else if (centeredOnCursor)
  158. {
  159. _popupPos = _userInterfaceManager.MousePositionScaled;
  160. }
  161. else
  162. {
  163. _popupPos = _eyeManager.CoordinatesToScreen(Transform(target).Coordinates);
  164. _popupPos = _userInterfaceManager.ScreenToUIPosition(_popupPos);
  165. }
  166. // Actually open the tooltip.
  167. _examineTooltipOpen = new Popup { MaxWidth = 400 };
  168. _userInterfaceManager.ModalRoot.AddChild(_examineTooltipOpen);
  169. var panel = new PanelContainer() { Name = "ExaminePopupPanel" };
  170. panel.AddStyleClass(StyleClassEntityTooltip);
  171. panel.ModulateSelfOverride = Color.LightGray.WithAlpha(0.90f);
  172. _examineTooltipOpen.AddChild(panel);
  173. var vBox = new BoxContainer
  174. {
  175. Name = "ExaminePopupVbox",
  176. Orientation = LayoutOrientation.Vertical,
  177. MaxWidth = _examineTooltipOpen.MaxWidth
  178. };
  179. panel.AddChild(vBox);
  180. var hBox = new BoxContainer
  181. {
  182. Orientation = LayoutOrientation.Horizontal,
  183. SeparationOverride = 5,
  184. Margin = new Thickness(6, 0, 6, 0)
  185. };
  186. vBox.AddChild(hBox);
  187. if (EntityManager.HasComponent<SpriteComponent>(target))
  188. {
  189. var spriteView = new SpriteView
  190. {
  191. OverrideDirection = Direction.South,
  192. SetSize = new Vector2(32, 32)
  193. };
  194. spriteView.SetEntity(target);
  195. hBox.AddChild(spriteView);
  196. }
  197. if (knowTarget)
  198. {
  199. var itemName = FormattedMessage.EscapeText(Identity.Name(target, EntityManager, player));
  200. var labelMessage = FormattedMessage.FromMarkupPermissive($"[bold]{itemName}[/bold]");
  201. var label = new RichTextLabel();
  202. label.SetMessage(labelMessage);
  203. hBox.AddChild(label);
  204. }
  205. else
  206. {
  207. var label = new RichTextLabel();
  208. label.SetMessage(FormattedMessage.FromMarkupOrThrow("[bold]???[/bold]"));
  209. hBox.AddChild(label);
  210. }
  211. panel.Measure(Vector2Helpers.Infinity);
  212. var size = Vector2.Max(new Vector2(minWidth, 0), panel.DesiredSize);
  213. _examineTooltipOpen.Open(UIBox2.FromDimensions(_popupPos.Position, size));
  214. }
  215. /// <summary>
  216. /// Fills the examine tooltip with a message and buttons if applicable.
  217. /// </summary>
  218. public void UpdateTooltipInfo(EntityUid player, EntityUid target, FormattedMessage message, List<Verb>? verbs=null)
  219. {
  220. var vBox = _examineTooltipOpen?.GetChild(0).GetChild(0);
  221. if (vBox == null)
  222. {
  223. return;
  224. }
  225. foreach (var msg in message.Nodes)
  226. {
  227. if (msg.Name != null)
  228. continue;
  229. var text = msg.Value.StringValue ?? "";
  230. if (string.IsNullOrWhiteSpace(text))
  231. continue;
  232. var richLabel = new RichTextLabel() { Margin = new Thickness(4, 4, 0, 4)};
  233. richLabel.SetMessage(message);
  234. vBox.AddChild(richLabel);
  235. break;
  236. }
  237. verbs ??= new List<Verb>();
  238. var totalVerbs = _verbSystem.GetLocalVerbs(target, player, typeof(ExamineVerb));
  239. totalVerbs.UnionWith(verbs);
  240. AddVerbsToTooltip(totalVerbs);
  241. }
  242. private void AddVerbsToTooltip(IEnumerable<Verb> verbs)
  243. {
  244. if (_examineTooltipOpen == null)
  245. return;
  246. var buttonsHBox = new BoxContainer
  247. {
  248. Name = "ExamineButtonsHBox",
  249. Orientation = LayoutOrientation.Horizontal,
  250. HorizontalAlignment = Control.HAlignment.Right,
  251. VerticalAlignment = Control.VAlignment.Bottom,
  252. };
  253. // Examine button time
  254. foreach (var verb in verbs)
  255. {
  256. if (verb is not ExamineVerb examine)
  257. continue;
  258. if (examine.Icon == null)
  259. continue;
  260. if (!examine.ShowOnExamineTooltip)
  261. continue;
  262. var button = new ExamineButton(examine);
  263. button.OnPressed += VerbButtonPressed;
  264. buttonsHBox.AddChild(button);
  265. }
  266. var vbox = _examineTooltipOpen?.GetChild(0).GetChild(0);
  267. if (vbox == null)
  268. {
  269. buttonsHBox.Dispose();
  270. return;
  271. }
  272. // Remove any existing buttons hbox, in case we generated it from the client
  273. // then received ones from the server
  274. var hbox = vbox.Children.Where(c => c.Name == "ExamineButtonsHBox").ToArray();
  275. if (hbox.Any())
  276. {
  277. vbox.Children.Remove(hbox.First());
  278. }
  279. vbox.AddChild(buttonsHBox);
  280. }
  281. public void VerbButtonPressed(BaseButton.ButtonEventArgs obj)
  282. {
  283. if (obj.Button is ExamineButton button)
  284. {
  285. _verbSystem.ExecuteVerb(_examinedEntity, button.Verb);
  286. if (button.Verb.CloseMenu ?? button.Verb.CloseMenuDefault)
  287. CloseTooltip();
  288. }
  289. }
  290. public void DoExamine(EntityUid entity, bool centeredOnCursor = true, EntityUid? userOverride = null)
  291. {
  292. var playerEnt = userOverride ?? _playerManager.LocalEntity;
  293. if (playerEnt == null)
  294. return;
  295. FormattedMessage message;
  296. OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false);
  297. // Always update tooltip info from client first.
  298. // If we get it wrong, server will correct us later anyway.
  299. // This will usually be correct (barring server-only components, which generally only adds, not replaces text)
  300. message = GetExamineText(entity, playerEnt);
  301. UpdateTooltipInfo(playerEnt.Value, entity, message);
  302. if (!IsClientSide(entity))
  303. {
  304. // Ask server for extra examine info.
  305. if (entity != _lastExaminedEntity)
  306. _idCounter += 1;
  307. if (_idCounter == int.MaxValue)
  308. _idCounter = 0;
  309. RaiseNetworkEvent(new ExamineSystemMessages.RequestExamineInfoMessage(GetNetEntity(entity), _idCounter, true));
  310. }
  311. RaiseLocalEvent(entity, new ClientExaminedEvent(entity, playerEnt.Value));
  312. _lastExaminedEntity = entity;
  313. }
  314. private void CloseTooltip()
  315. {
  316. if (_examineTooltipOpen != null)
  317. {
  318. foreach (var control in _examineTooltipOpen.Children)
  319. {
  320. if (control is ExamineButton button)
  321. {
  322. button.OnPressed -= VerbButtonPressed;
  323. }
  324. }
  325. _examineTooltipOpen.Dispose();
  326. _examineTooltipOpen = null;
  327. }
  328. if (_requestCancelTokenSource != null)
  329. {
  330. _requestCancelTokenSource.Cancel();
  331. _requestCancelTokenSource = null;
  332. }
  333. }
  334. }
  335. /// <summary>
  336. /// An entity was examined on the client.
  337. /// </summary>
  338. public sealed class ClientExaminedEvent : EntityEventArgs
  339. {
  340. /// <summary>
  341. /// The entity performing the examining.
  342. /// </summary>
  343. public readonly EntityUid Examiner;
  344. /// <summary>
  345. /// Entity being examined, for broadcast event purposes.
  346. /// </summary>
  347. public readonly EntityUid Examined;
  348. public ClientExaminedEvent(EntityUid examined, EntityUid examiner)
  349. {
  350. Examined = examined;
  351. Examiner = examiner;
  352. }
  353. }
  354. }