#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;
}
}
}