#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Barotrauma.Extensions; using Barotrauma.Networking; namespace Barotrauma { internal partial class MedicalClinic { public enum RequestResult { Undecided, Success, Error, Timeout } public readonly struct RequestAction { public readonly Action Callback; public readonly DateTimeOffset Timeout; public RequestAction(Action callback, DateTimeOffset timeout) { Callback = callback; Timeout = timeout; } } public readonly struct AfflictionRequest { public readonly RequestResult Result; public readonly ImmutableArray Afflictions; public AfflictionRequest(RequestResult result, ImmutableArray afflictions) { Result = result; Afflictions = afflictions; } } public readonly struct PendingRequest { public readonly RequestResult Result; public readonly ImmutableArray CrewMembers; public PendingRequest(RequestResult result, ImmutableArray crewMembers) { Result = result; CrewMembers = crewMembers; } } public readonly struct CallbackOnlyRequest { public readonly RequestResult Result; public CallbackOnlyRequest(RequestResult result) { Result = result; } } public readonly struct HealRequest { public readonly RequestResult Result; public readonly HealRequestResult HealResult; public HealRequest(RequestResult result, HealRequestResult healResult) { Result = result; HealResult = 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>(); public void 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; } #endif if (!(info is { Character: { CharacterHealth: { } health } })) { onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray.Empty)); return; } ImmutableArray pendingAfflictions = GetAllAfflictions(health).ToImmutableArray(); onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions)); return; } 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; } pendingHealRequests.Add(new RequestAction(onReceived, GetTimeout())); SendPendingRequest(); } public void Update(float deltaTime) { DateTimeOffset now = DateTimeOffset.Now; UpdateQueue(afflictionRequests, now, onTimeout: callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); UpdateQueue(pendingHealRequests, now, onTimeout: callback => { callback(new PendingRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); UpdateQueue(healAllRequests, now, onTimeout: callback => { callback(new HealRequest(RequestResult.Timeout, HealRequestResult.Unknown)); }); UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout); 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 { } action)) { result = _ => { }; 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 { } ownName) || !(GameMain.NetworkMember?.ConnectedClients is { } clients)) { return 0; } return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault(); } public void HealAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) { HealRequestResult result = HealAllPending(); onReceived(new HealRequest(RequestResult.Success, HealAllPending())); if (result == HealRequestResult.Success) { OnUpdate?.Invoke(); } return; } if (campaign?.CampaignUI?.MedicalClinic is { } ui) { ui.ClosePopup(); } healAllRequests.Add(new RequestAction(onReceived, GetTimeout())); ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable); } public void ClearAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) { ClearPendingHeals(); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); return; } clearAllRequests.Add(new RequestAction(onReceived, GetTimeout())); ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable); } private void ClearRequstReceived() { 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 void AddPendingButtonAction(NetCrewMember crewMember, Action onReceived) { if (GameMain.IsSingleplayer) { InsertPendingCrewMember(crewMember); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); return; } addRequests.Add(new RequestAction(onReceived, GetTimeout())); ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable); } public void RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action onReceived) { if (GameMain.IsSingleplayer) { RemovePendingAffliction(crewMember, affliction); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); return; } INetSerializableStruct removedAffliction = new NetRemovedAffliction { CrewMember = crewMember, Affliction = affliction }; removeRequests.Add(new RequestAction(onReceived, GetTimeout())); ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable); } private void NewAdditonReceived(IReadMessage inc, MessageFlag flag) { NetCrewMember crewMember = INetSerializableStruct.Read(inc); 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 { CharacterInfo = info, Afflictions = Array.Empty() }; 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 == 0 ? RequestResult.Error : RequestResult.Success; callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray())); } } private void PendingRequestReceived(IReadMessage inc) { NetPendingCrew pendingCrew = INetSerializableStruct.Read(inc); if (TryDequeue(pendingHealRequests, out var callback)) { callback(new PendingRequest(RequestResult.Success, pendingCrew.CrewMembers.ToImmutableArray())); } } 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.REQUEST_PENDING: PendingRequestReceived(inc); break; case NetworkHeader.ADD_PENDING: NewAdditonReceived(inc, flag); break; case NetworkHeader.REMOVE_PENDING: NewRemovalReceived(inc, flag); break; case NetworkHeader.HEAL_PENDING: HealRequestReceived(inc); break; case NetworkHeader.CLEAR_PENDING: ClearRequstReceived(); break; } } } }