using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using System; using System.Collections.Generic; using Content.Shared.BodySystem; using Robust.Shared.ViewVariables; using Robust.Shared.Interfaces.GameObjects; using System.Linq; using Content.Server.Interfaces.GameObjects.Components.Interaction; namespace Content.Server.BodySystem { /// /// Component representing a collection of BodyParts attached to each other. /// [RegisterComponent] public class BodyManagerComponent : Component, IBodyPartContainer { public sealed override string Name => "BodyManager"; #pragma warning disable CS0649 [Dependency] private IPrototypeManager _prototypeManager; #pragma warning restore [ViewVariables] private BodyTemplate _template; [ViewVariables] private string _presetName; [ViewVariables] private Dictionary _partDictionary = new Dictionary(); /// /// The that this BodyManagerComponent is adhering to. /// public BodyTemplate Template => _template; /// /// Maps slot name to the object filling it (if there is one). /// public Dictionary PartDictionary => _partDictionary; /// /// List of all occupied slots in this body, taken from the values of _parts. /// public IEnumerable AllSlots { get { return _template.Slots.Keys; } } /// /// List of all occupied slots in this body, taken from the values of _parts. /// public IEnumerable OccupiedSlots { get { return _partDictionary.Keys; } } /// /// List of all BodyParts in this body, taken from the keys of _parts. /// public IEnumerable Parts { get { return _partDictionary.Values; } } /// /// Recursive search that returns whether a given is connected to the center . /// Not efficient (O(n^2)), but most bodies don't have a ton of BodyParts. /// public bool ConnectedToCenterPart(BodyPart target) { List searchedSlots = new List { }; if (TryGetSlotName(target, out string result)) return false; return ConnectedToCenterPartRecursion(searchedSlots, result); } private bool ConnectedToCenterPartRecursion(List searchedSlots, string slotName) { TryGetBodyPart(slotName, out BodyPart part); if (part != null && part == GetCenterBodyPart()) return true; searchedSlots.Add(slotName); if (TryGetBodyPartConnections(slotName, out List connections)) { foreach (string connection in connections) { if (!searchedSlots.Contains(connection) && ConnectedToCenterPartRecursion(searchedSlots, connection)) return true; } } return false; } /// /// Returns the central of this body based on the . For humans, this is the torso. Returns null if not found. /// public BodyPart GetCenterBodyPart() { _partDictionary.TryGetValue(_template.CenterSlot, out BodyPart center); return center; } /// /// Returns whether the given slot name exists within the current . /// public bool SlotExists(string slotName) { return _template.SlotExists(slotName); } /// /// Grabs the in the given slotName if there is one. Returns true if a is found, /// false otherwise. If false, result will be null. /// public bool TryGetBodyPart(string slotName, out BodyPart result) { return _partDictionary.TryGetValue(slotName, out result); } /// /// Grabs the slotName that the given resides in. Returns true if the is /// part of this body and a slot is found, false otherwise. If false, result will be null. /// public bool TryGetSlotName(BodyPart part, out string result) { result = _partDictionary.FirstOrDefault(x => x.Value == part).Key; //We enforce that there is only one of each value in the dictionary, so we can iterate through the dictionary values to get the key from there. return result == null; } /// /// Grabs the of the given slotName if there is one. Returns true if the slot is found, false otherwise. If false, result will be null. /// public bool TryGetSlotType(string slotName, out BodyPartType result) { return _template.Slots.TryGetValue(slotName, out result); } /// /// Grabs the names of all connected slots to the given slotName from the template. Returns true if connections are found to the slotName, false otherwise. If false, connections will be null. /// public bool TryGetBodyPartConnections(string slotName, out List connections) { return _template.Connections.TryGetValue(slotName, out connections); } /// /// Grabs all occupied slots connected to the given slot, regardless of whether the target slot is occupied. Returns true if successful, false if there was an error or no connected BodyParts were found. /// public bool TryGetBodyPartConnections(string slotName, out List result) { result = null; if (!_template.Connections.TryGetValue(slotName, out List connections)) return false; List toReturn = new List(); foreach (string connection in connections) { if (TryGetBodyPart(connection, out BodyPart bodyPartResult)) { toReturn.Add(bodyPartResult); } } if (toReturn.Count <= 0) return false; result = toReturn; return true; } ///////// ///////// Server-specific stuff ///////// public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); serializer.DataReadWriteFunction( "BaseTemplate", "bodyTemplate.Humanoid", template => { if (!_prototypeManager.TryIndex(template, out BodyTemplatePrototype templateData)) { throw new InvalidOperationException("No BodyTemplatePrototype was found with the name " + template + " while loading a BodyTemplate!"); //Should never happen unless you fuck up the prototype. } _template = new BodyTemplate(templateData); }, () => _template.Name); serializer.DataReadWriteFunction( "BasePreset", "bodyPreset.BasicHuman", preset => { if (!_prototypeManager.TryIndex(preset, out BodyPresetPrototype presetData)) { throw new InvalidOperationException("No BodyPresetPrototype was found with the name " + preset + " while loading a BodyPreset!"); //Should never happen unless you fuck up the prototype. } LoadBodyPreset(new BodyPreset(presetData)); }, () => _presetName); } /// /// Loads the given - forcefully changes all limbs found in both the preset and this template! /// public void LoadBodyPreset(BodyPreset preset) { _presetName = preset.Name; foreach (var (slotName, type) in _template.Slots) { if (!preset.PartIDs.TryGetValue(slotName, out string partID)) { //For each slot in our BodyManagerComponent's template, try and grab what the ID of what the preset says should be inside it. continue; //If the preset doesn't define anything for it, continue. } if (!_prototypeManager.TryIndex(partID, out BodyPartPrototype newPartData)) { //Get the BodyPartPrototype corresponding to the BodyPart ID we grabbed. throw new InvalidOperationException("BodyPart prototype with ID " + partID + " could not be found!"); } //Try and remove an existing limb if that exists. if (_partDictionary.Remove(slotName, out var removedPart)) { BodyPartRemoved(removedPart, slotName); } var addedPart = new BodyPart(newPartData); _partDictionary.Add(slotName, addedPart); //Add a new BodyPart with the BodyPartPrototype as a baseline to our BodyComponent. BodyPartAdded(addedPart, slotName); } } /// /// Changes the current to the given . Attempts to keep previous BodyParts /// if there is a slot for them in both . /// public void ChangeBodyTemplate(BodyTemplatePrototype newTemplate) { foreach (KeyValuePair part in _partDictionary) { //TODO: Make this work. } } /// /// Grabs all BodyParts of the given type in this body. /// public List GetBodyPartsOfType(BodyPartType type) { List toReturn = new List(); foreach (var (slotName, bodyPart) in _partDictionary) { if (bodyPart.PartType == type) toReturn.Add(bodyPart); } return toReturn; } /// /// Installs the given into the given slot. Returns true if successful, false otherwise. /// public bool InstallBodyPart(BodyPart part, string slotName) { if (!SlotExists(slotName)) //Make sure the given slot exists return false; if (TryGetBodyPart(slotName, out BodyPart result)) //And that nothing is in it return false; _partDictionary.Add(slotName, part); BodyPartAdded(part, slotName); return true; } /// /// Installs the given into the given slot, deleting the afterwards. Returns true if successful, false otherwise. /// public bool InstallDroppedBodyPart(DroppedBodyPartComponent part, string slotName) { if (!InstallBodyPart(part.ContainedBodyPart, slotName)) return false; part.Owner.Delete(); return true; } /// /// Disconnects the given reference, potentially dropping other BodyParts /// if they were hanging off it. Returns the IEntity representing the dropped BodyPart. /// public IEntity DropBodyPart(BodyPart part) { if (!_partDictionary.ContainsValue(part)) return null; if (part != null) { string slotName = _partDictionary.FirstOrDefault(x => x.Value == part).Key; _partDictionary.Remove(slotName); if (TryGetBodyPartConnections(slotName, out List connections)) //Call disconnect on all limbs that were hanging off this limb. { foreach (string connectionName in connections) //This loop is an unoptimized travesty. TODO: optimize to be less shit { if (TryGetBodyPart(connectionName, out BodyPart result) && !ConnectedToCenterPart(result)) { DisconnectBodyPartByName(connectionName, true); } } } var partEntity = Owner.EntityManager.SpawnEntity("BaseDroppedBodyPart", Owner.Transform.GridPosition); partEntity.GetComponent().TransferBodyPartData(part); return partEntity; } return null; } /// /// Disconnects the given reference, potentially dropping other BodyParts if they were hanging off it. /// public void DisconnectBodyPart(BodyPart part, bool dropEntity) { if (!_partDictionary.ContainsValue(part)) return; if (part != null) { string slotName = _partDictionary.FirstOrDefault(x => x.Value == part).Key; if (_partDictionary.Remove(slotName, out var partRemoved)) { BodyPartRemoved(partRemoved, slotName); } if (TryGetBodyPartConnections(slotName, out List connections)) //Call disconnect on all limbs that were hanging off this limb. { foreach (string connectionName in connections) //This loop is an unoptimized travesty. TODO: optimize to be less shit { if (TryGetBodyPart(connectionName, out BodyPart result) && !ConnectedToCenterPart(result)) { DisconnectBodyPartByName(connectionName, dropEntity); } } } if (dropEntity) { var partEntity = Owner.EntityManager.SpawnEntity("BaseDroppedBodyPart", Owner.Transform.GridPosition); partEntity.GetComponent().TransferBodyPartData(part); } } } /// /// Internal string version of DisconnectBodyPart for performance purposes. Yes, it is actually more performant. /// private void DisconnectBodyPartByName(string name, bool dropEntity) { if (!TryGetBodyPart(name, out BodyPart part)) return; if (part != null) { if (_partDictionary.Remove(name, out var partRemoved)) { BodyPartRemoved(partRemoved, name); } if (TryGetBodyPartConnections(name, out List connections)) { foreach (string connectionName in connections) { if (TryGetBodyPart(connectionName, out BodyPart result) && !ConnectedToCenterPart(result)) { DisconnectBodyPartByName(connectionName, dropEntity); } } } if (dropEntity) { var partEntity = Owner.EntityManager.SpawnEntity("BaseDroppedBodyPart", Owner.Transform.GridPosition); partEntity.GetComponent().TransferBodyPartData(part); } } } private void BodyPartAdded(BodyPart part, string slotName) { var argsAdded = new BodyPartAddedEventArgs(part, slotName); foreach (var component in Owner.GetAllComponents().ToArray()) { component.BodyPartAdded(argsAdded); } } private void BodyPartRemoved(BodyPart part, string slotName) { var args = new BodyPartRemovedEventArgs(part, slotName); foreach (var component in Owner.GetAllComponents()) { component.BodyPartRemoved(args); } } } }