#nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Body; using Content.Server.Body.Network; using Content.Server.GameObjects.Components.Metabolism; using Content.Server.GameObjects.EntitySystems; using Content.Server.Interfaces.GameObjects.Components.Interaction; using Content.Server.Observer; using Content.Shared.Body.Part; using Content.Shared.Body.Part.Properties.Movement; using Content.Shared.Body.Part.Properties.Other; using Content.Shared.Body.Preset; using Content.Shared.Body.Template; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Movement; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Reflection; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Body { /// /// Component representing a collection of /// attached to each other. /// [RegisterComponent] [ComponentReference(typeof(IDamageableComponent))] [ComponentReference(typeof(ISharedBodyManagerComponent))] [ComponentReference(typeof(IBodyManagerComponent))] public class BodyManagerComponent : SharedBodyManagerComponent, IBodyPartContainer, IRelayMoveInput, IBodyManagerComponent { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IBodyNetworkFactory _bodyNetworkFactory = default!; [Dependency] private readonly IReflectionManager _reflectionManager = default!; [ViewVariables] private string _presetName = default!; private readonly Dictionary _parts = new Dictionary(); [ViewVariables] private readonly Dictionary _networks = new Dictionary(); /// /// All with /// that are currently affecting move speed, mapped to how big that leg /// they're on is. /// [ViewVariables] private readonly Dictionary _activeLegs = new Dictionary(); [ViewVariables] public BodyTemplate Template { get; private set; } = default!; [ViewVariables] public BodyPreset Preset { get; private set; } = default!; /// /// Maps slot name to the /// object filling it (if there is one). /// [ViewVariables] public IReadOnlyDictionary Parts => _parts; /// /// List of all slots in this body, taken from the keys of /// slots. /// public IEnumerable AllSlots => Template.Slots.Keys; /// /// List of all occupied slots in this body, taken from the values of /// . /// public IEnumerable OccupiedSlots => Parts.Keys; public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); serializer.DataReadWriteFunction( "baseTemplate", "bodyTemplate.Humanoid", template => { if (!_prototypeManager.TryIndex(template, out BodyTemplatePrototype templateData)) { // Invalid prototype throw new InvalidOperationException( $"No {nameof(BodyTemplatePrototype)} found with name {template}"); } Template = new BodyTemplate(templateData); }, () => Template.Name); serializer.DataReadWriteFunction( "basePreset", "bodyPreset.BasicHuman", preset => { if (!_prototypeManager.TryIndex(preset, out BodyPresetPrototype presetData)) { // Invalid prototype throw new InvalidOperationException( $"No {nameof(BodyPresetPrototype)} found with name {preset}"); } Preset = new BodyPreset(presetData); }, () => _presetName); } public override void Initialize() { base.Initialize(); LoadBodyPreset(Preset); } protected override void Startup() { base.Startup(); // Just in case something activates at default health. ForceHealthChangedEvent(); } private void LoadBodyPreset(BodyPreset preset) { _presetName = preset.Name; foreach (var slotName in Template.Slots.Keys) { // For each slot in our BodyManagerComponent's template, // try and grab what the ID of what the preset says should be inside it. if (!preset.PartIDs.TryGetValue(slotName, out var partId)) { // If the preset doesn't define anything for it, continue. continue; } // Get the BodyPartPrototype corresponding to the BodyPart ID we grabbed. if (!_prototypeManager.TryIndex(partId, out BodyPartPrototype newPartData)) { throw new InvalidOperationException($"No {nameof(BodyPartPrototype)} prototype found with ID {partId}"); } // Try and remove an existing limb if that exists. RemoveBodyPart(slotName, false); // Add a new BodyPart with the BodyPartPrototype as a baseline to our // BodyComponent. var addedPart = new BodyPart(newPartData); TryAddPart(slotName, addedPart); } OnBodyChanged(); // TODO: Duplicate code } /// /// Changes the current to the given /// . /// Attempts to keep previous if there is a /// slot for them in both . /// public void ChangeBodyTemplate(BodyTemplatePrototype newTemplate) { foreach (var part in Parts) { // TODO: Make this work. } OnBodyChanged(); } /// /// This method is called by before /// is called. /// public void PreMetabolism(float frameTime) { if (CurrentDamageState == DamageState.Dead) { return; } foreach (var part in Parts.Values) { part.PreMetabolism(frameTime); } foreach (var network in _networks.Values) { network.Update(frameTime); } } /// /// This method is called by after /// is called. /// public void PostMetabolism(float frameTime) { if (CurrentDamageState == DamageState.Dead) { return; } foreach (var part in Parts.Values) { part.PostMetabolism(frameTime); } foreach (var network in _networks.Values) { network.Update(frameTime); } } /// /// Called when the layout of this body changes. /// private void OnBodyChanged() { // Calculate move speed based on this body. if (Owner.HasComponent()) { _activeLegs.Clear(); var legParts = Parts.Values.Where(x => x.HasProperty(typeof(LegProperty))); foreach (var part in legParts) { var footDistance = DistanceToNearestFoot(this, part); if (Math.Abs(footDistance - float.MinValue) > 0.001f) { _activeLegs.Add(part, footDistance); } } CalculateSpeed(); } } private void CalculateSpeed() { if (!Owner.TryGetComponent(out MovementSpeedModifierComponent? playerMover)) { return; } float speedSum = 0; foreach (var part in _activeLegs.Keys) { if (!part.HasProperty()) { _activeLegs.Remove(part); } } foreach (var (key, value) in _activeLegs) { if (key.TryGetProperty(out LegProperty? leg)) { // Speed of a leg = base speed * (1+log1024(leg length)) speedSum += leg.Speed * (1 + (float) Math.Log(value, 1024.0)); } } if (speedSum <= 0.001f || _activeLegs.Count <= 0) { // Case: no way of moving. Fall down. EntitySystem.Get().Down(Owner); playerMover.BaseWalkSpeed = 0.8f; playerMover.BaseSprintSpeed = 2.0f; } else { // Case: have at least one leg. Set move speed. EntitySystem.Get().Standing(Owner); // Extra legs stack diminishingly. // Final speed = speed sum/(leg count-log4(leg count)) playerMover.BaseWalkSpeed = speedSum / (_activeLegs.Count - (float) Math.Log(_activeLegs.Count, 4.0)); playerMover.BaseSprintSpeed = playerMover.BaseWalkSpeed * 1.75f; } } void IRelayMoveInput.MoveInputPressed(ICommonSession session) { if (CurrentDamageState == DamageState.Dead) { new Ghost().Execute(null, (IPlayerSession) session, null); } } #region BodyPart Functions /// /// Recursively searches for if is connected to /// the center. Not efficient (O(n^2)), but most bodies don't have a ton /// of s. /// /// The body part to find the center for. /// True if it is connected to the center, false otherwise. private bool ConnectedToCenterPart(IBodyPart target) { var searchedSlots = new List(); return TryGetSlotName(target, out var result) && ConnectedToCenterPartRecursion(searchedSlots, result); } private bool ConnectedToCenterPartRecursion(ICollection searchedSlots, string slotName) { if (!TryGetBodyPart(slotName, out var part)) { return false; } if (part == GetCenterBodyPart()) { return true; } searchedSlots.Add(slotName); if (!TryGetBodyPartConnections(slotName, out List connections)) { return false; } foreach (var connection in connections) { if (!searchedSlots.Contains(connection) && ConnectedToCenterPartRecursion(searchedSlots, connection)) { return true; } } return false; } /// /// Finds the central , if any, of this body based on /// the . For humans, this is the torso. /// /// The if one exists, null otherwise. private IBodyPart? GetCenterBodyPart() { Parts.TryGetValue(Template.CenterSlot, out var center); return center; } /// /// Returns whether the given slot name exists within the current /// . /// private bool SlotExists(string slotName) { return Template.SlotExists(slotName); } /// /// Finds the in the given if /// one exists. /// /// The slot to search in. /// The body part in that slot, if any. /// True if found, false otherwise. private bool TryGetBodyPart(string slotName, [NotNullWhen(true)] out IBodyPart? result) { return Parts.TryGetValue(slotName, out result!); } /// /// Finds the slotName that the given resides in. /// /// The to find the slot for. /// The slot found, if any. /// True if a slot was found, false otherwise private bool TryGetSlotName(IBodyPart part, [NotNullWhen(true)] out string result) { // 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. var pair = Parts.FirstOrDefault(x => x.Value == part); result = pair.Key; return !pair.Equals(default); } /// /// Finds the in the given /// if one exists. /// /// The slot to search in. /// /// The of that slot, if any. /// /// True if found, false otherwise. public bool TryGetSlotType(string slotName, out BodyPartType result) { return Template.Slots.TryGetValue(slotName, out result); } /// /// Finds the names of all slots connected to the given /// for the template. /// /// The slot to search in. /// The connections found, if any. /// True if the connections are found, false otherwise. private bool TryGetBodyPartConnections(string slotName, [NotNullWhen(true)] out List connections) { return Template.Connections.TryGetValue(slotName, out connections!); } /// /// Grabs all occupied slots connected to the given slot, /// regardless of whether the given is occupied. /// /// The slot name to find connections from. /// The connected body parts, if any. /// /// True if successful, false if there was an error or no connected /// s were found. /// public bool TryGetBodyPartConnections(string slotName, [NotNullWhen(true)] out List result) { result = null!; if (!Template.Connections.TryGetValue(slotName, out var connections)) { return false; } var toReturn = new List(); foreach (var connection in connections) { if (TryGetBodyPart(connection, out var bodyPartResult)) { toReturn.Add(bodyPartResult); } } if (toReturn.Count <= 0) { return false; } result = toReturn; return true; } /// /// Grabs all parts connected to the given , regardless /// of whether the given is occupied. /// /// /// True if successful, false if there was an error or no connected /// s were found. /// private bool TryGetBodyPartConnections(IBodyPart part, [NotNullWhen(true)] out List result) { result = null!; return TryGetSlotName(part, out var slotName) && TryGetBodyPartConnections(slotName, out result); } /// /// Grabs all of the given type in this body. /// public List GetBodyPartsOfType(BodyPartType type) { var toReturn = new List(); foreach (var part in Parts.Values) { if (part.PartType == type) { toReturn.Add(part); } } return toReturn; } /// /// Installs the given into the /// given slot, deleting the afterwards. /// /// True if successful, false otherwise. public bool InstallDroppedBodyPart(DroppedBodyPartComponent part, string slotName) { DebugTools.AssertNotNull(part); if (!TryAddPart(slotName, part.ContainedBodyPart)) { return false; } part.Owner.Delete(); return true; } /// /// Disconnects the given reference, potentially /// dropping other BodyParts if they were hanging /// off of it. /// /// /// The representing the dropped /// , or null if none was dropped. /// public IEntity? DropPart(IBodyPart part) { DebugTools.AssertNotNull(part); if (!_parts.ContainsValue(part)) { return null; } if (!RemoveBodyPart(part, out var slotName)) { return null; } // Call disconnect on all limbs that were hanging off this limb. if (TryGetBodyPartConnections(slotName, out List connections)) { // This loop is an unoptimized travesty. TODO: optimize to be less shit foreach (var connectionName in connections) { if (TryGetBodyPart(connectionName, out var result) && !ConnectedToCenterPart(result)) { DisconnectBodyPart(connectionName, true); } } } part.SpawnDropped(out var dropped); OnBodyChanged(); return dropped; } /// /// Disconnects the given reference, potentially /// dropping other BodyParts if they were hanging /// off of it. /// public void DisconnectBodyPart(IBodyPart part, bool dropEntity) { DebugTools.AssertNotNull(part); if (!_parts.ContainsValue(part)) { return; } var slotName = Parts.FirstOrDefault(x => x.Value == part).Key; RemoveBodyPart(slotName, dropEntity); // Call disconnect on all limbs that were hanging off this limb if (TryGetBodyPartConnections(slotName, out List connections)) { // TODO: Optimize foreach (var connectionName in connections) { if (TryGetBodyPart(connectionName, out var result) && !ConnectedToCenterPart(result)) { DisconnectBodyPart(connectionName, dropEntity); } } } OnBodyChanged(); } /// /// Disconnects a body part in the given slot if one exists, /// optionally dropping it. /// /// The slot to remove the body part from /// /// Whether or not to drop the body part as an entity if it exists. /// private void DisconnectBodyPart(string slotName, bool dropEntity) { DebugTools.AssertNotNull(slotName); if (!HasPart(slotName)) { return; } RemoveBodyPart(slotName, dropEntity); if (TryGetBodyPartConnections(slotName, out List connections)) { foreach (var connectionName in connections) { if (TryGetBodyPart(connectionName, out var result) && !ConnectedToCenterPart(result)) { DisconnectBodyPart(connectionName, dropEntity); } } } OnBodyChanged(); } public bool TryAddPart(string slot, IBodyPart part, bool force = false) { DebugTools.AssertNotNull(part); DebugTools.AssertNotNull(slot); // Make sure the given slot exists if (!force) { if (!SlotExists(slot)) { return false; } // And that nothing is in it if (!_parts.TryAdd(slot, part)) { return false; } } else { _parts[slot] = part; } part.Body = this; var argsAdded = new BodyPartAddedEventArgs(part, slot); foreach (var component in Owner.GetAllComponents().ToArray()) { component.BodyPartAdded(argsAdded); } // TODO: Sort this duplicate out OnBodyChanged(); if (!Template.Layers.TryGetValue(slot, out var partMap) || !_reflectionManager.TryParseEnumReference(partMap, out var partEnum)) { Logger.Warning($"Template {Template.Name} has an invalid RSI map key {partMap} for body part {part.Name}."); return false; } part.RSIMap = partEnum; var partMessage = new BodyPartAddedMessage(part.RSIPath, part.RSIState, partEnum); SendNetworkMessage(partMessage); foreach (var mechanism in part.Mechanisms) { if (!Template.MechanismLayers.TryGetValue(mechanism.Id, out var mechanismMap)) { continue; } if (!_reflectionManager.TryParseEnumReference(mechanismMap, out var mechanismEnum)) { Logger.Warning($"Template {Template.Name} has an invalid RSI map key {mechanismMap} for mechanism {mechanism.Id}."); continue; } var mechanismMessage = new MechanismSpriteAddedMessage(mechanismEnum); SendNetworkMessage(mechanismMessage); } return true; } public bool HasPart(string slot) { return _parts.ContainsKey(slot); } /// /// Removes the body part in slot from this body, /// if one exists. /// /// The slot to remove it from. /// /// Whether or not to drop the removed . /// /// private bool RemoveBodyPart(string slotName, bool drop) { DebugTools.AssertNotNull(slotName); if (!_parts.Remove(slotName, out var part)) { return false; } IEntity? dropped = null; if (drop) { part.SpawnDropped(out dropped); } part.Body = null; var args = new BodyPartRemovedEventArgs(part, slotName); foreach (var component in Owner.GetAllComponents()) { component.BodyPartRemoved(args); } if (part.RSIMap != null) { var message = new BodyPartRemovedMessage(part.RSIMap, dropped?.Uid); SendNetworkMessage(message); } foreach (var mechanism in part.Mechanisms) { if (!Template.MechanismLayers.TryGetValue(mechanism.Id, out var mechanismMap)) { continue; } if (!_reflectionManager.TryParseEnumReference(mechanismMap, out var mechanismEnum)) { Logger.Warning($"Template {Template.Name} has an invalid RSI map key {mechanismMap} for mechanism {mechanism.Id}."); continue; } var mechanismMessage = new MechanismSpriteRemovedMessage(mechanismEnum); SendNetworkMessage(mechanismMessage); } return true; } /// /// Removes the body part from this body, if one exists. /// /// The part to remove from this body. /// The slot that the part was in, if any. /// True if was removed, false otherwise. private bool RemoveBodyPart(IBodyPart part, [NotNullWhen(true)] out string? slotName) { DebugTools.AssertNotNull(part); var pair = _parts.FirstOrDefault(kvPair => kvPair.Value == part); if (pair.Equals(default)) { slotName = null; return false; } slotName = pair.Key; return RemoveBodyPart(slotName, false); } #endregion #region BodyNetwork Functions private bool EnsureNetwork(BodyNetwork network) { DebugTools.AssertNotNull(network); if (_networks.ContainsKey(network.GetType())) { return true; } _networks.Add(network.GetType(), network); network.OnAdd(Owner); return false; } /// /// Attempts to add a of the given type to this body. /// /// /// True if successful, false if there was an error /// (such as passing in an invalid type or a network of that type already /// existing). /// public bool EnsureNetwork(Type networkType) { DebugTools.Assert(networkType.IsSubclassOf(typeof(BodyNetwork))); var network = _bodyNetworkFactory.GetNetwork(networkType); return EnsureNetwork(network); } /// /// Attempts to add a of the given type to /// this body. /// /// The type of network to add. /// /// True if successful, false if there was an error /// (such as passing in an invalid type or a network of that type already /// existing). /// public bool EnsureNetwork() where T : BodyNetwork { return EnsureNetwork(typeof(T)); } public void RemoveNetwork(Type networkType) { DebugTools.AssertNotNull(networkType); if (_networks.Remove(networkType, out var network)) { network.OnRemove(); } } public void RemoveNetwork() where T : BodyNetwork { RemoveNetwork(typeof(T)); } /// /// Attempts to get the of the given type in this body. /// /// The type to search for. /// /// The if found, null otherwise. /// /// True if found, false otherwise. public bool TryGetNetwork(Type networkType, [NotNullWhen(true)] out BodyNetwork result) { return _networks.TryGetValue(networkType, out result!); } #endregion #region Recursion Functions /// /// Returns the combined length of the distance to the nearest with a /// . Returns /// if there is no foot found. If you consider a a node map, then it will look for /// a foot node from the given node. It can /// only search through BodyParts with . /// private static float DistanceToNearestFoot(BodyManagerComponent body, IBodyPart source) { if (source.HasProperty() && source.TryGetProperty(out var property)) { return property.ReachDistance; } return LookForFootRecursion(body, source, new List()); } // TODO: Make this not static and not keep me up at night private static float LookForFootRecursion(BodyManagerComponent body, IBodyPart current, ICollection searchedParts) { if (!current.TryGetProperty(out var extProperty)) { return float.MinValue; } // Get all connected parts if the current part has an extension property if (!body.TryGetBodyPartConnections(current, out var connections)) { return float.MinValue; } // If a connected BodyPart is a foot, return this BodyPart's length. foreach (var connection in connections) { if (!searchedParts.Contains(connection) && connection.HasProperty()) { return extProperty.ReachDistance; } } // Otherwise, get the recursion values of all connected BodyParts and // store them in a list. var distances = new List(); foreach (var connection in connections) { if (!searchedParts.Contains(connection)) { continue; } var result = LookForFootRecursion(body, connection, searchedParts); if (Math.Abs(result - float.MinValue) > 0.001f) { distances.Add(result); } } // If one or more of the searches found a foot, return the smallest one // and add this ones length. if (distances.Count > 0) { return distances.Min() + extProperty.ReachDistance; } return float.MinValue; // No extension property, no go. } #endregion } public interface IBodyManagerHealthChangeParams { BodyPartType Part { get; } } public class BodyManagerHealthChangeParams : HealthChangeParams, IBodyManagerHealthChangeParams { public BodyManagerHealthChangeParams(BodyPartType part) { Part = part; } public BodyPartType Part { get; } } }