4 using System.Collections.Generic;
5 using System.Collections.Immutable;
12 internal sealed
partial class MedicalClinic
16 public enum RequestResult
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);
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>>();
38 private static readonly LeakyBucket requestBucket =
new(RateLimitExpiry / (float)RateLimitMaxRequests, 10);
40 public bool RequestAfflictions(CharacterInfo info, Action<AfflictionRequest> onReceived)
42 if (GameMain.IsSingleplayer)
45 if (Screen.Selected is TestScreen)
47 onReceived.Invoke(
new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray()));
52 if (info is not {
Character.CharacterHealth: { } health })
54 onReceived.Invoke(
new AfflictionRequest(RequestResult.CharacterInfoMissing, ImmutableArray<NetAffliction>.Empty));
58 ImmutableArray<NetAffliction> pendingAfflictions = GetAllAfflictions(health);
59 onReceived.Invoke(
new AfflictionRequest(RequestResult.Success, pendingAfflictions));
63 return requestBucket.TryEnqueue(() =>
65 afflictionRequests.Add(
new RequestAction<AfflictionRequest>(onReceived, GetTimeout()));
66 SendAfflictionRequest(info);
70 public void RequestLatestPending(Action<PendingRequest> onReceived)
73 if (GameMain.IsSingleplayer) {
return; }
75 requestBucket.TryEnqueue(() =>
77 pendingHealRequests.Add(
new RequestAction<PendingRequest>(onReceived, GetTimeout()));
82 public void Update(
float deltaTime)
84 processAfflictionChangesTimer -= deltaTime;
85 if (processAfflictionChangesTimer <= 0.0f)
87 foreach (var character
in charactersWithAfflictionChanges)
89 if (GameMain.NetworkMember is
null)
91 ImmutableArray<NetAffliction> afflictions = GetAllAfflictions(character.CharacterHealth);
92 ui?.UpdateAfflictions(
new NetCrewMember(character.Info, afflictions));
94 ui?.UpdateCrewPanel();
96 charactersWithAfflictionChanges.Clear();
97 processAfflictionChangesTimer = ProcessAfflictionChangesInterval;
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);
109 static void CallbackOnlyTimeout(Action<CallbackOnlyRequest> callback) { callback(
new CallbackOnlyRequest(RequestResult.Timeout)); }
112 public bool IsAfflictionPending(NetCrewMember character, NetAffliction affliction)
114 foreach (NetCrewMember crewMember
in PendingHeals)
116 if (!crewMember.CharacterEquals(character)) {
continue; }
118 return crewMember.Afflictions.Any(a => a.AfflictionEquals(affliction));
124 private static bool TryDequeue<T>(List<RequestAction<T>> requestQueue, out Action<T> result)
126 RequestAction<T>? first = requestQueue.FirstOrNull();
127 if (first is not { } action)
129 result =
static _ => { };
133 requestQueue.Remove(action);
134 result = action.Callback;
138 private static void UpdateQueue<T>(List<RequestAction<T>> requestQueue, DateTimeOffset now, Action<Action<T>> onTimeout)
140 HashSet<RequestAction<T>>? removals =
null;
141 foreach (RequestAction<T> action
in requestQueue)
143 if (action.Timeout < now)
145 onTimeout.Invoke(action.Callback);
147 removals ??=
new HashSet<RequestAction<T>>();
148 removals.Add(action);
152 if (removals is
null) {
return; }
154 foreach (RequestAction<T> action
in removals)
156 requestQueue.Remove(action);
160 private void OnMoneyChanged(WalletChangedEvent e)
162 if (e.Wallet.IsOwnWallet) { OnUpdate?.Invoke(); }
166 private static DateTimeOffset GetTimeout() => DateTimeOffset.Now.AddSeconds(5).AddMilliseconds(GetPing());
168 private static int GetPing()
170 if (GameMain.IsSingleplayer || GameMain.Client?.Name is not { } ownName || GameMain.NetworkMember?.ConnectedClients is not { } clients) {
return 0; }
172 return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault();
175 public bool TreatAllButtonAction(Action<CallbackOnlyRequest> onReceived)
177 if (GameMain.IsSingleplayer)
179 AddEverythingToPending();
180 onReceived(
new CallbackOnlyRequest(RequestResult.Success));
185 return requestBucket.TryEnqueue(() =>
187 addRequests.Add(
new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
188 ClientSend(
null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable);
193 public bool HealAllButtonAction(Action<HealRequest> onReceived)
195 if (GameMain.IsSingleplayer)
197 HealRequestResult result = HealAllPending();
198 onReceived(
new HealRequest(RequestResult.Success, HealAllPending()));
199 if (result == HealRequestResult.Success)
207 if (campaign?.CampaignUI?.MedicalClinic is { } openedUi)
209 openedUi.ClosePopup();
212 return requestBucket.TryEnqueue(() =>
214 healAllRequests.Add(
new RequestAction<HealRequest>(onReceived, GetTimeout()));
215 ClientSend(
null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable);
219 public bool ClearAllButtonAction(Action<CallbackOnlyRequest> onReceived)
221 if (GameMain.IsSingleplayer)
224 onReceived(
new CallbackOnlyRequest(RequestResult.Success));
229 return requestBucket.TryEnqueue(() =>
231 clearAllRequests.Add(
new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
232 ClientSend(
null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable);
236 private void ClearRequestReceived()
239 if (TryDequeue(clearAllRequests, out var callback))
241 callback(
new CallbackOnlyRequest(RequestResult.Success));
248 NetHealRequest request = INetSerializableStruct.Read<NetHealRequest>(inc);
250 if (request.Result == HealRequestResult.Success)
252 HealAllPending(force:
true);
255 if (TryDequeue(healAllRequests, out var callback))
257 callback(
new HealRequest(RequestResult.Success, request.Result));
263 public bool AddPendingButtonAction(NetCrewMember crewMember, Action<CallbackOnlyRequest> onReceived)
265 if (GameMain.IsSingleplayer)
267 InsertPendingCrewMember(crewMember);
268 onReceived(
new CallbackOnlyRequest(RequestResult.Success));
273 return requestBucket.TryEnqueue(() =>
275 addRequests.Add(
new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
276 ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable);
280 public bool RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action<CallbackOnlyRequest> onReceived)
282 if (GameMain.IsSingleplayer)
284 RemovePendingAffliction(crewMember, affliction);
285 onReceived(
new CallbackOnlyRequest(RequestResult.Success));
290 INetSerializableStruct removedAffliction =
new NetRemovedAffliction
292 CrewMember = crewMember,
296 return requestBucket.TryEnqueue(() =>
298 removeRequests.Add(
new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
299 ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable);
303 private void NewAdditionReceived(
IReadMessage inc, MessageFlag flag)
305 var crewMembers = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc);
306 foreach (var crewMember
in crewMembers)
308 InsertPendingCrewMember(crewMember);
310 if (flag == MessageFlag.Response && TryDequeue(addRequests, out var callback))
312 callback(
new CallbackOnlyRequest(RequestResult.Success));
317 private void NewRemovalReceived(
IReadMessage inc, MessageFlag flag)
319 NetRemovedAffliction removed = INetSerializableStruct.Read<NetRemovedAffliction>(inc);
320 RemovePendingAffliction(removed.CrewMember, removed.Affliction);
321 if (flag == MessageFlag.Response && TryDequeue(removeRequests, out var callback))
323 callback(
new CallbackOnlyRequest(RequestResult.Success));
328 private static void SendAfflictionRequest(CharacterInfo info)
330 INetSerializableStruct crewMember =
new NetCrewMember(info);
332 ClientSend(crewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable);
335 private static void SendPendingRequest()
337 ClientSend(
null, NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable);
340 private void AfflictionRequestReceived(
IReadMessage inc)
342 NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
343 if (TryDequeue(afflictionRequests, out var callback))
345 RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.CharacterNotFound : RequestResult.Success;
346 callback(
new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray()));
352 NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
353 ui?.UpdateAfflictions(crewMember);
358 var pendingCrew = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc);
359 if (TryDequeue(pendingHealRequests, out var callback))
361 callback(
new PendingRequest(RequestResult.Success, pendingCrew));
365 public static void SendUnsubscribeRequest() => ClientSend(
null,
366 header: NetworkHeader.UNSUBSCRIBE_ME,
367 deliveryMethod: DeliveryMethod.Reliable);
376 private static void ClientSend(INetSerializableStruct? netStruct, NetworkHeader header, DeliveryMethod deliveryMethod)
380 netStruct?.Write(msg);
381 GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod);
386 NetworkHeader header = (NetworkHeader)inc.
ReadByte();
387 MessageFlag flag = (MessageFlag)inc.
ReadByte();
391 case NetworkHeader.REQUEST_AFFLICTIONS:
392 AfflictionRequestReceived(inc);
394 case NetworkHeader.AFFLICTION_UPDATE:
395 AfflictionUpdateReceived(inc);
397 case NetworkHeader.REQUEST_PENDING:
398 PendingRequestReceived(inc);
400 case NetworkHeader.ADD_PENDING:
401 NewAdditionReceived(inc, flag);
403 case NetworkHeader.REMOVE_PENDING:
404 NewRemovalReceived(inc, flag);
406 case NetworkHeader.HEAL_PENDING:
407 HealRequestReceived(inc);
409 case NetworkHeader.CLEAR_PENDING:
410 ClearRequestReceived();
MedicalClinicUI MedicalClinic