Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs
1 #nullable enable
2 
3 using System;
4 using System.Collections.Generic;
5 using System.Collections.Immutable;
6 using System.Linq;
9 
10 namespace Barotrauma
11 {
12  internal sealed partial class MedicalClinic
13  {
14  private MedicalClinicUI? ui => campaign?.CampaignUI?.MedicalClinic;
15 
16  public enum RequestResult
17  {
18  Undecided,
19  Success,
20  CharacterInfoMissing,
21  CharacterNotFound,
22  Timeout
23  }
24 
25  public readonly record struct RequestAction<T>(Action<T> Callback, DateTimeOffset Timeout);
26  public readonly record struct AfflictionRequest(RequestResult Result, ImmutableArray<NetAffliction> Afflictions);
27  public readonly record struct PendingRequest(RequestResult Result, NetCollection<NetCrewMember> CrewMembers);
28  public readonly record struct CallbackOnlyRequest(RequestResult Result);
29  public readonly record struct HealRequest(RequestResult Result, HealRequestResult HealResult);
30 
31  private readonly List<RequestAction<AfflictionRequest>> afflictionRequests = new List<RequestAction<AfflictionRequest>>();
32  private readonly List<RequestAction<PendingRequest>> pendingHealRequests = new List<RequestAction<PendingRequest>>();
33  private readonly List<RequestAction<CallbackOnlyRequest>> clearAllRequests = new List<RequestAction<CallbackOnlyRequest>>();
34  private readonly List<RequestAction<HealRequest>> healAllRequests = new List<RequestAction<HealRequest>>();
35  private readonly List<RequestAction<CallbackOnlyRequest>> addRequests = new List<RequestAction<CallbackOnlyRequest>>();
36  private readonly List<RequestAction<CallbackOnlyRequest>> removeRequests = new List<RequestAction<CallbackOnlyRequest>>();
37 
38  private static readonly LeakyBucket requestBucket = new(RateLimitExpiry / (float)RateLimitMaxRequests, 10);
39 
40  public bool RequestAfflictions(CharacterInfo info, Action<AfflictionRequest> onReceived)
41  {
42  if (GameMain.IsSingleplayer)
43  {
44 #if DEBUG && LINUX
45  if (Screen.Selected is TestScreen)
46  {
47  onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray()));
48  return true;
49  }
50 #endif
51 
52  if (info is not { Character.CharacterHealth: { } health })
53  {
54  onReceived.Invoke(new AfflictionRequest(RequestResult.CharacterInfoMissing, ImmutableArray<NetAffliction>.Empty));
55  return true;
56  }
57 
58  ImmutableArray<NetAffliction> pendingAfflictions = GetAllAfflictions(health);
59  onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions));
60  return true;
61  }
62 
63  return requestBucket.TryEnqueue(() =>
64  {
65  afflictionRequests.Add(new RequestAction<AfflictionRequest>(onReceived, GetTimeout()));
66  SendAfflictionRequest(info);
67  });
68  }
69 
70  public void RequestLatestPending(Action<PendingRequest> onReceived)
71  {
72  // no need to worry about syncing when there's only one pair of eyes capable of looking at the UI
73  if (GameMain.IsSingleplayer) { return; }
74 
75  requestBucket.TryEnqueue(() =>
76  {
77  pendingHealRequests.Add(new RequestAction<PendingRequest>(onReceived, GetTimeout()));
78  SendPendingRequest();
79  });
80  }
81 
82  public void Update(float deltaTime)
83  {
84  processAfflictionChangesTimer -= deltaTime;
85  if (processAfflictionChangesTimer <= 0.0f)
86  {
87  foreach (var character in charactersWithAfflictionChanges)
88  {
89  if (GameMain.NetworkMember is null)
90  {
91  ImmutableArray<NetAffliction> afflictions = GetAllAfflictions(character.CharacterHealth);
92  ui?.UpdateAfflictions(new NetCrewMember(character.Info, afflictions));
93  }
94  ui?.UpdateCrewPanel();
95  }
96  charactersWithAfflictionChanges.Clear();
97  processAfflictionChangesTimer = ProcessAfflictionChangesInterval;
98  }
99 
100  DateTimeOffset now = DateTimeOffset.Now;
101  UpdateQueue(afflictionRequests, now, onTimeout: static callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray<NetAffliction>.Empty)); });
102  UpdateQueue(pendingHealRequests, now, onTimeout: static callback => { callback(new PendingRequest(RequestResult.Timeout, NetCollection<NetCrewMember>.Empty)); });
103  UpdateQueue(healAllRequests, now, onTimeout: static callback => { callback(new HealRequest(RequestResult.Timeout, HealRequestResult.Unknown)); });
104  UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout);
105  UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout);
106  UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout);
107  requestBucket.Update(deltaTime);
108 
109  static void CallbackOnlyTimeout(Action<CallbackOnlyRequest> callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); }
110  }
111 
112  public bool IsAfflictionPending(NetCrewMember character, NetAffliction affliction)
113  {
114  foreach (NetCrewMember crewMember in PendingHeals)
115  {
116  if (!crewMember.CharacterEquals(character)) { continue; }
117 
118  return crewMember.Afflictions.Any(a => a.AfflictionEquals(affliction));
119  }
120 
121  return false;
122  }
123 
124  private static bool TryDequeue<T>(List<RequestAction<T>> requestQueue, out Action<T> result)
125  {
126  RequestAction<T>? first = requestQueue.FirstOrNull();
127  if (first is not { } action)
128  {
129  result = static _ => { };
130  return false;
131  }
132 
133  requestQueue.Remove(action);
134  result = action.Callback;
135  return true;
136  }
137 
138  private static void UpdateQueue<T>(List<RequestAction<T>> requestQueue, DateTimeOffset now, Action<Action<T>> onTimeout)
139  {
140  HashSet<RequestAction<T>>? removals = null;
141  foreach (RequestAction<T> action in requestQueue)
142  {
143  if (action.Timeout < now)
144  {
145  onTimeout.Invoke(action.Callback);
146 
147  removals ??= new HashSet<RequestAction<T>>();
148  removals.Add(action);
149  }
150  }
151 
152  if (removals is null) { return; }
153 
154  foreach (RequestAction<T> action in removals)
155  {
156  requestQueue.Remove(action);
157  }
158  }
159 
160  private void OnMoneyChanged(WalletChangedEvent e)
161  {
162  if (e.Wallet.IsOwnWallet) { OnUpdate?.Invoke(); }
163  }
164 
165  // if you have more than 5000 ping there are probably more important things to worry about but hey just in case
166  private static DateTimeOffset GetTimeout() => DateTimeOffset.Now.AddSeconds(5).AddMilliseconds(GetPing());
167 
168  private static int GetPing()
169  {
170  if (GameMain.IsSingleplayer || GameMain.Client?.Name is not { } ownName || GameMain.NetworkMember?.ConnectedClients is not { } clients) { return 0; }
171 
172  return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault();
173  }
174 
175  public bool TreatAllButtonAction(Action<CallbackOnlyRequest> onReceived)
176  {
177  if (GameMain.IsSingleplayer)
178  {
179  AddEverythingToPending();
180  onReceived(new CallbackOnlyRequest(RequestResult.Success));
181  OnUpdate?.Invoke();
182  return true;
183  }
184 
185  return requestBucket.TryEnqueue(() =>
186  {
187  addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
188  ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable);
189  });
190  }
191 
192 
193  public bool HealAllButtonAction(Action<HealRequest> onReceived)
194  {
195  if (GameMain.IsSingleplayer)
196  {
197  HealRequestResult result = HealAllPending();
198  onReceived(new HealRequest(RequestResult.Success, HealAllPending()));
199  if (result == HealRequestResult.Success)
200  {
201  OnUpdate?.Invoke();
202  }
203 
204  return true;
205  }
206 
207  if (campaign?.CampaignUI?.MedicalClinic is { } openedUi)
208  {
209  openedUi.ClosePopup();
210  }
211 
212  return requestBucket.TryEnqueue(() =>
213  {
214  healAllRequests.Add(new RequestAction<HealRequest>(onReceived, GetTimeout()));
215  ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable);
216  });
217  }
218 
219  public bool ClearAllButtonAction(Action<CallbackOnlyRequest> onReceived)
220  {
221  if (GameMain.IsSingleplayer)
222  {
223  ClearPendingHeals();
224  onReceived(new CallbackOnlyRequest(RequestResult.Success));
225  OnUpdate?.Invoke();
226  return true;
227  }
228 
229  return requestBucket.TryEnqueue(() =>
230  {
231  clearAllRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
232  ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable);
233  });
234  }
235 
236  private void ClearRequestReceived()
237  {
238  ClearPendingHeals();
239  if (TryDequeue(clearAllRequests, out var callback))
240  {
241  callback(new CallbackOnlyRequest(RequestResult.Success));
242  }
243  OnUpdate?.Invoke();
244  }
245 
246  private void HealRequestReceived(IReadMessage inc)
247  {
248  NetHealRequest request = INetSerializableStruct.Read<NetHealRequest>(inc);
249 
250  if (request.Result == HealRequestResult.Success)
251  {
252  HealAllPending(force: true);
253  }
254 
255  if (TryDequeue(healAllRequests, out var callback))
256  {
257  callback(new HealRequest(RequestResult.Success, request.Result));
258  }
259 
260  OnUpdate?.Invoke();
261  }
262 
263  public bool AddPendingButtonAction(NetCrewMember crewMember, Action<CallbackOnlyRequest> onReceived)
264  {
265  if (GameMain.IsSingleplayer)
266  {
267  InsertPendingCrewMember(crewMember);
268  onReceived(new CallbackOnlyRequest(RequestResult.Success));
269  OnUpdate?.Invoke();
270  return true;
271  }
272 
273  return requestBucket.TryEnqueue(() =>
274  {
275  addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
276  ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable);
277  });
278  }
279 
280  public bool RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action<CallbackOnlyRequest> onReceived)
281  {
282  if (GameMain.IsSingleplayer)
283  {
284  RemovePendingAffliction(crewMember, affliction);
285  onReceived(new CallbackOnlyRequest(RequestResult.Success));
286  OnUpdate?.Invoke();
287  return true;
288  }
289 
290  INetSerializableStruct removedAffliction = new NetRemovedAffliction
291  {
292  CrewMember = crewMember,
293  Affliction = affliction
294  };
295 
296  return requestBucket.TryEnqueue(() =>
297  {
298  removeRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
299  ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable);
300  });
301  }
302 
303  private void NewAdditionReceived(IReadMessage inc, MessageFlag flag)
304  {
305  var crewMembers = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc);
306  foreach (var crewMember in crewMembers)
307  {
308  InsertPendingCrewMember(crewMember);
309  }
310  if (flag == MessageFlag.Response && TryDequeue(addRequests, out var callback))
311  {
312  callback(new CallbackOnlyRequest(RequestResult.Success));
313  }
314  OnUpdate?.Invoke();
315  }
316 
317  private void NewRemovalReceived(IReadMessage inc, MessageFlag flag)
318  {
319  NetRemovedAffliction removed = INetSerializableStruct.Read<NetRemovedAffliction>(inc);
320  RemovePendingAffliction(removed.CrewMember, removed.Affliction);
321  if (flag == MessageFlag.Response && TryDequeue(removeRequests, out var callback))
322  {
323  callback(new CallbackOnlyRequest(RequestResult.Success));
324  }
325  OnUpdate?.Invoke();
326  }
327 
328  private static void SendAfflictionRequest(CharacterInfo info)
329  {
330  INetSerializableStruct crewMember = new NetCrewMember(info);
331 
332  ClientSend(crewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable);
333  }
334 
335  private static void SendPendingRequest()
336  {
337  ClientSend(null, NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable);
338  }
339 
340  private void AfflictionRequestReceived(IReadMessage inc)
341  {
342  NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
343  if (TryDequeue(afflictionRequests, out var callback))
344  {
345  RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.CharacterNotFound : RequestResult.Success;
346  callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray()));
347  }
348  }
349 
350  private void AfflictionUpdateReceived(IReadMessage inc)
351  {
352  NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
353  ui?.UpdateAfflictions(crewMember);
354  }
355 
356  private void PendingRequestReceived(IReadMessage inc)
357  {
358  var pendingCrew = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc);
359  if (TryDequeue(pendingHealRequests, out var callback))
360  {
361  callback(new PendingRequest(RequestResult.Success, pendingCrew));
362  }
363  }
364 
365  public static void SendUnsubscribeRequest() => ClientSend(null,
366  header: NetworkHeader.UNSUBSCRIBE_ME,
367  deliveryMethod: DeliveryMethod.Reliable);
368 
369  private static IWriteMessage StartSending()
370  {
371  IWriteMessage writeMessage = new WriteOnlyMessage();
372  writeMessage.WriteByte((byte)ClientPacketHeader.MEDICAL);
373  return writeMessage;
374  }
375 
376  private static void ClientSend(INetSerializableStruct? netStruct, NetworkHeader header, DeliveryMethod deliveryMethod)
377  {
378  IWriteMessage msg = StartSending();
379  msg.WriteByte((byte)header);
380  netStruct?.Write(msg);
381  GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod);
382  }
383 
384  public void ClientRead(IReadMessage inc)
385  {
386  NetworkHeader header = (NetworkHeader)inc.ReadByte();
387  MessageFlag flag = (MessageFlag)inc.ReadByte();
388 
389  switch (header)
390  {
391  case NetworkHeader.REQUEST_AFFLICTIONS:
392  AfflictionRequestReceived(inc);
393  break;
394  case NetworkHeader.AFFLICTION_UPDATE:
395  AfflictionUpdateReceived(inc);
396  break;
397  case NetworkHeader.REQUEST_PENDING:
398  PendingRequestReceived(inc);
399  break;
400  case NetworkHeader.ADD_PENDING:
401  NewAdditionReceived(inc, flag);
402  break;
403  case NetworkHeader.REMOVE_PENDING:
404  NewRemovalReceived(inc, flag);
405  break;
406  case NetworkHeader.HEAL_PENDING:
407  HealRequestReceived(inc);
408  break;
409  case NetworkHeader.CLEAR_PENDING:
410  ClearRequestReceived();
411  break;
412  }
413  }
414  }
415 }
MedicalClinicUI MedicalClinic
Definition: CampaignUI.cs:48