1
0

ExamineSystem.cs 16 KB

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