Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Collections.Immutable;
5 using System.Linq;
7 using Microsoft.Xna.Framework;
8 
9 namespace Barotrauma
10 {
11  internal partial class ReadyCheck
12  {
13  private static LocalizedString ReadyCheckBody(string name) => string.IsNullOrWhiteSpace(name) ? TextManager.Get("readycheck.serverbody") : TextManager.GetWithVariable("readycheck.body", "[player]", name);
14 
15  private static LocalizedString ReadyCheckStatus(int ready, int total) => TextManager.GetWithVariables("readycheck.readycount",
16  ("[ready]", ready.ToString()),
17  ("[total]", total.ToString()));
18  private static LocalizedString ReadyCheckPleaseWait(int seconds) => TextManager.GetWithVariable("readycheck.pleasewait", "[seconds]", seconds.ToString());
19 
20  private static readonly LocalizedString readyCheckHeader = TextManager.Get("ReadyCheck.Title");
21 
22  private static readonly LocalizedString noButton = TextManager.Get("No"),
23  yesButton = TextManager.Get("Yes"),
24  closeButton = TextManager.Get("Close");
25 
26  private const string TimerData = "Timer",
27  PromptData = "ReadyCheck",
28  ResultData = "ReadyCheckResults",
29  UserListData = "ReadyUserList",
30  ReadySpriteData = "ReadySprite";
31 
32  private int lastSecond = 1;
33 
34  private GUIMessageBox? msgBox;
35  private GUIMessageBox? resultsBox;
36 
37  public static DateTime ReadyCheckCooldown = DateTime.MinValue;
38 
39  public static bool IsReadyCheck(GUIComponent? msgBox) => msgBox?.UserData as string == PromptData || msgBox?.UserData as string == ResultData;
40 
41  private void CreateMessageBox(string author)
42  {
43  Vector2 relativeSize = new Vector2(0.2f / GUI.AspectRatioAdjustment, 0.15f);
44  Point minSize = new Point(300, 200);
45  msgBox = new GUIMessageBox(readyCheckHeader, ReadyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true };
46 
47  GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.125f), msgBox.Content.RectTransform), childAnchor: Anchor.Center);
48  new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), 0.0f, GUIStyle.Orange) { UserData = TimerData };
49 
50  // Yes
51  msgBox.Buttons[0].OnClicked = delegate
52  {
53  msgBox.Close();
54  if (GameMain.Client == null) { return true; }
55  SendState(ReadyStatus.Yes);
56  CreateResultsMessage();
57  return true;
58  };
59 
60  // No
61  msgBox.Buttons[1].OnClicked = delegate
62  {
63  msgBox.Close();
64  if (GameMain.Client == null) { return true; }
65  SendState(ReadyStatus.No);
66  CreateResultsMessage();
67  return true;
68  };
69  }
70 
71  private void CreateResultsMessage()
72  {
73  if (GameMain.Client == null) { return; }
74 
75  Vector2 relativeSize = new Vector2(0.2f, 0.3f);
76  Point minSize = new Point(300, 400);
77  resultsBox = new GUIMessageBox(readyCheckHeader, string.Empty, new[] { closeButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = ResultData, Draggable = true };
78  if (msgBox != null)
79  {
80  resultsBox.RectTransform.ScreenSpaceOffset = msgBox.RectTransform.ScreenSpaceOffset;
81  }
82 
83  GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), resultsBox.Content.RectTransform)) { UserData = UserListData };
84 
85  foreach (var (id, status) in Clients)
86  {
87  Client? client = GameMain.Client.ConnectedClients.FirstOrDefault(c => c.SessionId == id);
88  if (client == null)
89  {
90  string list = GameMain.Client.ConnectedClients.Aggregate("Available clients:\n", (current, c) => current + $"{c.SessionId}: {c.Name}\n");
91  DebugConsole.AddWarning($"Client ID {id} was reported in ready check but was not found.\n" + list.TrimEnd('\n'));
92  continue;
93  }
94  GUIFrame container = new GUIFrame(new RectTransform(new Vector2(1f, 0.15f), listBox.Content.RectTransform), style: "ListBoxElement") { UserData = id };
95  GUILayoutGroup frame = new GUILayoutGroup(new RectTransform(Vector2.One, container.RectTransform), isHorizontal: true) { Stretch = true };
96 
97  int height = frame.Rect.Height;
98 
99  JobPrefab? jobPrefab = client?.Character?.Info?.Job?.Prefab;
100 
101 
102  if (jobPrefab?.Icon != null)
103  {
104  // job icon
105  new GUIImage(new RectTransform(new Point(height, height), frame.RectTransform), jobPrefab.Icon, scaleToFit: true) { Color = jobPrefab.UIColor };
106  }
107 
108  new GUITextBlock(new RectTransform(new Vector2(0.75f, 1), frame.RectTransform), client?.Name ?? $"Unknown ID {id}", jobPrefab?.UIColor ?? Color.White, textAlignment: Alignment.Center) { AutoScaleHorizontal = true };
109  var statusIcon = new GUIImage(new RectTransform(new Point(height, height), frame.RectTransform), null, scaleToFit: true) { UserData = ReadySpriteData };
110  UpdateStatusIcon(statusIcon, status);
111  }
112 
113  resultsBox.Buttons[0].OnClicked = delegate
114  {
115  resultsBox.Close();
116  return true;
117  };
118  }
119 
120  private void UpdateBar()
121  {
122  double elapsedTime = (DateTime.Now - startTime).TotalSeconds;
123  if (msgBox is { Closed: false } && GUIMessageBox.MessageBoxes.Contains(msgBox))
124  {
125  if (msgBox.FindChild(TimerData, true) is GUIProgressBar bar)
126  {
127  bar.BarSize = (float)(elapsedTime / (endTime - startTime).TotalSeconds);
128  }
129  }
130 
131  // play click sound after a second has passed
132  int second = (int)Math.Ceiling(elapsedTime);
133  if (second > lastSecond)
134  {
135  if (msgBox is { Closed: false })
136  {
137  SoundPlayer.PlayUISound(GUISoundType.PopupMenu);
138  }
139  lastSecond = second;
140  }
141  }
142 
143  private static void CloseLingeringPopups()
144  {
145  foreach (GUIComponent box in GUIMessageBox.MessageBoxes.ToImmutableArray())
146  {
147  if (box is not GUIMessageBox msgBox) { continue; }
148 
149  if (msgBox.UserData is PromptData or ResultData)
150  {
151  msgBox.Close();
152  }
153  }
154  }
155 
156  public static void ClientRead(IReadMessage inc)
157  {
158  ReadyCheckState state = (ReadyCheckState)inc.ReadByte();
159  CrewManager? crewManager = GameMain.GameSession?.CrewManager;
160  var otherClients = GameMain.Client.ConnectedClients;
161  if (crewManager == null || otherClients == null)
162  {
163  if (state == ReadyCheckState.Start)
164  {
165  SendState(ReadyStatus.No);
166  }
167  return;
168  }
169 
170  switch (state)
171  {
172  case ReadyCheckState.Start:
173  CloseLingeringPopups();
174 
175  bool isOwn = false;
176  byte authorId = 0;
177 
178  long startTime = inc.ReadInt64();
179  long endTime = inc.ReadInt64();
180  string author = inc.ReadString();
181  bool hasAuthor = inc.ReadBoolean();
182 
183  if (hasAuthor)
184  {
185  authorId = inc.ReadByte();
186  isOwn = authorId == GameMain.Client.SessionId;
187  }
188 
189  ushort clientCount = inc.ReadUInt16();
190  List<byte> clients = new List<byte>();
191  for (int i = 0; i < clientCount; i++)
192  {
193  clients.Add(inc.ReadByte());
194  }
195 
196  ReadyCheck rCheck = new ReadyCheck(clients,
197  DateTimeOffset.FromUnixTimeSeconds(startTime).LocalDateTime,
198  DateTimeOffset.FromUnixTimeSeconds(endTime).LocalDateTime);
199  crewManager.ActiveReadyCheck = rCheck;
200 
201  if (isOwn)
202  {
203  SendState(ReadyStatus.Yes);
204  rCheck.CreateResultsMessage();
205  }
206  else
207  {
208  rCheck.CreateMessageBox(author);
209  }
210 
211  if (hasAuthor && rCheck.Clients.ContainsKey(authorId))
212  {
213  rCheck.Clients[authorId] = ReadyStatus.Yes;
214  }
215  break;
216  case ReadyCheckState.Update:
217  ReadyStatus newState = (ReadyStatus)inc.ReadByte();
218  byte targetId = inc.ReadByte();
219  crewManager.ActiveReadyCheck?.UpdateState(targetId, newState);
220  break;
221  case ReadyCheckState.End:
222  ushort count = inc.ReadUInt16();
223  for (int i = 0; i < count; i++)
224  {
225  byte id = inc.ReadByte();
226  ReadyStatus status = (ReadyStatus)inc.ReadByte();
227  crewManager.ActiveReadyCheck?.UpdateState(id, status);
228  }
229 
230  crewManager.ActiveReadyCheck?.EndReadyCheck();
231  crewManager.ActiveReadyCheck?.msgBox?.Close();
232  crewManager.ActiveReadyCheck = null;
233  break;
234  }
235  }
236 
237  partial void EndReadyCheck()
238  {
239  if (IsFinished) { return; }
240  IsFinished = true;
241 
242  int readyCount = Clients.Count(static pair => pair.Value == ReadyStatus.Yes);
243  int totalCount = Clients.Count;
244  GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, ReadyCheckStatus(readyCount, totalCount).Value, ChatMessageType.Server, null));
245  }
246 
247  private void UpdateState(byte id, ReadyStatus status)
248  {
249  if (Clients.ContainsKey(id))
250  {
251  Clients[id] = status;
252  }
253 
254  if (resultsBox == null || resultsBox.Closed || !GUIMessageBox.MessageBoxes.Contains(resultsBox)) { return; }
255  if (resultsBox.Content.FindChild(UserListData) is not GUIListBox userList) { return; }
256 
257  var child = userList.Content.FindChild(id);
258  if (child?.GetChild<GUILayoutGroup>().FindChild(ReadySpriteData) is not GUIImage image) { return; }
259  UpdateStatusIcon(image, status);
260  }
261 
262  private static void UpdateStatusIcon(GUIImage image, ReadyStatus status)
263  {
264  string style;
265  switch (status)
266  {
267  case ReadyStatus.Yes:
268  style = "MissionCompletedIcon";
269  break;
270  case ReadyStatus.No:
271  style = "MissionFailedIcon";
272  break;
273  default:
274  return;
275  }
276  image.ApplyStyle(GUIStyle.GetComponentStyle(style));
277  }
278 
279  private static void SendState(ReadyStatus status)
280  {
281  IWriteMessage msg = new WriteOnlyMessage();
282  msg.WriteByte((byte)ClientPacketHeader.READY_CHECK);
283  msg.WriteByte((byte)ReadyCheckState.Update);
284  msg.WriteByte((byte)status);
285  GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
286  }
287 
288  public static void CreateReadyCheck()
289  {
290  if (ReadyCheckCooldown < DateTime.Now)
291  {
292 #if !DEBUG
293  ReadyCheckCooldown = DateTime.Now.AddMinutes(1);
294 #endif
295  IWriteMessage msg = new WriteOnlyMessage();
296  msg.WriteByte((byte)ClientPacketHeader.READY_CHECK);
297  msg.WriteByte((byte)ReadyCheckState.Start);
298  GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
299  return;
300  }
301 
302  GUIMessageBox msgBox = new GUIMessageBox(readyCheckHeader, ReadyCheckPleaseWait((ReadyCheckCooldown - DateTime.Now).Seconds), new[] { closeButton });
303  msgBox.Buttons[0].OnClicked = delegate
304  {
305  msgBox.Close();
306  return true;
307  };
308  }
309  }
310 }
JobPrefab Prefab
Definition: Job.cs:18
static ChatMessage Create(string senderName, string text, ChatMessageType type, Entity sender, Client client=null, PlayerConnectionChangeType changeType=PlayerConnectionChangeType.None, Color? textColor=null)
GUISoundType
Definition: GUI.cs:21