#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Barotrauma.Extensions; using Barotrauma.Networking; namespace Barotrauma { internal sealed partial class MedicalClinic { private MedicalClinicUI? ui => campaign?.CampaignUI?.MedicalClinic; public enum RequestResult { Undecided, Success, CharacterInfoMissing, CharacterNotFound, Timeout } public readonly record struct RequestAction(Action Callback, DateTimeOffset Timeout); public readonly record struct AfflictionRequest(RequestResult Result, ImmutableArray Afflictions); public readonly record struct PendingRequest(RequestResult Result, NetCollection CrewMembers); public readonly record struct CallbackOnlyRequest(RequestResult Result); public readonly record struct HealRequest(RequestResult Result, HealRequestResult HealResult); private readonly List> afflictionRequests = new List>(); private readonly List> pendingHealRequests = new List>(); private readonly List> clearAllRequests = new List>(); private readonly List> healAllRequests = new List>(); private readonly List> addRequests = new List>(); private readonly List> removeRequests = new List>(); private static readonly LeakyBucket requestBucket = new(RateLimitExpiry / (float)RateLimitMaxRequests, 10); public bool RequestAfflictions(CharacterInfo info, Action onReceived) { if (GameMain.IsSingleplayer) { #if DEBUG && LINUX if (Screen.Selected is TestScreen) { onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray())); return true; } #endif if (info is not { Character.CharacterHealth: { } health }) { onReceived.Invoke(new AfflictionRequest(RequestResult.CharacterInfoMissing, ImmutableArray.Empty)); return true; } ImmutableArray pendingAfflictions = GetAllAfflictions(health); onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions)); return true; } return requestBucket.TryEnqueue(() => { afflictionRequests.Add(new RequestAction(onReceived, GetTimeout())); SendAfflictionRequest(info); }); } public void RequestLatestPending(Action onReceived) { // no need to worry about syncing when there's only one pair of eyes capable of looking at the UI if (GameMain.IsSingleplayer) { return; } requestBucket.TryEnqueue(() => { pendingHealRequests.Add(new RequestAction(onReceived, GetTimeout())); SendPendingRequest(); }); } public void Update(float deltaTime) { processAfflictionChangesTimer -= deltaTime; if (processAfflictionChangesTimer <= 0.0f) { foreach (var character in charactersWithAfflictionChanges) { if (GameMain.NetworkMember is null) { ImmutableArray afflictions = GetAllAfflictions(character.CharacterHealth); ui?.UpdateAfflictions(new NetCrewMember(character.Info, afflictions)); } ui?.UpdateCrewPanel(); } charactersWithAfflictionChanges.Clear(); processAfflictionChangesTimer = ProcessAfflictionChangesInterval; } DateTimeOffset now = DateTimeOffset.Now; UpdateQueue(afflictionRequests, now, onTimeout: static callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); UpdateQueue(pendingHealRequests, now, onTimeout: static callback => { callback(new PendingRequest(RequestResult.Timeout, NetCollection.Empty)); }); UpdateQueue(healAllRequests, now, onTimeout: static callback => { callback(new HealRequest(RequestResult.Timeout, HealRequestResult.Unknown)); }); UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout); requestBucket.Update(deltaTime); static void CallbackOnlyTimeout(Action callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } } public bool IsAfflictionPending(NetCrewMember character, NetAffliction affliction) { foreach (NetCrewMember crewMember in PendingHeals) { if (!crewMember.CharacterEquals(character)) { continue; } return crewMember.Afflictions.Any(a => a.AfflictionEquals(affliction)); } return false; } private static bool TryDequeue(List> requestQueue, out Action result) { RequestAction? first = requestQueue.FirstOrNull(); if (first is not { } action) { result = static _ => { }; return false; } requestQueue.Remove(action); result = action.Callback; return true; } private static void UpdateQueue(List> requestQueue, DateTimeOffset now, Action> onTimeout) { HashSet>? removals = null; foreach (RequestAction action in requestQueue) { if (action.Timeout < now) { onTimeout.Invoke(action.Callback); removals ??= new HashSet>(); removals.Add(action); } } if (removals is null) { return; } foreach (RequestAction action in removals) { requestQueue.Remove(action); } } private void OnMoneyChanged(WalletChangedEvent e) { if (e.Wallet.IsOwnWallet) { OnUpdate?.Invoke(); } } // if you have more than 5000 ping there are probably more important things to worry about but hey just in case private static DateTimeOffset GetTimeout() => DateTimeOffset.Now.AddSeconds(5).AddMilliseconds(GetPing()); private static int GetPing() { if (GameMain.IsSingleplayer || GameMain.Client?.Name is not { } ownName || GameMain.NetworkMember?.ConnectedClients is not { } clients) { return 0; } return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault(); } public bool TreatAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) { AddEverythingToPending(); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); return true; } return requestBucket.TryEnqueue(() => { addRequests.Add(new RequestAction(onReceived, GetTimeout())); ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable); }); } public bool HealAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) { HealRequestResult result = HealAllPending(); onReceived(new HealRequest(RequestResult.Success, HealAllPending())); if (result == HealRequestResult.Success) { OnUpdate?.Invoke(); } return true; } if (campaign?.CampaignUI?.MedicalClinic is { } openedUi) { openedUi.ClosePopup(); } return requestBucket.TryEnqueue(() => { healAllRequests.Add(new RequestAction(onReceived, GetTimeout())); ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable); }); } public bool ClearAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) { ClearPendingHeals(); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); return true; } return requestBucket.TryEnqueue(() => { clearAllRequests.Add(new RequestAction(onReceived, GetTimeout())); ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable); }); } private void ClearRequestReceived() { ClearPendingHeals(); if (TryDequeue(clearAllRequests, out var callback)) { callback(new CallbackOnlyRequest(RequestResult.Success)); } OnUpdate?.Invoke(); } private void HealRequestReceived(IReadMessage inc) { NetHealRequest request = INetSerializableStruct.Read(inc); if (request.Result == HealRequestResult.Success) { HealAllPending(force: true); } if (TryDequeue(healAllRequests, out var callback)) { callback(new HealRequest(RequestResult.Success, request.Result)); } OnUpdate?.Invoke(); } public bool AddPendingButtonAction(NetCrewMember crewMember, Action onReceived) { if (GameMain.IsSingleplayer) { InsertPendingCrewMember(crewMember); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); return true; } return requestBucket.TryEnqueue(() => { addRequests.Add(new RequestAction(onReceived, GetTimeout())); ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable); }); } public bool RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action onReceived) { if (GameMain.IsSingleplayer) { RemovePendingAffliction(crewMember, affliction); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); return true; } INetSerializableStruct removedAffliction = new NetRemovedAffliction { CrewMember = crewMember, Affliction = affliction }; return requestBucket.TryEnqueue(() => { removeRequests.Add(new RequestAction(onReceived, GetTimeout())); ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable); }); } private void NewAdditionReceived(IReadMessage inc, MessageFlag flag) { var crewMembers = INetSerializableStruct.Read>(inc); foreach (var crewMember in crewMembers) { InsertPendingCrewMember(crewMember); } if (flag == MessageFlag.Response && TryDequeue(addRequests, out var callback)) { callback(new CallbackOnlyRequest(RequestResult.Success)); } OnUpdate?.Invoke(); } private void NewRemovalReceived(IReadMessage inc, MessageFlag flag) { NetRemovedAffliction removed = INetSerializableStruct.Read(inc); RemovePendingAffliction(removed.CrewMember, removed.Affliction); if (flag == MessageFlag.Response && TryDequeue(removeRequests, out var callback)) { callback(new CallbackOnlyRequest(RequestResult.Success)); } OnUpdate?.Invoke(); } private static void SendAfflictionRequest(CharacterInfo info) { INetSerializableStruct crewMember = new NetCrewMember(info); ClientSend(crewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable); } private static void SendPendingRequest() { ClientSend(null, NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable); } private void AfflictionRequestReceived(IReadMessage inc) { NetCrewMember crewMember = INetSerializableStruct.Read(inc); if (TryDequeue(afflictionRequests, out var callback)) { RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.CharacterNotFound : RequestResult.Success; callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray())); } } private void AfflictionUpdateReceived(IReadMessage inc) { NetCrewMember crewMember = INetSerializableStruct.Read(inc); ui?.UpdateAfflictions(crewMember); } private void PendingRequestReceived(IReadMessage inc) { var pendingCrew = INetSerializableStruct.Read>(inc); if (TryDequeue(pendingHealRequests, out var callback)) { callback(new PendingRequest(RequestResult.Success, pendingCrew)); } } public static void SendUnsubscribeRequest() => ClientSend(null, header: NetworkHeader.UNSUBSCRIBE_ME, deliveryMethod: DeliveryMethod.Reliable); private static IWriteMessage StartSending() { IWriteMessage writeMessage = new WriteOnlyMessage(); writeMessage.WriteByte((byte)ClientPacketHeader.MEDICAL); return writeMessage; } private static void ClientSend(INetSerializableStruct? netStruct, NetworkHeader header, DeliveryMethod deliveryMethod) { IWriteMessage msg = StartSending(); msg.WriteByte((byte)header); netStruct?.Write(msg); GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod); } public void ClientRead(IReadMessage inc) { NetworkHeader header = (NetworkHeader)inc.ReadByte(); MessageFlag flag = (MessageFlag)inc.ReadByte(); switch (header) { case NetworkHeader.REQUEST_AFFLICTIONS: AfflictionRequestReceived(inc); break; case NetworkHeader.AFFLICTION_UPDATE: AfflictionUpdateReceived(inc); break; case NetworkHeader.REQUEST_PENDING: PendingRequestReceived(inc); break; case NetworkHeader.ADD_PENDING: NewAdditionReceived(inc, flag); break; case NetworkHeader.REMOVE_PENDING: NewRemovalReceived(inc, flag); break; case NetworkHeader.HEAL_PENDING: HealRequestReceived(inc); break; case NetworkHeader.CLEAR_PENDING: ClearRequestReceived(); break; } } } }