#nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Body.Mechanisms; using Content.Server.Body.Surgery; using Content.Server.GameObjects.Components.Body; using Content.Server.GameObjects.Components.Metabolism; using Content.Shared.Body.Mechanism; using Content.Shared.Body.Part; using Content.Shared.Body.Part.Properties; using Content.Shared.Damage.DamageContainer; using Content.Shared.Damage.ResistanceSet; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Damage; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Reflection; using Robust.Shared.Interfaces.Serialization; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.Body { /// /// Data class representing a singular limb such as an arm or a leg. /// Typically held within either a , /// which coordinates functions between BodyParts, or a /// . /// public class BodyPart : IBodyPart { private IBodyManagerComponent? _body; private readonly HashSet _mechanisms = new HashSet(); public BodyPart(BodyPartPrototype data) { SurgeryData = null!; Properties = new HashSet(); Name = null!; Plural = null!; RSIPath = null!; RSIState = null!; RSIMap = null!; Damage = null!; Resistances = null!; LoadFromPrototype(data); } [ViewVariables] public IBodyManagerComponent? Body { get => _body; set { var old = _body; _body = value; if (value == null && old != null) { foreach (var mechanism in Mechanisms) { mechanism.RemovedFromBody(old); } } else { foreach (var mechanism in Mechanisms) { mechanism.InstalledIntoBody(); } } } } /// /// The class currently representing this BodyPart's /// surgery status. /// [ViewVariables] private SurgeryData SurgeryData { get; set; } /// /// How much space is currently taken up by Mechanisms in this BodyPart. /// [ViewVariables] private int SizeUsed { get; set; } /// /// List of properties, allowing for additional /// data classes to be attached to a limb, such as a "length" class to an arm. /// [ViewVariables] private HashSet Properties { get; } [ViewVariables] public string Name { get; private set; } [ViewVariables] public string Plural { get; private set; } [ViewVariables] public string RSIPath { get; private set; } [ViewVariables] public string RSIState { get; private set; } [ViewVariables] public Enum? RSIMap { get; set; } // TODO: SpriteComponent rework [ViewVariables] public Color? RSIColor { get; set; } [ViewVariables] public BodyPartType PartType { get; private set; } [ViewVariables] public int Size { get; private set; } [ViewVariables] public int MaxDurability { get; private set; } [ViewVariables] public int CurrentDurability => MaxDurability - Damage.TotalDamage; // TODO: Individual body part damage /// /// Current damage dealt to this . /// [ViewVariables] public DamageContainer Damage { get; private set; } /// /// Armor of this against damages. /// [ViewVariables] public ResistanceSet Resistances { get; private set; } /// /// At what HP this destroyed. /// [ViewVariables] public int DestroyThreshold { get; private set; } /// /// What types of BodyParts this can easily attach to. /// For the most part, most limbs aren't universal and require extra work to /// attach between types. /// [ViewVariables] public BodyPartCompatibility Compatibility { get; private set; } /// /// Set of all currently inside this /// . /// [ViewVariables] public IReadOnlyCollection Mechanisms => _mechanisms; /// /// Represents if body part is vital for creature. /// If the last vital body part is removed creature dies /// [ViewVariables] public bool IsVital { get; private set; } /// /// This method is called by /// before /// is called. /// public void PreMetabolism(float frameTime) { foreach (var mechanism in Mechanisms) { mechanism.PreMetabolism(frameTime); } } /// /// This method is called by /// after /// is called. /// public void PostMetabolism(float frameTime) { foreach (var mechanism in Mechanisms) { mechanism.PreMetabolism(frameTime); } } /// /// Attempts to add the given . /// /// /// True if a of that type doesn't exist, /// false otherwise. /// public bool TryAddProperty(BodyPartProperty property) { if (HasProperty(property.GetType())) { return false; } Properties.Add(property); return true; } /// /// Attempts to retrieve the given type. /// The resulting will be null if unsuccessful. /// /// The property if found, null otherwise. /// The type of the property to find. /// True if successful, false otherwise. public bool TryGetProperty([NotNullWhen(true)] out T? property) where T : BodyPartProperty { property = (T?) Properties.FirstOrDefault(x => x.GetType() == typeof(T)); return property != null; } /// /// Attempts to retrieve the given type. /// The resulting will be null if unsuccessful. /// /// True if successful, false otherwise. public bool TryGetProperty(Type propertyType, [NotNullWhen(true)] out BodyPartProperty? property) { property = (BodyPartProperty?) Properties.First(x => x.GetType() == propertyType); return property != null; } /// /// Checks if the given type is on this . /// /// /// The subtype of to look for. /// /// /// True if this has a property of type /// , false otherwise. /// public bool HasProperty() where T : BodyPartProperty { return Properties.Count(x => x.GetType() == typeof(T)) > 0; } /// /// Checks if a subtype of is on this /// . /// /// /// The subtype of to look for. /// /// /// True if this has a property of type /// , false otherwise. /// public bool HasProperty(Type propertyType) { return Properties.Count(x => x.GetType() == propertyType) > 0; } public bool CanAttachPart(IBodyPart part) { return SurgeryData.CanAttachBodyPart(part); } public bool CanInstallMechanism(IMechanism mechanism) { return SizeUsed + mechanism.Size <= Size && SurgeryData.CanInstallMechanism(mechanism); } /// /// Tries to install a mechanism onto this body part. /// Call instead if you want to /// easily install an with a /// . /// /// The mechanism to try to install. /// /// True if successful, false if there was an error /// (e.g. not enough room in ). /// private bool TryInstallMechanism(IMechanism mechanism) { if (!CanInstallMechanism(mechanism)) { return false; } AddMechanism(mechanism); return true; } /// /// Tries to install a into this /// , potentially deleting the dropped /// . /// /// The mechanism to install. /// /// True if successful, false if there was an error /// (e.g. not enough room in ). /// public bool TryInstallDroppedMechanism(DroppedMechanismComponent droppedMechanism) { if (!TryInstallMechanism(droppedMechanism.ContainedMechanism)) { return false; // Installing the mechanism failed for some reason. } droppedMechanism.Owner.Delete(); return true; } public bool TryDropMechanism(IEntity dropLocation, IMechanism mechanismTarget, [NotNullWhen(true)] out DroppedMechanismComponent dropped) { dropped = null!; if (!_mechanisms.Remove(mechanismTarget)) { return false; } SizeUsed -= mechanismTarget.Size; var entityManager = IoCManager.Resolve(); var position = dropLocation.Transform.Coordinates; var mechanismEntity = entityManager.SpawnEntity("BaseDroppedMechanism", position); dropped = mechanismEntity.GetComponent(); dropped.InitializeDroppedMechanism(mechanismTarget); return true; } /// /// Tries to destroy the given in this /// . Does NOT spawn a dropped entity. /// /// /// Tries to destroy the given in this /// . /// /// The mechanism to destroy. /// True if successful, false otherwise. public bool DestroyMechanism(IMechanism mechanismTarget) { if (!RemoveMechanism(mechanismTarget)) { return false; } return true; } public bool SurgeryCheck(SurgeryType surgery) { return SurgeryData.CheckSurgery(surgery); } /// /// Attempts to perform surgery on this with the given /// tool. /// /// True if successful, false if there was an error. public bool AttemptSurgery(SurgeryType toolType, IBodyPartContainer target, ISurgeon surgeon, IEntity performer) { return SurgeryData.PerformSurgery(toolType, target, surgeon, performer); } private void AddMechanism(IMechanism mechanism) { DebugTools.AssertNotNull(mechanism); _mechanisms.Add(mechanism); SizeUsed += mechanism.Size; mechanism.Part = this; mechanism.EnsureInitialize(); if (Body == null) { return; } if (!Body.Template.MechanismLayers.TryGetValue(mechanism.Id, out var mapString)) { return; } if (!IoCManager.Resolve().TryParseEnumReference(mapString, out var @enum)) { Logger.Warning($"Template {Body.Template.Name} has an invalid RSI map key {mapString} for mechanism {mechanism.Id}."); return; } var message = new MechanismSpriteAddedMessage(@enum); Body.Owner.SendNetworkMessage(Body, message); } /// /// Tries to remove the given from this /// . /// /// The mechanism to remove. /// True if it was removed, false otherwise. private bool RemoveMechanism(IMechanism mechanism) { DebugTools.AssertNotNull(mechanism); if (!_mechanisms.Remove(mechanism)) { return false; } SizeUsed -= mechanism.Size; mechanism.Part = null; if (Body == null) { return true; } if (!Body.Template.MechanismLayers.TryGetValue(mechanism.Id, out var mapString)) { return true; } if (!IoCManager.Resolve().TryParseEnumReference(mapString, out var @enum)) { Logger.Warning($"Template {Body.Template.Name} has an invalid RSI map key {mapString} for mechanism {mechanism.Id}."); return true; } var message = new MechanismSpriteRemovedMessage(@enum); Body.Owner.SendNetworkMessage(Body, message); return true; } /// /// Loads the given . /// Current data on this will be overwritten! /// protected virtual void LoadFromPrototype(BodyPartPrototype data) { var prototypeManager = IoCManager.Resolve(); Name = data.Name; Plural = data.Plural; PartType = data.PartType; RSIPath = data.RSIPath; RSIState = data.RSIState; MaxDurability = data.Durability; IsVital = data.IsVital; if (!prototypeManager.TryIndex(data.DamageContainerPresetId, out DamageContainerPrototype damageContainerData)) { throw new InvalidOperationException( $"No {nameof(DamageContainerPrototype)} found with id {data.DamageContainerPresetId}"); } Damage = new DamageContainer(OnHealthChanged, damageContainerData); if (!prototypeManager.TryIndex(data.ResistanceSetId, out ResistanceSetPrototype resistancesData)) { throw new InvalidOperationException( $"No {nameof(ResistanceSetPrototype)} found with id {data.ResistanceSetId}"); } Resistances = new ResistanceSet(resistancesData); Size = data.Size; Compatibility = data.Compatibility; Properties.Clear(); Properties.UnionWith(data.Properties); var surgeryDataType = Type.GetType(data.SurgeryDataName); if (surgeryDataType == null) { throw new InvalidOperationException($"No {nameof(Surgery.SurgeryData)} found with name {data.SurgeryDataName}"); } if (!surgeryDataType.IsSubclassOf(typeof(SurgeryData))) { throw new InvalidOperationException( $"Class {data.SurgeryDataName} is not a subtype of {nameof(Surgery.SurgeryData)} with id {data.ID}"); } SurgeryData = IoCManager.Resolve().CreateInstance(surgeryDataType, new object[] {this}); foreach (var id in data.Mechanisms) { if (!prototypeManager.TryIndex(id, out MechanismPrototype mechanismData)) { throw new InvalidOperationException($"No {nameof(MechanismPrototype)} found with id {id}"); } var mechanism = new Mechanism(mechanismData); AddMechanism(mechanism); } } private void OnHealthChanged(List changes) { // TODO } public bool SpawnDropped([NotNullWhen(true)] out IEntity dropped) { dropped = default!; if (Body == null) { return false; } dropped = IoCManager.Resolve().SpawnEntity("BaseDroppedBodyPart", Body.Owner.Transform.Coordinates); dropped.GetComponent().TransferBodyPartData(this); return true; } } }