#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.Server.GameObjects; 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.Map; 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 { /// /// The body that this body part is in, if any. /// private BodyManagerComponent? _body; /// /// Set of all currently inside this /// . /// To add and remove from this list see and /// /// 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); } /// /// The body that this body part is in, if any. /// [ViewVariables] public BodyManagerComponent? 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; } /// /// The name of this , often displayed to the user. /// For example, it could be named "advanced robotic arm". /// [ViewVariables] public string Name { get; private set; } /// /// Plural version of this name. /// [ViewVariables] public string Plural { get; private set; } /// /// Path to the RSI that represents this . /// [ViewVariables] public string RSIPath { get; private set; } /// /// RSI state that represents this . /// [ViewVariables] public string RSIState { get; private set; } /// /// RSI map keys that this body part changes on the sprite. /// [ViewVariables] public Enum? RSIMap { get; set; } /// /// RSI color of this body part. /// // TODO: SpriteComponent rework public Color? RSIColor { get; set; } /// /// that this is considered /// to be. /// For example, . /// [ViewVariables] public BodyPartType PartType { get; private set; } /// /// Determines many things: how many mechanisms can be fit inside this /// , whether a body can fit through tiny crevices, etc. /// [ViewVariables] private int Size { get; set; } /// /// Max HP of this . /// [ViewVariables] public int MaxDurability { get; private set; } /// /// Current HP of this based on sum of all damage types. /// [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; /// /// 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(out T property) { property = (T) Properties.First(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, 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; } /// /// Checks if another can be connected to this one. /// /// The part to connect. /// True if it can be connected, false otherwise. public bool CanAttachBodyPart(BodyPart toBeConnected) { return SurgeryData.CanAttachBodyPart(toBeConnected); } /// /// Checks if a can be installed on this /// . /// /// True if it can be installed, false otherwise. public bool CanInstallMechanism(Mechanism 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(Mechanism 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; } /// /// Tries to remove the given reference from /// this . /// /// /// The newly spawned , or null /// if there was an error in spawning the entity or removing the mechanism. /// public bool TryDropMechanism(IEntity dropLocation, Mechanism 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.GridPosition; 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(Mechanism mechanismTarget) { if (!RemoveMechanism(mechanismTarget)) { return false; } return true; } /// /// Checks if the given can be used on /// the current state of this . /// /// True if it can be used, false otherwise. public bool SurgeryCheck(SurgeryType toolType) { return SurgeryData.CheckSurgery(toolType); } /// /// 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(Mechanism 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(Mechanism 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; 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.GridPosition); dropped.GetComponent().TransferBodyPartData(this); return true; } } }