#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; using Barotrauma.Extensions; using Barotrauma.Networking; namespace Barotrauma { internal partial class MedicalClinic { // allow 20 requests per 5 seconds, announce to chat if the limit is reached private readonly RateLimiter rateLimiter = new( maxRequests: RateLimitMaxRequests, expiryInSeconds: RateLimitExpiry, punishmentRules: (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce)); private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry); private readonly List afflictionSubscribers = new(); public void Update(float deltaTime) { processAfflictionChangesTimer -= deltaTime; if (processAfflictionChangesTimer <= 0.0f) { foreach (var character in charactersWithAfflictionChanges) { ImmutableArray afflictions = GetAllAfflictions(character.CharacterHealth); foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) { if (sub.Expiry < DateTimeOffset.Now) { afflictionSubscribers.Remove(sub); continue; } if (sub.Target == character.Info) { ServerSend(new NetCrewMember(character.Info, afflictions), header: NetworkHeader.AFFLICTION_UPDATE, deliveryMethod: DeliveryMethod.Unreliable, targetClient: sub.Subscriber); } } } charactersWithAfflictionChanges.Clear(); processAfflictionChangesTimer = ProcessAfflictionChangesInterval; } } public void ServerRead(IReadMessage inc, Client sender) { NetworkHeader header = (NetworkHeader)inc.ReadByte(); switch (header) { case NetworkHeader.ADD_EVERYTHING_TO_PENDING: ProcessAddEverything(sender); break; case NetworkHeader.UNSUBSCRIBE_ME: RemoveClientSubscription(sender); break; case NetworkHeader.REQUEST_AFFLICTIONS: ProcessRequestedAfflictions(inc, sender); break; case NetworkHeader.REQUEST_PENDING: ProcessRequestedPending(sender); break; case NetworkHeader.ADD_PENDING: ProcessNewAddition(inc, sender); break; case NetworkHeader.REMOVE_PENDING: ProcessNewRemoval(inc, sender); break; case NetworkHeader.HEAL_PENDING: ProcessHealing(sender); break; case NetworkHeader.CLEAR_PENDING: ProcessClearing(sender); break; } } private void ProcessNewAddition(IReadMessage inc, Client client) { if (rateLimiter.IsLimitReached(client)) { return; } NetCrewMember newCrewMember = INetSerializableStruct.Read(inc); InsertPendingCrewMember(newCrewMember); ServerSend(new NetCollection(newCrewMember), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); } private void ProcessAddEverything(Client client) { if (rateLimiter.IsLimitReached(client)) { return; } AddEverythingToPending(); ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); } private void RemoveClientSubscription(Client client) { foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) { if (sub.Subscriber == client || sub.Expiry < DateTimeOffset.Now) { afflictionSubscribers.Remove(sub); } } } private void ProcessNewRemoval(IReadMessage inc, Client client) { if (rateLimiter.IsLimitReached(client)) { return; } NetRemovedAffliction removed = INetSerializableStruct.Read(inc); RemovePendingAffliction(removed.CrewMember, removed.Affliction); ServerSend(removed, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable, reponseClient: client); } private void ProcessRequestedPending(Client client) { if (rateLimiter.IsLimitReached(client)) { return; } ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); } private void ProcessHealing(Client client) { if (rateLimiter.IsLimitReached(client)) { return; } HealRequestResult result = HealAllPending(client: client); ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client); } private void ProcessClearing(Client client) { if (rateLimiter.IsLimitReached(client)) { return; } if (!PendingHeals.Any()) { return; } ClearPendingHeals(); ServerSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable, reponseClient: client); } private void ProcessRequestedAfflictions(IReadMessage inc, Client client) { if (rateLimiter.IsLimitReached(client)) { return; } NetCrewMember crewMember = INetSerializableStruct.Read(inc); CharacterInfo? foundInfo = crewMember.FindCharacterInfo(GetCrewCharacters()); ImmutableArray pendingAfflictions = ImmutableArray.Empty; int infoId = 0; if (foundInfo is null) { StringBuilder sb = new(); foreach (CharacterInfo character in GetCrewCharacters()) { sb.AppendLine($" - {character.DisplayName} ({character.ID})"); } DebugConsole.ThrowError($"Could not find the requested crew member with ID {crewMember.CharacterInfoID}.\n{sb}"); } if (foundInfo is { Character.CharacterHealth: { } health }) { pendingAfflictions = GetAllAfflictions(health); infoId = foundInfo.ID; } INetSerializableStruct writeCrewMember = new NetCrewMember { CharacterInfoID = infoId, Afflictions = pendingAfflictions }; if (foundInfo is not null) { RemoveClientSubscription(client); // the client subscribes to the afflictions of the crew member for the next minute afflictionSubscribers.Add(new AfflictionSubscriber(client, foundInfo, DateTimeOffset.Now.AddMinutes(1))); } ServerSend(writeCrewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable, client); } private IWriteMessage StartSending() { IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ServerPacketHeader.MEDICAL); return msg; } private void ServerSend(INetSerializableStruct? netStruct, NetworkHeader header, DeliveryMethod deliveryMethod, Client? targetClient = null, Client? reponseClient = null) { if (targetClient is null) { foreach (Client c in GameMain.Server.ConnectedClients) { SendToClient(c); } return; } SendToClient(targetClient); void SendToClient(Client c) { MessageFlag flag = MessageFlag.Announce; if (reponseClient != null && reponseClient == c) { flag = MessageFlag.Response; } IWriteMessage msg = StartSending(); msg.WriteByte((byte)header); msg.WriteByte((byte)flag); netStruct?.Write(msg); GameMain.Server.ServerPeer.Send(msg, c.Connection, deliveryMethod); } } } }