Client LuaCsForBarotrauma
SocialOverlay.cs
1 #nullable enable
2 using Barotrauma.Eos;
5 using Barotrauma.Steam;
6 using Microsoft.Xna.Framework;
7 using Microsoft.Xna.Framework.Graphics;
8 using System;
9 using System.Collections.Generic;
10 using System.Collections.Immutable;
11 using System.Linq;
12 using System.Threading.Tasks;
13 
14 namespace Barotrauma;
15 
16 sealed class SocialOverlay : IDisposable
17 {
18  public static readonly LocalizedString ShortcutBindText = TextManager.Get("SocialOverlayShortcutBind");
19 
20  public static SocialOverlay? Instance { get; private set; }
21  public static void Init()
22  {
23  Instance ??= new SocialOverlay();
24  }
25 
26  private sealed class NotificationHandler
27  {
28  public record Notification(
29  DateTime ReceiveTime,
30  GUIComponent GuiElement);
31  private readonly List<Notification> notifications = new();
32 
33  private static readonly TimeSpan notificationDuration = TimeSpan.FromSeconds(8);
34  private static readonly TimeSpan notificationEasingTimeSpan = TimeSpan.FromSeconds(0.5);
35  public readonly GUIFrame NotificationContainer =
36  new GUIFrame(new RectTransform((0.4f, 0.15f), GUI.Canvas, Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight), style: null)
37  {
38  CanBeFocused = false
39  };
40 
41  public void Update()
42  {
43  var now = DateTime.Now;
44  float cumulativeNotificationOffset = 0;
45 
46  for (int i = notifications.Count - 1; i >= 0; i--)
47  {
48  var notification = notifications[i];
49 
50  var expiryTime = notification.ReceiveTime + notificationDuration;
51  if (now > expiryTime
52  || notification.GuiElement.Parent is null)
53  {
54  RemoveNotification(notification);
55  continue;
56  }
57 
58  TimeSpan diffToStart = now - notification.ReceiveTime;
59  TimeSpan diffToEnd = expiryTime - now;
60 
61  float offsetToAdd = 1f;
62  offsetToAdd = Math.Min(
63  offsetToAdd,
64  (float)diffToStart.TotalSeconds / (float)notificationEasingTimeSpan.TotalSeconds);
65  offsetToAdd = Math.Min(
66  offsetToAdd,
67  (float)diffToEnd.TotalSeconds / (float)notificationEasingTimeSpan.TotalSeconds);
68 
69  offsetToAdd = Math.Max(offsetToAdd, 0f);
70 
71  cumulativeNotificationOffset += offsetToAdd;
72 
73  notification.GuiElement.RectTransform.RelativeOffset = (0, cumulativeNotificationOffset - 1f);
74  }
75  }
76 
77  public void AddToGuiUpdateList()
78  {
79  NotificationContainer.AddToGUIUpdateList();
80  }
81 
82  public void AddNotification(Notification notification)
83  {
84  notifications.Add(notification);
85  }
86 
87  public void RemoveNotification(Notification notification)
88  {
89  notifications.Remove(notification);
90  NotificationContainer.RemoveChild(notification.GuiElement);
91  }
92  }
93 
94  private sealed class InviteHandler : IDisposable
95  {
96  private readonly record struct Invite(
97  FriendInfo Sender,
98  DateTime ReceiveTime,
99  Option<NotificationHandler.Notification> NotificationOption);
100 
101  private readonly SocialOverlay socialOverlay;
102  private readonly FriendProvider friendProvider;
103  private readonly NotificationHandler notificationHandler;
104 
105  private readonly List<Invite> invites = new List<Invite>();
106  private static readonly TimeSpan inviteDuration = TimeSpan.FromMinutes(5);
107  private readonly Identifier inviteReceivedEventIdentifier;
108 
109  public InviteHandler(
110  SocialOverlay inSocialOverlay,
111  FriendProvider inFriendProvider,
112  NotificationHandler inNotificationHandler)
113  {
114  socialOverlay = inSocialOverlay;
115  friendProvider = inFriendProvider;
116  notificationHandler = inNotificationHandler;
117 
118  inviteReceivedEventIdentifier = GetHashCode().ToIdentifier();
119  EosInterface.Presence.OnInviteReceived.Register(
120  identifier: inviteReceivedEventIdentifier,
121  OnEosInviteReceived);
122  Steamworks.SteamFriends.OnChatMessage += OnSteamChatMsgReceived;
123  }
124 
125  private void OnSteamChatMsgReceived(Steamworks.Friend steamFriend, string msgType, string msgContent)
126  {
127  if (!string.Equals(msgType, "InviteGame")) { return; }
128 
129  var friendId = new SteamId(steamFriend.Id);
130  TaskPool.Add(
131  $"ReceivedInviteFrom{friendId}",
132  friendProvider.RetrieveFriend(friendId),
133  t =>
134  {
135  if (!t.TryGetResult(out Option<FriendInfo> friendInfoOption)) { return; }
136  if (!friendInfoOption.TryUnwrap(out var friendInfo)) { return; }
137  RegisterInvite(friendInfo, showNotification: false);
138  });
139  }
140 
141  private void OnEosInviteReceived(EosInterface.Presence.ReceiveInviteInfo info)
142  {
143  TaskPool.Add(
144  $"ReceivedInviteFrom{info.SenderId}",
145  friendProvider.RetrieveFriendWithAvatar(info.SenderId, notificationHandler.NotificationContainer.Rect.Height),
146  t =>
147  {
148  if (!t.TryGetResult(out Option<FriendInfo> friendInfoOption)) { return; }
149  if (!friendInfoOption.TryUnwrap(out var friendInfo)) { return; }
150  RegisterInvite(friendInfo, showNotification: true);
151  });
152  }
153 
154  public bool HasInviteFrom(AccountId sender)
155  => invites.Any(invite => invite.Sender.Id == sender);
156 
157  public void ClearInvitesFrom(AccountId sender)
158  {
159  foreach (var invite in invites)
160  {
161  if (invite.Sender.Id == sender && invite.NotificationOption.TryUnwrap(out var notification))
162  {
163  notificationHandler.RemoveNotification(notification);
164  }
165  }
166  invites.RemoveAll(invite => invite.Sender.Id == sender);
167 
168  if (sender is not EpicAccountId friendEpicId) { return; }
169 
170  var selfEpicIds = EosInterface.IdQueries.GetLoggedInEpicIds();
171  if (selfEpicIds.Length == 0) { return; }
172 
173  var selfEpicId = selfEpicIds[0];
174  EosInterface.Presence.DeclineInvite(selfEpicId, friendEpicId);
175  }
176 
177  public void Update()
178  {
179  var now = DateTime.Now;
180 
181  for (int i = invites.Count - 1; i >= 0; i--)
182  {
183  var invite = invites[i];
184 
185  var expiryTime = invite.ReceiveTime + inviteDuration;
186  if (now > expiryTime)
187  {
188  if (invite.NotificationOption.TryUnwrap(out var notification))
189  {
190  notificationHandler.RemoveNotification(notification);
191  }
192  invites.RemoveAt(i);
193  }
194  }
195  }
196 
197  private void RegisterInvite(FriendInfo senderInfo, bool showNotification)
198  {
199  var now = DateTime.Now;
200 
201  var invite = new Invite(
202  Sender: senderInfo,
203  ReceiveTime: now,
204  NotificationOption: Option.None);
205 
206  if (showNotification)
207  {
208  var baseButton = new GUIButton(
209  new RectTransform(Vector2.One, notificationHandler.NotificationContainer.RectTransform, Anchor.BottomRight)
210  {
211  RelativeOffset = (0, -1)
212  }, style: "SocialOverlayPopup");
213  baseButton.Frame.OutlineThickness = 1f;
214 
215  var topLayout = new GUILayoutGroup(new RectTransform(Vector2.One, baseButton.RectTransform), isHorizontal: true)
216  {
217  Stretch = true,
218  RelativeSpacing = 0.05f
219  };
220 
221  var avatarContainer = new GUIFrame(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: null);
222 
223  var avatarComponent = new GUICustomComponent(
224  new RectTransform(
225  Vector2.One * 0.8f,
226  avatarContainer.RectTransform,
227  Anchor.Center,
228  scaleBasis: ScaleBasis.BothHeight),
229  onDraw: (sb, component) =>
230  {
231  if (!senderInfo.Avatar.TryUnwrap(out var avatar)) { return; }
232 
233  var rect = component.Rect;
234  sb.Draw(avatar.Texture, rect, avatar.Texture.Bounds, Color.White);
235  });
236 
237  var textLayout = new GUILayoutGroup(new RectTransform(Vector2.One, topLayout.RectTransform))
238  {
239  Stretch = true
240  };
241 
242  void addPadding()
243  => new GUIFrame(new RectTransform((1.0f, 0.2f), textLayout.RectTransform), style: null);
244 
245  void addText(LocalizedString text, GUIFont font)
246  => new GUITextBlock(new RectTransform((1.0f, 0.2f), textLayout.RectTransform), text, font: font);
247 
248  addPadding();
249  addText(senderInfo.Name, GUIStyle.SubHeadingFont);
250  addText(TextManager.Get("InvitedYou"), GUIStyle.Font);
251  addPadding();
252  addText(TextManager.GetWithVariable("ClickHereOrPressSocialOverlayShortcut", "[shortcut]", ShortcutBindText), GUIStyle.SmallFont);
253  addPadding();
254 
255  var notification = new NotificationHandler.Notification(
256  ReceiveTime: now,
257  GuiElement: baseButton);
258  baseButton.OnClicked = (_, _) =>
259  {
260  socialOverlay.IsOpen = true;
261  notificationHandler.RemoveNotification(notification);
262  return false;
263  };
264  baseButton.OnSecondaryClicked = (_, _) =>
265  {
266  notificationHandler.RemoveNotification(notification);
267  return false;
268  };
269 
270  notificationHandler.AddNotification(notification);
271 
272  invite = invite with { NotificationOption = Option.Some(notification) };
273  }
274 
275  invites.Add(invite);
276  }
277 
278  public void Dispose()
279  {
280  EosInterface.Presence.OnInviteReceived.Deregister(inviteReceivedEventIdentifier);
281  Steamworks.SteamFriends.OnChatMessage -= OnSteamChatMsgReceived;
282  }
283  }
284 
285  private readonly NotificationHandler notificationHandler;
286  private readonly InviteHandler inviteHandler;
287  private readonly GUIFrame background;
288  private readonly GUIButton linkHint;
289  private readonly GUILayoutGroup contentLayout;
290 
291  private readonly GUIFrame selectedFriendInfoFrame;
292 
293  private const float WidthToHeightRatio = 7f;
294 
295  private readonly TimeSpan refreshInterval = TimeSpan.FromSeconds(30);
296  private DateTime lastRefreshTime;
297 
298  public bool IsOpen;
299 
300  private static RectTransform CreateRowRectT(GUIComponent parent, float heightScale = 1f)
301  => new RectTransform((1.0f, heightScale / WidthToHeightRatio), parent.RectTransform, scaleBasis: ScaleBasis.BothWidth);
302 
303  private static GUILayoutGroup CreateRowLayout(GUIComponent parent, float heightScale = 1f)
304  {
305  var rowLayout = new GUILayoutGroup(CreateRowRectT(parent, heightScale), isHorizontal: true)
306  {
307  Stretch = true
308  };
309 
310  new GUICustomComponent(new RectTransform(Vector2.Zero, rowLayout.RectTransform),
311  onUpdate: (f, component) =>
312  {
313  rowLayout.RectTransform.NonScaledSize = calculateSize();
314  });
315 
316  return rowLayout;
317 
318  Point calculateSize() => new Point(parent.Rect.Width, (int)((parent.Rect.Width * heightScale) / WidthToHeightRatio));
319  }
320 
321  private readonly struct PlayerRow
322  {
323  public readonly GUIFrame AvatarContainer;
324  public readonly GUIFrame InfoContainer;
325  public readonly FriendInfo FriendInfo;
326 
327  internal PlayerRow(FriendInfo friendInfo, GUILayoutGroup containerLayout, bool invitedYou, IEnumerable<LocalizedString>? metadataText = null)
328  {
329  FriendInfo = friendInfo;
330  AvatarContainer = new GUIFrame(new RectTransform(Vector2.One, containerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: null);
331  InfoContainer = new GUIFrame(new RectTransform(Vector2.One, containerLayout.RectTransform, scaleBasis: ScaleBasis.Normal), style: null);
332 
333  friendInfo.RetrieveOrInheritAvatar(Option.None, AvatarContainer.Rect.Height);
334 
335  var avatarBackground = new GUIFrame(new RectTransform(Vector2.One * 0.9f, AvatarContainer.RectTransform, Anchor.Center),
336  style: invitedYou
337  ? "FriendInvitedYou"
338  : $"Friend{friendInfo.CurrentStatus}");
339 
340  var textLayout = new GUILayoutGroup(new RectTransform(Vector2.One, InfoContainer.RectTransform)) { Stretch = true };
341  var textBlocks = new List<GUITextBlock>();
342 
343  addTextLayoutPadding();
344  addTextBlock(friendInfo.Name, font: GUIStyle.SubHeadingFont);
345  metadataText ??= new[] { friendInfo.StatusText };
346  foreach (var line in metadataText)
347  {
348  addTextBlock(line, font: GUIStyle.Font);
349  }
350  addTextLayoutPadding();
351 
352  new GUICustomComponent(new RectTransform(Vector2.One, avatarBackground.RectTransform),
353  onUpdate: updateTextAlignments,
354  onDraw: drawAvatar);
355 
356  if (invitedYou)
357  {
358  var inviteIcon = new GUIImage(new RectTransform(new Vector2(0.5f), avatarBackground.RectTransform, Anchor.TopRight, Pivot.Center)
359  { RelativeOffset = Vector2.One * 0.15f }, style: "InviteNotification")
360  {
361  ToolTip = TextManager.Get("InviteNotification")
362  };
363  inviteIcon.OnAddedToGUIUpdateList += (GUIComponent component) =>
364  {
365  if (component.FlashTimer <= 0.0f)
366  {
367  component.Flash(GUIStyle.Green, useCircularFlash: true);
368  component.Pulsate(Vector2.One, Vector2.One * 1.5f, 0.5f);
369  }
370  };
371  }
372 
373  void addTextLayoutPadding()
374  => new GUIFrame(new RectTransform(Vector2.One, textLayout.RectTransform), style: null);
375 
376  void addTextBlock(LocalizedString text, GUIFont font)
377  => textBlocks.Add(new GUITextBlock(new RectTransform(Vector2.One, textLayout.RectTransform), text,
378  textColor: Color.White, font: font, textAlignment: Alignment.CenterLeft)
379  {
380  ForceUpperCase = ForceUpperCase.No,
381  TextColor = avatarBackground.Color,
382  HoverTextColor = avatarBackground.HoverColor,
383  SelectedTextColor = avatarBackground.SelectedColor,
384  PressedColor = avatarBackground.PressedColor,
385  });
386 
387  void updateTextAlignments(float deltaTime, GUICustomComponent component)
388  {
389  foreach (var textBlock in textBlocks)
390  {
391  int height = (int)textBlock.Font.LineHeight + GUI.IntScale(2);
392  textBlock.RectTransform.NonScaledSize =
393  (textBlock.RectTransform.NonScaledSize.X, height);
394  }
395  textLayout.NeedsToRecalculate = true;
396  }
397 
398  void drawAvatar(SpriteBatch sb, GUICustomComponent component)
399  {
400  if (!friendInfo.Avatar.TryUnwrap(out var avatar)) { return; }
401  Rectangle rect = component.Rect;
402  rect.Inflate(-GUI.IntScale(4f), -GUI.IntScale(4f));
403  sb.Draw(avatar.Texture, rect, Color.White);
404  }
405  }
406  }
407 
408  private readonly FriendProvider friendProvider;
409 
410  private readonly GUILayoutGroup selfPlayerRowLayout;
411 
412  private readonly GUIButton? eosConfigButton;
413  private readonly GUILayoutGroup? eosStatusTextContainer;
414  private EosInterface.Core.Status eosLastKnownStatus;
415 
416  private readonly GUIListBox friendPlayerListBox;
417  private readonly List<PlayerRow> friendPlayerRows = new List<PlayerRow>();
418 
419  private void RecreateSelfPlayerRow()
420  {
421  if (SteamManager.GetSteamId().TryUnwrap(out var steamId))
422  {
423  selfPlayerRowLayout.ClearChildren();
424  _ = new PlayerRow(
425  new FriendInfo(
426  name: SteamManager.GetUsername(),
427  id: steamId,
428  status: FriendStatus.PlayingBarotrauma,
429  serverName: "",
430  connectCommand: Option.None,
431  provider: friendProvider),
432  selfPlayerRowLayout,
433  invitedYou: false);
434  }
435  else if (EosInterface.IdQueries.IsLoggedIntoEosConnect)
436  {
437  static async Task<Option<EosInterface.EgsFriend>> GetEpicAccountInfo()
438  {
439  if (!EosAccount.SelfAccountIds.OfType<EpicAccountId>().FirstOrNone().TryUnwrap(out var epicAccountId))
440  {
441  return Option.None;
442  }
443 
444  var selfUserInfoResult = await EosInterface.Friends.GetSelfUserInfo(epicAccountId);
445 
446  if (!selfUserInfoResult.TryUnwrapSuccess(out var selfUserInfo))
447  {
448  return Option.None;
449  }
450 
451  return Option.Some(selfUserInfo);
452  }
453 
454  TaskPool.Add(
455  "GetEpicAccountIdForSelfPlayerRow",
456  GetEpicAccountInfo(),
457  t =>
458  {
459  if (!t.TryGetResult(out Option<EosInterface.EgsFriend> userInfoOption)
460  || !userInfoOption.TryUnwrap(out var userInfo))
461  {
462  return;
463  }
464 
465  selfPlayerRowLayout.ClearChildren();
466  _ = new PlayerRow(
467  new FriendInfo(
468  name: userInfo.DisplayName,
469  id: userInfo.EpicAccountId,
470  status: FriendStatus.PlayingBarotrauma,
471  serverName: "",
472  connectCommand: Option.None,
473  provider: friendProvider),
474  selfPlayerRowLayout,
475  invitedYou: false);
476  });
477  }
478  }
479 
480  private SocialOverlay()
481  {
482  background =
483  new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: "SocialOverlayBackground");
484  var rightSideLayout =
485  new GUILayoutGroup(
486  new RectTransform((0.9f, 1.0f), background.RectTransform, Anchor.CenterRight,
487  scaleBasis: ScaleBasis.BothHeight), isHorizontal: true, childAnchor: Anchor.BottomLeft);
488 
489  linkHint = new GUIButton(new RectTransform((0.5f, 0.9f / WidthToHeightRatio), rightSideLayout.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.BothWidth), style: "FriendsButton")
490  {
491  OnClicked = (btn, _) =>
492  {
493  eosConfigButton?.Flash(GUIStyle.Green);
494  EosSteamPrimaryLogin.IsNewEosPlayer = false;
495  btn.Visible = false;
496  return false;
497  },
498  Visible = false
499  };
500  _ = new GUITextBlock(new RectTransform(Vector2.One * 0.95f, linkHint.RectTransform, Anchor.Center),
501  text: TextManager.Get("EosSettings.RecommendLinkingToEpicAccount"),
502  wrap: true,
503  style: "FriendsButton");
504 
505  var content = new GUIFrame(
506  new RectTransform((0.5f, 1.0f), rightSideLayout.RectTransform),
507  style: "SocialOverlayFriendsList");
508 
509  _ = new GUIButton(
510  new RectTransform(Vector2.One * 0.08f, content.RectTransform, Anchor.TopLeft, Pivot.TopRight,
511  scaleBasis: ScaleBasis.BothWidth)
512  {
513  RelativeOffset = (-0.03f, 0.015f)
514  },
515  style: "SocialOverlayCloseButton")
516  {
517  OnClicked = (_, _) =>
518  {
519  IsOpen = false;
520  return false;
521  }
522  };
523 
524  friendProvider = new CompositeFriendProvider(new SteamFriendProvider(), new EpicFriendProvider());
525 
526  notificationHandler = new NotificationHandler();
527  inviteHandler = new InviteHandler(
528  inSocialOverlay: this,
529  inFriendProvider: friendProvider,
530  inNotificationHandler: notificationHandler);
531 
532  selectedFriendInfoFrame = new GUIFrame(new RectTransform((0.25f, 0.28f), background.RectTransform,
533  Anchor.TopRight, scaleBasis: ScaleBasis.BothHeight), style: "SocialOverlayPopup")
534  {
535  OutlineThickness = 1f,
536  Visible = false
537  };
538 
539  contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, content.RectTransform)) { Stretch = true };
540 
541  selfPlayerRowLayout = CreateRowLayout(contentLayout);
542  RecreateSelfPlayerRow();
543 
544  friendPlayerListBox =
545  new GUIListBox(new RectTransform(Vector2.One, contentLayout.RectTransform), style: null)
546  {
547  OnSelected = (component, userData) =>
548  {
549  if (userData is not FriendInfo friendInfo) { return false; }
550  selectedFriendInfoFrame.Visible = true;
551  selectedFriendInfoFrame.RectTransform.AbsoluteOffset = (
552  X: background.Rect.Right - component.Rect.X,
553  Y: Math.Clamp(
554  value: component.Rect.Center.Y - selectedFriendInfoFrame.Rect.Height / 2,
555  min: 0,
556  max: background.Rect.Bottom - selectedFriendInfoFrame.Rect.Height));
557  PopulateSelectedFriendInfoFrame(friendInfo);
558  return true;
559  }
560  };
561  friendPlayerListBox.ScrollBar.OnMoved += (_, _) => { friendPlayerListBox.Deselect(); return true; };
562 
563  if (SteamManager.IsInitialized)
564  {
565  var eosConfigRowLayout = CreateRowLayout(contentLayout, heightScale: 1.5f);
566  eosConfigRowLayout.ChildAnchor = Anchor.CenterLeft;
567 
568  eosConfigButton = new GUIButton(
569  new RectTransform(Vector2.One * 0.8f, eosConfigRowLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight),
570  style: null)
571  {
572  Enabled = GameMain.NetworkMember == null,
573  OnClicked = (_, _) => { ShowEosSettingsMenu(); return true; }
574  };
575  new GUIFrame(new RectTransform(Vector2.One * 0.5f, eosConfigButton.RectTransform, Anchor.Center), style: "GUIButtonSettings")
576  {
577  CanBeFocused = false
578  };
579 
580  eosStatusTextContainer = new GUILayoutGroup(new RectTransform(Vector2.One, eosConfigRowLayout.RectTransform));
581  RefreshEosStatusText();
582  }
583 
584  RefreshFriendList();
585  }
586 
588  {
589  if (IsOpen) { return; }
590 
591  var baseButton = new GUIButton(
592  new RectTransform(Vector2.One, notificationHandler.NotificationContainer.RectTransform, Anchor.BottomRight)
593  {
594  RelativeOffset = (0, -1)
595  }, style: "SocialOverlayPopup");
596  baseButton.Frame.OutlineThickness = 1f;
597 
598  var notification = new NotificationHandler.Notification(
599  ReceiveTime: DateTime.Now,
600  GuiElement: baseButton);
601  baseButton.OnClicked = (_, _) =>
602  {
603  IsOpen = true;
604  notificationHandler.RemoveNotification(notification);
605  return false;
606  };
607  baseButton.OnSecondaryClicked = (_, _) =>
608  {
609  notificationHandler.RemoveNotification(notification);
610  return false;
611  };
612 
613  _ = new GUITextBlock(
614  new RectTransform(Vector2.One * 0.98f, baseButton.RectTransform, Anchor.Center),
615  text: TextManager.GetWithVariable("SocialOverlayShortcutHint", "[shortcut]", ShortcutBindText),
616  textAlignment: Alignment.Center,
617  wrap: true)
618  {
619  CanBeFocused = false
620  };
621 
622  notificationHandler.AddNotification(notification);
623  }
624 
625  private void ShowEosSettingsMenu()
626  {
627  bool hasEpicAccount = EosAccount.SelfAccountIds.OfType<EpicAccountId>().Any();
628  string manageAccountsText = hasEpicAccount
629  ? "EosSettings.ManageConnectedAccounts"
630  : "EosSettings.LinkToEpicAccount";
631 
632  bool eosEnabled = EosInterface.Core.IsInitialized;
633  string enableButtonText = eosEnabled ? "EosSettings.DisableEos" : "EosSettings.EnableEos";
634 
635  var msgBox = new GUIMessageBox(TextManager.Get("EosSettings"), string.Empty,
636  new LocalizedString[]
637  {
638  TextManager.Get(manageAccountsText),
639  TextManager.Get(enableButtonText),
640  TextManager.Get("EosSettings.RequestDeletion")
641  }, minSize: new Point(GUI.IntScale(550), 0))
642  {
643  DrawOnTop = true
644  };
645  msgBox.Buttons[0].Enabled = eosEnabled;
646  msgBox.Buttons[0].ToolTip = TextManager.Get($"{manageAccountsText}.Tooltip");
647  msgBox.Buttons[1].ToolTip = TextManager.Get($"{enableButtonText}.Tooltip");
648  msgBox.Buttons[2].ToolTip = TextManager.Get("EosSettings.RequestDeletion.Tooltip");
649 
650  var closeButton = new GUIButton(new RectTransform(new Point(GUI.IntScale(35)), msgBox.InnerFrame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(GUI.IntScale(8)) },
651  style: "SocialOverlayCloseButton")
652  {
653  OnClicked = closeMsgBox(msgBox)
654  };
655 
656  msgBox.Buttons[0].OnClicked += (_, _) =>
657  {
658  if (!hasEpicAccount)
659  {
660  //attempt to create an epic account and link it with the Steam account
661  var loadingBox = GUIMessageBox.CreateLoadingBox(
662  text: TextManager.Get("EosLinkSteamToEpicLoadingText"),
663  new[] { (TextManager.Get("Cancel"), new Action<GUIMessageBox>(msgBox => msgBox.Close())) },
664  relativeSize: (0.35f, 0.25f));
665  loadingBox.DrawOnTop = true;
666  TaskPool.Add(
667  $"LoginToEpicAccountAsSecondary",
668  EosEpicSecondaryLogin.LoginToLinkedEpicAccount(),
669  t =>
670  {
671  if (t.TryGetResult(out Result<Unit, EosEpicSecondaryLogin.LoginError>? result))
672  {
673  LocalizedString taskResultMsg;
674  if (result.IsSuccess)
675  {
676  taskResultMsg = TextManager.Get("EosLinkSuccess");
677  }
678  else if (result.TryUnwrapFailure(out var failure))
679  {
680  taskResultMsg = TextManager.GetWithVariable("EosLinkError", "[error]", failure.ToString());
681  }
682  else
683  {
684  taskResultMsg = TextManager.GetWithVariable("EosLinkError", "[error]", result.ToString());
685  }
686 
687  var msgBox = new GUIMessageBox(
688  TextManager.Get("EosSettings.LinkToEpicAccount"),
689  taskResultMsg,
690  new[]
691  {
692  TextManager.Get("OK"),
693  })
694  {
695  DrawOnTop = true
696  };
697  msgBox.Buttons[0].OnClicked = closeMsgBox(msgBox);
698  }
699  loadingBox.Close();
700  });
701  msgBox.Close();
702  }
703  else
704  {
705  //if the user has an epic account, we can just go and link it in the browser
706  const string url = "https://www.epicgames.com/account/connections";
707  var prompt = GameMain.ShowOpenUriPrompt(url);
708  prompt.DrawOnTop = true;
709  msgBox.Close();
710  }
711  return true;
712  };
713  msgBox.Buttons[1].OnClicked += (btn, obj) =>
714  {
715  var crossplayChoice = eosEnabled
716  ? EosSteamPrimaryLogin.CrossplayChoice.Disabled
717  : EosSteamPrimaryLogin.CrossplayChoice.Enabled;
718  EosSteamPrimaryLogin.HandleCrossplayChoiceChange(crossplayChoice);
719  GameSettings.SetCurrentConfig(GameSettings.CurrentConfig with { CrossplayChoice = crossplayChoice });
720  GameSettings.SaveCurrentConfig();
721  closeMsgBox(msgBox)(btn, obj);
722  return true;
723  };
724  msgBox.Buttons[2].OnClicked += (btn, obj) =>
725  {
726  const string emailAddress = "contact@barotraumagame.com";
727  const string subject = "Requesting account information deletion";
728  string bodyText = "I would like to delete all of my account information stored by Epic Games.";
729 
730  bool epicAccountIdAvailable = EosAccount.SelfAccountIds.OfType<EpicAccountId>().Any();
731  bool steamIdAvailable = SteamManager.GetSteamId().TryUnwrap(out SteamId? steamId);
732  if (!steamIdAvailable && !epicAccountIdAvailable)
733  {
734  new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariable(
735  "EosSettings.RequestDeletion.NoAccountId",
736  "[emailAddress]",
737  emailAddress));
738  return false;
739  }
740 
741  if (epicAccountIdAvailable)
742  {
743  bodyText += $"\n\nMy Epic Account ID(s): {string.Join(", ", EosAccount.SelfAccountIds.OfType<EpicAccountId>().Select(id => id.StringRepresentation))}";
744  }
745  if (steamIdAvailable)
746  {
747  bodyText += $"\n\nMy Steam ID: {steamId!.StringRepresentation}";
748  }
749 
750  string uri =
751  $"mailto:{emailAddress}?" +
752  $"subject={Uri.EscapeDataString(subject)}" +
753  $"&body={Uri.EscapeDataString(bodyText)}";
754  var prompt = GameMain.ShowOpenUriPrompt(uri,
755  TextManager.GetWithVariables("OpenLinkInEmailClient",
756  ("[recipient]", emailAddress),
757  ("[message]", bodyText)));
758 
759  if (prompt != null)
760  {
761  prompt.DrawOnTop = true;
762  }
763 
764  closeMsgBox(msgBox)(btn, obj);
765  return true;
766  };
767  return;
768 
769  GUIButton.OnClickedHandler closeMsgBox(GUIMessageBox msgBox)
770  {
771  return (button, obj) =>
772  {
773  RefreshEosStatusText();
774  return msgBox.Close(button, obj);
775  };
776  }
777  }
778 
779  private void PopulateSelectedFriendInfoFrame(FriendInfo friendInfo)
780  {
781  selectedFriendInfoFrame.ClearChildren();
782  var layout =
783  new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, selectedFriendInfoFrame.RectTransform,
784  Anchor.Center))
785  {
786  Stretch = true,
787  RelativeSpacing = 0.02f
788  };
789 
790  addPadding();
791  new GUITextBlock(
792  new RectTransform((1.0f, 0.08f), layout.RectTransform),
793  text: friendInfo.Name,
794  font: GUIStyle.SubHeadingFont,
795  textAlignment: Alignment.Center)
796  {
798  };
799  new GUITextBlock(
800  new RectTransform((1.0f, 0.08f), layout.RectTransform),
801  text: friendInfo.StatusText,
802  font: GUIStyle.Font,
803  textAlignment: Alignment.TopCenter)
804  {
806  };
807  addPadding();
808  var viewProfileButton = addButton(friendInfo.Id.ViewProfileLabel());
809  viewProfileButton.OnClicked = (_, _) =>
810  {
811  friendInfo.Id.OpenProfile();
812  return false;
813  };
814  if (friendInfo.IsInServer &&
815  /* don't allow joining other servers when hosting */
816  GameMain.Client is not { IsServerOwner: true } &&
817  /* can't join if already joined */
818  friendInfo.ConnectCommand.TryUnwrap(out var command) && !command.IsClientConnectedToEndpoint())
819  {
820  var joinButton = addButton(TextManager.Get("ServerListJoin"));
821  joinButton.OnClicked = (_, _) =>
822  {
823  GameMain.Instance.ConnectCommand = friendInfo.ConnectCommand;
824  selectedFriendInfoFrame.Visible = false;
825  IsOpen = false;
826  return false;
827  };
828  }
829  if (inviteHandler.HasInviteFrom(friendInfo.Id))
830  {
831  var declineButton = addButton(TextManager.Get("DeclineInvite"));
832  declineButton.OnClicked = (_, _) =>
833  {
834  inviteHandler.ClearInvitesFrom(friendInfo.Id);
835  selectedFriendInfoFrame.Visible = false;
836  return false;
837  };
838  }
839  if (GameMain.Client is not null)
840  {
841  var inviteButton = addButton(TextManager.Get("InviteFriend"));
842  inviteButton.OnClicked = (_, _) =>
843  {
844  selectedFriendInfoFrame.Visible = false;
845  var connectCommandOption = (GameMain.Client?.ClientPeer.ServerEndpoint) switch
846  {
847  LidgrenEndpoint lidgrenEndpoint => Option.Some(new ConnectCommand(GameMain.Client.Name, lidgrenEndpoint)),
848  P2PEndpoint or PipeEndpoint => Option.Some(new ConnectCommand(GameMain.Client.Name, GameMain.Client.ClientPeer.AllServerEndpoints.OfType<P2PEndpoint>().ToImmutableArray())),
849  _ => Option.None
850  };
851  if (!connectCommandOption.TryUnwrap(out var connectCommand))
852  {
853  DebugConsole.AddWarning($"Could not create an invite for the endpoint {GameMain.Client?.ClientPeer.ServerEndpoint}.");
854  return false;
855  }
856 
857  if (friendInfo.Id is SteamId friendSteamId && SteamManager.IsInitialized)
858  {
859  var steamFriend = new Steamworks.Friend(friendSteamId.Value);
860  steamFriend.InviteToGame(connectCommand.ToString());
861  }
862  else if (friendInfo.Id is EpicAccountId friendEpicId && EosInterface.Core.IsInitialized)
863  {
864  async Task sendEpicInvite()
865  {
866  var selfEpicIds = EosInterface.IdQueries.GetLoggedInEpicIds();
867  if (selfEpicIds.Length == 0) { return; }
868 
869  var selfEpicId = selfEpicIds[0];
870  await EosInterface.Presence.SendInvite(selfEpicId, friendEpicId);
871  }
872 
873  TaskPool.Add(
874  $"Invite{friendEpicId}",
875  sendEpicInvite(),
876  _ => { });
877  }
878  return false;
879  };
880  }
881  addPadding();
882 
883  void addPadding()
884  => new GUIFrame(new RectTransform((1.0f, 0.05f), layout.RectTransform), style: null);
885 
886  GUIButton addButton(LocalizedString label)
887  => new GUIButton(new RectTransform((1.0f, 0.08f), layout.RectTransform), label, style: "SocialOverlayButton");
888  }
889 
890  private void RefreshEosStatusText()
891  {
892  if (eosStatusTextContainer is null) { return; }
893 
894  eosStatusTextContainer.ClearChildren();
895  bool linkedToEpicAccount = EosAccount.SelfAccountIds.OfType<EpicAccountId>().Any();
896  _ = new GUITextBlock(new RectTransform(Vector2.One, eosStatusTextContainer.RectTransform),
897  textAlignment: Alignment.CenterLeft,
898  wrap: true,
899  text: TextManager.Get($"EosStatus.{EosInterface.Core.CurrentStatus}")
900  + "\n"
901  + TextManager.Get(linkedToEpicAccount
902  ? "EosSettings.LinkedToAccount"
903  : "EosSettings.NotLinkedToAccount"));
904 
905  linkHint.Visible = !linkedToEpicAccount && EosSteamPrimaryLogin.IsNewEosPlayer;
906  }
907 
908  public void RefreshFriendList()
909  {
910  EosAccount.RefreshSelfAccountIds(onRefreshComplete: () =>
911  {
912  RefreshEosStatusText();
913  lastRefreshTime = DateTime.Now;
914 
915  if (EosInterface.Core.CurrentStatus != EosInterface.Core.Status.Online
916  && !SteamManager.IsInitialized)
917  {
918  friendPlayerListBox.ClearChildren();
919  var offlineLabel = insertLabel(TextManager.Get("SocialOverlayOffline"), heightScale: 4.0f);
920  offlineLabel.Wrap = true;
921 
922  return;
923  }
924 
925  TaskPool.Add(
926  "RefreshFriendList",
927  friendProvider.RetrieveFriends(),
928  t =>
929  {
930  if (!t.TryGetResult(out ImmutableArray<FriendInfo> friends))
931  {
932  return;
933  }
934 
935  friendPlayerListBox.ClearChildren();
936  friendPlayerRows.ForEach(f => f.FriendInfo.Dispose());
937  friendPlayerRows.Clear();
938 
939  var friendsOrdered = friends
940  .OrderByDescending(f => f.CurrentStatus)
941  .ThenByDescending(f => inviteHandler.HasInviteFrom(f.Id))
942  .ThenBy(f => f.Name)
943  .ToImmutableArray();
944  bool prevWasOnline = true;
945  if (friendsOrdered.Length > 0 && friendsOrdered[0].IsOnline)
946  {
947  insertLabel(TextManager.Get("Label.OnlineLabel"));
948  }
949 
950  for (int friendIndex = 0; friendIndex < friendsOrdered.Length; friendIndex++)
951  {
952  var friend = friendsOrdered[friendIndex];
953  if (prevWasOnline && !friend.IsOnline)
954  {
955  if (friendIndex > 0)
956  {
957  insertLabel("");
958  }
959 
960  insertLabel(TextManager.Get("Label.OfflineLabel"));
961  }
962 
963  var friendFrame = new GUIFrame(CreateRowRectT(friendPlayerListBox.Content),
964  style: "ListBoxElement")
965  {
966  UserData = friend
967  };
968  GUILayoutGroup newRowLayout = CreateRowLayout(friendFrame);
969  newRowLayout.RectTransform.RelativeSize = Vector2.One;
970  newRowLayout.RectTransform.ScaleBasis = ScaleBasis.Normal;
971  var newRow = new PlayerRow(friend, newRowLayout,
972  invitedYou: inviteHandler.HasInviteFrom(friend.Id));
973  friendPlayerRows.Add(newRow);
974 
975  prevWasOnline = friend.IsOnline;
976  }
977 
978  contentLayout.Recalculate();
979  friendPlayerListBox.UpdateScrollBarSize();
980  });
981  });
982 
983  GUITextBlock insertLabel(LocalizedString text, float heightScale = 0.5f)
984  {
985  var labelContainer = new GUIFrame(CreateRowRectT(friendPlayerListBox.Content), style: null)
986  {
987  CanBeFocused = false
988  };
989  Vector2 oldRelativeSize = labelContainer.RectTransform.RelativeSize;
990  labelContainer.RectTransform.RelativeSize
991  = (oldRelativeSize.X, oldRelativeSize.Y * heightScale);
992  return new GUITextBlock(new RectTransform(Vector2.One, labelContainer.RectTransform),
993  text: text,
994  font: GUIStyle.SubHeadingFont);
995  }
996  }
997 
998  public void AddToGuiUpdateList()
999  {
1000  if (IsOpen)
1001  {
1002  background.AddToGUIUpdateList();
1003  }
1004  notificationHandler.AddToGuiUpdateList();
1005  }
1006 
1007  public void Update()
1008  {
1009  inviteHandler.Update();
1010  notificationHandler.Update();
1011 
1012  if (!IsOpen) { return; }
1013 
1014  if (selectedFriendInfoFrame.Visible)
1015  {
1016  if (PlayerInput.PrimaryMouseButtonClicked()
1017  && selectedFriendInfoFrame.Visible
1018  && !GUI.IsMouseOn(friendPlayerListBox)
1019  && !GUI.IsMouseOn(selectedFriendInfoFrame))
1020  {
1021  friendPlayerListBox.Deselect();
1022  }
1023 
1024  if (GUI.IsMouseOn(friendPlayerListBox)
1025  && PlayerInput.ScrollWheelSpeed != 0)
1026  {
1027  friendPlayerListBox.Deselect();
1028  }
1029 
1030  if (!friendPlayerListBox.Selected)
1031  {
1032  selectedFriendInfoFrame.Visible = false;
1033  }
1034  }
1035 
1036  if (eosConfigButton != null)
1037  {
1038  bool eosConfigAccessible = GameMain.NetworkMember == null;
1039  if (eosConfigAccessible != eosConfigButton.Enabled)
1040  {
1041  eosConfigButton.Enabled = eosConfigAccessible;
1042  eosConfigButton.Children.ForEach(c => c.Enabled = eosConfigAccessible);
1043  eosConfigButton.ToolTip = eosConfigAccessible ? string.Empty : TextManager.Get("CantAccessEOSSettingsInMP");
1044  }
1045  }
1046 
1047  var currentEosStatus = EosInterface.Core.CurrentStatus;
1048  if (currentEosStatus != eosLastKnownStatus)
1049  {
1050  eosLastKnownStatus = currentEosStatus;
1051  RefreshEosStatusText();
1052  }
1053 
1054  if (DateTime.Now < lastRefreshTime + refreshInterval) { return; }
1055 
1056  RefreshFriendList();
1057  }
1058 
1059  public void Dispose()
1060  {
1061  inviteHandler.Dispose();
1062  }
1063 }
readonly string Name
Definition: FriendInfo.cs:9
readonly Option< ConnectCommand > ConnectCommand
Definition: FriendInfo.cs:14
Option< Sprite > Avatar
Definition: FriendInfo.cs:17
readonly AccountId Id
Definition: FriendInfo.cs:10
LocalizedString StatusText
Definition: FriendInfo.cs:26
void RetrieveOrInheritAvatar(Option< Sprite > inheritableAvatar, int size)
Definition: FriendInfo.cs:45
bool IsInServer
Definition: FriendInfo.cs:20
static ? SocialOverlay Instance
void AddToGuiUpdateList()
void DisplayBindHintToPlayer()
static readonly LocalizedString ShortcutBindText
static void Init()
void RefreshFriendList()