#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 NetworkHeader { REQUEST_AFFLICTIONS, REQUEST_PENDING, ADD_PENDING, REMOVE_PENDING, CLEAR_PENDING, HEAL_PENDING } public enum AfflictionSeverity { Low, Medium, High } public enum MessageFlag { Response, // responding to your request Announce // responding to someone else's request } public enum HealRequestResult { Unknown, // everything is not ok Success, // everything ok InsufficientFunds, // not enough money Refused // the outpost has refused to provide medical assistance } [NetworkSerialize] public struct NetHealRequest : INetSerializableStruct { public HealRequestResult Result; } [NetworkSerialize] public struct NetRemovedAffliction : INetSerializableStruct { public NetCrewMember CrewMember; public NetAffliction Affliction; } public struct NetPendingCrew : INetSerializableStruct { [NetworkSerialize(ArrayMaxSize = CrewManager.MaxCrewSize)] public NetCrewMember[] CrewMembers; } public struct NetAffliction : INetSerializableStruct { [NetworkSerialize] public Identifier Identifier; [NetworkSerialize] public ushort Strength; [NetworkSerialize] public ushort Price; public AfflictionSeverity AfflictionSeverity { get { if (Prefab is null) { return AfflictionSeverity.Low; } float normalizedStrength = Strength / Prefab.MaxStrength; // lesser than 0.1 if (normalizedStrength <= 0.1) { return AfflictionSeverity.Low; } // between 0.1 and 0.5 if (normalizedStrength > 0.1f && normalizedStrength < 0.5f) { return AfflictionSeverity.Medium; } // greater than 0.5 return AfflictionSeverity.High; } } public Affliction Affliction { set { Identifier = value.Identifier; Strength = (ushort)Math.Ceiling(value.Strength); Price = (ushort)(value.Prefab.BaseHealCost + Strength * value.Prefab.HealCostMultiplier); } } private AfflictionPrefab? cachedPrefab; public AfflictionPrefab? Prefab { get { if (cachedPrefab is { } cached) { return cached; } foreach (AfflictionPrefab prefab in AfflictionPrefab.List) { if (prefab.Identifier == Identifier) { cachedPrefab = prefab; return prefab; } } return null; } set { cachedPrefab = value; Identifier = value?.Identifier ?? Identifier.Empty; Strength = 0; Price = 0; } } public readonly bool AfflictionEquals(AfflictionPrefab prefab) { return prefab.Identifier == Identifier; } public readonly bool AfflictionEquals(NetAffliction affliction) { return affliction.Identifier == Identifier; } } public struct NetCrewMember : INetSerializableStruct { [NetworkSerialize] public int CharacterInfoID; [NetworkSerialize] public NetAffliction[] Afflictions; public CharacterInfo CharacterInfo { set => CharacterInfoID = value.GetIdentifierUsingOriginalName(); } public readonly CharacterInfo? FindCharacterInfo(ImmutableArray crew) { foreach (CharacterInfo info in crew) { if (info.GetIdentifierUsingOriginalName() == CharacterInfoID) { return info; } } return null; } public readonly bool CharacterEquals(NetCrewMember crewMember) { return crewMember.CharacterInfoID == CharacterInfoID; } } public readonly List PendingHeals = new List(); public Action? OnUpdate; private readonly CampaignMode? campaign; public MedicalClinic(CampaignMode campaign) { this.campaign = campaign; #if CLIENT campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(MedicalClinic).ToIdentifier(), OnMoneyChanged); #endif } private static bool IsOutpostInCombat() { if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; } IEnumerable crew = GetCrewCharacters().Where(c => c.Character != null).Select(c => c.Character).ToImmutableHashSet(); foreach (Character npc in Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.FriendlyNPC)) { bool isInCombatWithCrew = !npc.IsInstigator && npc.AIController is HumanAIController { ObjectiveManager: { CurrentObjective: AIObjectiveCombat combatObjective } } && crew.Contains(combatObjective.Enemy); if (isInCombatWithCrew) { return true; } } return false; } private HealRequestResult HealAllPending(bool force = false, Client? client = null) { int totalCost = GetTotalCost(); if (!force) { if (IsOutpostInCombat()) { return HealRequestResult.Refused; } if (!(campaign?.TryPurchase(client, totalCost) ?? false)) { return HealRequestResult.InsufficientFunds; } } ImmutableArray crew = GetCrewCharacters(); foreach (NetCrewMember crewMember in PendingHeals) { CharacterInfo? targetCharacter = crewMember.FindCharacterInfo(crew); if (!(targetCharacter?.Character is { CharacterHealth: { } health })) { continue; } foreach (NetAffliction affliction in crewMember.Afflictions) { health.ReduceAfflictionOnAllLimbs(affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength); } } ClearPendingHeals(); return HealRequestResult.Success; } private void ClearPendingHeals() { PendingHeals.Clear(); } private void RemovePendingAffliction(NetCrewMember crewMember, NetAffliction affliction) { foreach (NetCrewMember listMember in PendingHeals.ToList()) { PendingHeals.Remove(listMember); NetCrewMember pendingMember = listMember; if (pendingMember.CharacterEquals(crewMember)) { List newAfflictions = new List(); foreach (NetAffliction pendingAffliction in pendingMember.Afflictions) { if (pendingAffliction.AfflictionEquals(affliction)) { continue; } newAfflictions.Add(pendingAffliction); } pendingMember.Afflictions = newAfflictions.ToArray(); } if (!pendingMember.Afflictions.Any()) { continue; } PendingHeals.Add(pendingMember); } } private void InsertPendingCrewMember(NetCrewMember crewMember) { if (PendingHeals.FirstOrNull(m => m.CharacterEquals(crewMember)) is { } foundHeal) { PendingHeals.Remove(foundHeal); } PendingHeals.Add(crewMember); } public static bool IsHealable(Affliction affliction) { return affliction.Prefab.HealableInMedicalClinic && affliction.Strength > GetShowTreshold(affliction); static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold)); } private NetAffliction[] GetAllAfflictions(CharacterHealth health) { IEnumerable rawAfflictions = health.GetAllAfflictions().Where(a => IsHealable(a)); List afflictions = new List(); foreach (Affliction affliction in rawAfflictions) { NetAffliction newAffliction; if (afflictions.FirstOrNull(netAffliction => netAffliction.AfflictionEquals(affliction.Prefab)) is { } foundAffliction) { afflictions.Remove(foundAffliction); foundAffliction.Strength += (ushort)affliction.Strength; foundAffliction.Price += (ushort)GetAdjustedPrice(GetHealPrice(affliction)); newAffliction = foundAffliction; } else { newAffliction = new NetAffliction { Affliction = affliction }; newAffliction.Price = (ushort)GetAdjustedPrice(newAffliction.Price); } afflictions.Add(newAffliction); } return afflictions.ToArray(); static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength)); } public int GetTotalCost() => PendingHeals.SelectMany(h => h.Afflictions).Aggregate(0, (current, affliction) => current + affliction.Price); private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue; public int GetBalance() => campaign?.GetBalance() ?? 0; public static ImmutableArray GetCrewCharacters() { #if DEBUG && CLIENT if (Screen.Selected is TestScreen) { return TestInfos.ToImmutableArray(); } #endif return Character.CharacterList.Where(c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(c => c.Info).ToImmutableArray(); } #if DEBUG && CLIENT private static readonly CharacterInfo[] TestInfos = { new CharacterInfo(CharacterPrefab.HumanSpeciesName), new CharacterInfo(CharacterPrefab.HumanSpeciesName), new CharacterInfo(CharacterPrefab.HumanSpeciesName), new CharacterInfo(CharacterPrefab.HumanSpeciesName), new CharacterInfo(CharacterPrefab.HumanSpeciesName), new CharacterInfo(CharacterPrefab.HumanSpeciesName), new CharacterInfo(CharacterPrefab.HumanSpeciesName) }; private static readonly NetAffliction[] TestAfflictions = { new NetAffliction { Identifier = "internaldamage".ToIdentifier(), Strength = 80, Price = 10 }, new NetAffliction { Identifier = "blunttrauma".ToIdentifier(), Strength = 50, Price = 10 }, new NetAffliction { Identifier = "lacerations".ToIdentifier(), Strength = 20, Price = 10 }, new NetAffliction { Identifier = "burn".ToIdentifier(), Strength = 10, Price = 10 } }; #endif } }