diff --git a/Content.Server/Body/BodyCommands.cs b/Content.Server/Body/BodyCommands.cs index 47da73fb64..3769d78dfe 100644 --- a/Content.Server/Body/BodyCommands.cs +++ b/Content.Server/Body/BodyCommands.cs @@ -93,7 +93,7 @@ namespace Content.Server.Body } else { - body.DisconnectBodyPart(hand.Value, true); + body.RemovePart(hand.Value, true); } } } diff --git a/Content.Server/Body/BodyPreset.cs b/Content.Server/Body/BodyPreset.cs index 2afc863beb..03572fcbb8 100644 --- a/Content.Server/Body/BodyPreset.cs +++ b/Content.Server/Body/BodyPreset.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Content.Shared.Body.Part; using Content.Shared.Body.Preset; +using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.Body @@ -13,24 +14,25 @@ namespace Content.Server.Body /// public class BodyPreset { - public BodyPreset(BodyPresetPrototype data) - { - LoadFromPrototype(data); - } + [ViewVariables] public bool Initialized { get; private set; } - [ViewVariables] public string Name { get; private set; } + [ViewVariables] public string Name { get; protected set; } /// - /// Maps a template slot to the ID of the that should - /// fill it. E.g. "right arm" : "BodyPart.arm.basic_human". + /// Maps a template slot to the ID of the + /// that should fill it. E.g. "right arm" : "BodyPart.arm.basic_human". /// [ViewVariables] - public Dictionary PartIDs { get; private set; } + public Dictionary PartIDs { get; protected set; } - protected virtual void LoadFromPrototype(BodyPresetPrototype data) + public virtual void Initialize(BodyPresetPrototype prototype) { - Name = data.Name; - PartIDs = data.PartIDs; + DebugTools.Assert(!Initialized, $"{nameof(BodyPreset)} {Name} has already been initialized!"); + + Name = prototype.Name; + PartIDs = prototype.PartIDs; + + Initialized = true; } } } diff --git a/Content.Server/Body/BodyTemplate.cs b/Content.Server/Body/BodyTemplate.cs index ae32c02688..88af6ee085 100644 --- a/Content.Server/Body/BodyTemplate.cs +++ b/Content.Server/Body/BodyTemplate.cs @@ -4,6 +4,7 @@ using System.Linq; using Content.Server.GameObjects.Components.Body; using Content.Shared.Body.Template; using Content.Shared.GameObjects.Components.Body; +using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.Body @@ -11,35 +12,22 @@ namespace Content.Server.Body /// /// This class is a data capsule representing the standard format of a /// . - /// For instance, the "humanoid" BodyTemplate defines two arms, each connected to - /// a torso and so on. + /// For instance, the "humanoid" BodyTemplate defines two arms, each + /// connected to a torso and so on. /// Capable of loading data from a . /// public class BodyTemplate { - public BodyTemplate() - { - Name = "empty"; - CenterSlot = ""; - Slots = new Dictionary(); - Connections = new Dictionary>(); - Layers = new Dictionary(); - MechanismLayers = new Dictionary(); - } + [ViewVariables] public bool Initialized { get; private set; } - public BodyTemplate(BodyTemplatePrototype data) - { - LoadFromPrototype(data); - } - - [ViewVariables] public string Name { get; private set; } + [ViewVariables] public string Name { get; private set; } = ""; /// /// The name of the center BodyPart. For humans, this is set to "torso". /// Used in many calculations. /// [ViewVariables] - public string CenterSlot { get; set; } + public string CenterSlot { get; set; } = ""; /// /// Maps all parts on this template to its BodyPartType. @@ -47,7 +35,7 @@ namespace Content.Server.Body /// template. /// [ViewVariables] - public Dictionary Slots { get; private set; } + public Dictionary Slots { get; private set; } = new Dictionary(); /// /// Maps limb name to the list of their connections to other limbs. @@ -58,13 +46,13 @@ namespace Content.Server.Body /// map "left arm" to "torso". /// [ViewVariables] - public Dictionary> Connections { get; private set; } + public Dictionary> Connections { get; private set; } = new Dictionary>(); [ViewVariables] - public Dictionary Layers { get; private set; } + public Dictionary Layers { get; private set; } = new Dictionary(); [ViewVariables] - public Dictionary MechanismLayers { get; private set; } + public Dictionary MechanismLayers { get; private set; } = new Dictionary(); public bool Equals(BodyTemplate other) { @@ -75,7 +63,7 @@ namespace Content.Server.Body /// Checks if the given slot exists in this . /// /// True if it does, false otherwise. - public bool SlotExists(string slotName) + public bool HasSlot(string slotName) { return Slots.Keys.Any(slot => slot == slotName); } @@ -132,14 +120,18 @@ namespace Content.Server.Body return hash; } - protected virtual void LoadFromPrototype(BodyTemplatePrototype data) + public virtual void Initialize(BodyTemplatePrototype prototype) { - Name = data.Name; - CenterSlot = data.CenterSlot; - Slots = data.Slots; - Connections = data.Connections; - Layers = data.Layers; - MechanismLayers = data.MechanismLayers; + DebugTools.Assert(!Initialized, $"{nameof(BodyTemplate)} {Name} has already been initialized!"); + + Name = prototype.Name; + CenterSlot = prototype.CenterSlot; + Slots = new Dictionary(prototype.Slots); + Connections = new Dictionary>(prototype.Connections); + Layers = new Dictionary(prototype.Layers); + MechanismLayers = new Dictionary(prototype.MechanismLayers); + + Initialized = true; } } } diff --git a/Content.Server/Body/Network/BodyNetwork.cs b/Content.Server/Body/Network/BodyNetwork.cs index 911bfbde75..b9ba20c272 100644 --- a/Content.Server/Body/Network/BodyNetwork.cs +++ b/Content.Server/Body/Network/BodyNetwork.cs @@ -31,9 +31,16 @@ namespace Content.Server.Body.Network public virtual void OnRemove() { } /// - /// Called every update by . + /// Called every update by + /// . /// - public virtual void Update(float frameTime) { } + public virtual void PreMetabolism(float frameTime) { } + + /// + /// Called every update by + /// . + /// + public virtual void PostMetabolism(float frameTime) { } } public static class BodyNetworkExtensions diff --git a/Content.Server/Body/Surgery/BiologicalSurgeryData.cs b/Content.Server/Body/Surgery/BiologicalSurgeryData.cs index 30cbd2a56b..49fad56ec4 100644 --- a/Content.Server/Body/Surgery/BiologicalSurgeryData.cs +++ b/Content.Server/Body/Surgery/BiologicalSurgeryData.cs @@ -243,7 +243,7 @@ namespace Content.Server.Body.Surgery performer.PopupMessage(Loc.GetString("Saw off the limb!")); // TODO do_after: Delay - bmTarget.DisconnectBodyPart(Parent, true); + bmTarget.RemovePart(Parent, true); } } } diff --git a/Content.Server/GameObjects/Components/Body/BodyManagerComponent.Parts.cs b/Content.Server/GameObjects/Components/Body/BodyManagerComponent.Parts.cs new file mode 100644 index 0000000000..614cb5eb0e --- /dev/null +++ b/Content.Server/GameObjects/Components/Body/BodyManagerComponent.Parts.cs @@ -0,0 +1,543 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Server.Body; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces.GameObjects.Components.Interaction; +using Content.Shared.Body.Part.Properties.Movement; +using Content.Shared.Body.Part.Properties.Other; +using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Movement; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Log; +using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Body +{ + public partial class BodyManagerComponent + { + private readonly Dictionary _parts = new Dictionary(); + + [ViewVariables] public BodyPreset Preset { get; private set; } = default!; + + /// + /// 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(); + + /// + /// Maps slot name to the + /// object filling it (if there is one). + /// + [ViewVariables] + public IReadOnlyDictionary Parts => _parts; + + /// + /// List of all occupied slots in this body, taken from the values of + /// . + /// + public IEnumerable OccupiedSlots => Parts.Keys; + + /// + /// List of all slots in this body, taken from the keys of + /// slots. + /// + public IEnumerable AllSlots => Template.Slots.Keys; + + public bool TryAddPart(string slot, DroppedBodyPartComponent part, bool force = false) + { + DebugTools.AssertNotNull(part); + + if (!TryAddPart(slot, part.ContainedBodyPart, force)) + { + return false; + } + + part.Owner.Delete(); + return true; + } + + 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 (!HasSlot(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); + } + + public void RemovePart(IBodyPart part, bool drop) + { + DebugTools.AssertNotNull(part); + + var slotName = _parts.FirstOrDefault(x => x.Value == part).Key; + + if (string.IsNullOrEmpty(slotName)) return; + + RemovePart(slotName, drop); + } + + public bool RemovePart(string slot, bool drop) + { + DebugTools.AssertNotNull(slot); + + if (!_parts.Remove(slot, out var part)) + { + return false; + } + + IEntity? dropped = null; + if (drop) + { + part.SpawnDropped(out dropped); + } + + part.Body = null; + + var args = new BodyPartRemovedEventArgs(part, slot); + + 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); + } + + if (CurrentDamageState == DamageState.Dead) return true; + + // creadth: fall down if no legs + if (part.PartType == BodyPartType.Leg && Parts.Count(x => x.Value.PartType == BodyPartType.Leg) == 0) + { + EntitySystem.Get().Down(Owner); + } + + // creadth: immediately kill entity if last vital part removed + if (part.IsVital && Parts.Count(x => x.Value.PartType == part.PartType) == 0) + { + CurrentDamageState = DamageState.Dead; + ForceHealthChangedEvent(); + } + + if (TryGetSlotConnections(slot, out var connections)) + { + foreach (var connectionName in connections) + { + if (TryGetPart(connectionName, out var result) && !ConnectedToCenter(result)) + { + RemovePart(connectionName, drop); + } + } + } + + OnBodyChanged(); + return true; + } + + public bool RemovePart(IBodyPart part, [NotNullWhen(true)] out string? slot) + { + DebugTools.AssertNotNull(part); + + var pair = _parts.FirstOrDefault(kvPair => kvPair.Value == part); + + if (pair.Equals(default)) + { + slot = null; + return false; + } + + slot = pair.Key; + + return RemovePart(slot, false); + } + + public IEntity? DropPart(IBodyPart part) + { + DebugTools.AssertNotNull(part); + + if (!_parts.ContainsValue(part)) + { + return null; + } + + if (!RemovePart(part, out var slotName)) + { + return null; + } + + // Call disconnect on all limbs that were hanging off this limb. + if (TryGetSlotConnections(slotName, out var connections)) + { + // This loop is an unoptimized travesty. TODO: optimize to be less shit + foreach (var connectionName in connections) + { + if (TryGetPart(connectionName, out var result) && !ConnectedToCenter(result)) + { + RemovePart(connectionName, true); + } + } + } + + part.SpawnDropped(out var dropped); + + OnBodyChanged(); + return dropped; + } + + public bool ConnectedToCenter(IBodyPart part) + { + var searchedSlots = new List(); + + return TryGetSlot(part, out var result) && + ConnectedToCenterPartRecursion(searchedSlots, result); + } + + private bool ConnectedToCenterPartRecursion(ICollection searchedSlots, string slotName) + { + if (!TryGetPart(slotName, out var part)) + { + return false; + } + + if (part == CenterPart()) + { + return true; + } + + searchedSlots.Add(slotName); + + if (!TryGetSlotConnections(slotName, out var connections)) + { + return false; + } + + foreach (var connection in connections) + { + if (!searchedSlots.Contains(connection) && + ConnectedToCenterPartRecursion(searchedSlots, connection)) + { + return true; + } + } + + return false; + } + + public IBodyPart? CenterPart() + { + Parts.TryGetValue(Template.CenterSlot, out var center); + return center; + } + + public bool HasSlot(string slot) + { + return Template.HasSlot(slot); + } + + public bool TryGetPart(string slot, [NotNullWhen(true)] out IBodyPart? result) + { + return Parts.TryGetValue(slot, out result); + } + + public bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out string? slot) + { + // 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); + slot = pair.Key; + + return !pair.Equals(default); + } + + public bool TryGetSlotType(string slot, out BodyPartType result) + { + return Template.Slots.TryGetValue(slot, out result); + } + + public bool TryGetSlotConnections(string slot, [NotNullWhen(true)] out List? connections) + { + return Template.Connections.TryGetValue(slot, out connections); + } + + public bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List? result) + { + result = null; + + if (!Template.Connections.TryGetValue(slot, out var connections)) + { + return false; + } + + var toReturn = new List(); + foreach (var connection in connections) + { + if (TryGetPart(connection, out var partResult)) + { + toReturn.Add(partResult); + } + } + + if (toReturn.Count <= 0) + { + return false; + } + + result = toReturn; + return true; + } + + public bool TryGetPartConnections(IBodyPart part, [NotNullWhen(true)] out List? connections) + { + connections = null; + + return TryGetSlot(part, out var slotName) && + TryGetPartConnections(slotName, out connections); + } + + public List GetPartsOfType(BodyPartType type) + { + var toReturn = new List(); + + foreach (var part in Parts.Values) + { + if (part.PartType == type) + { + toReturn.Add(part); + } + } + + return toReturn; + } + + 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) + { + playerMover.BaseWalkSpeed = 0.8f; + playerMover.BaseSprintSpeed = 2.0f; + } + else + { + // 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; + } + } + + /// + /// 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(part); + + if (Math.Abs(footDistance - float.MinValue) > 0.001f) + { + _activeLegs.Add(part, footDistance); + } + } + + CalculateSpeed(); + } + } + + /// + /// 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 . + /// + public float DistanceToNearestFoot(IBodyPart source) + { + if (source.HasProperty() && source.TryGetProperty(out var property)) + { + return property.ReachDistance; + } + + return LookForFootRecursion(source, new List()); + } + + private float LookForFootRecursion(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 (!TryGetPartConnections(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(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. + } + } +} diff --git a/Content.Server/GameObjects/Components/Body/BodyManagerComponent.cs b/Content.Server/GameObjects/Components/Body/BodyManagerComponent.cs index 29329a75dd..13add1a697 100644 --- a/Content.Server/GameObjects/Components/Body/BodyManagerComponent.cs +++ b/Content.Server/GameObjects/Components/Body/BodyManagerComponent.cs @@ -2,16 +2,12 @@ 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; @@ -19,11 +15,8 @@ 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; @@ -39,8 +32,9 @@ namespace Content.Server.GameObjects.Components.Body [RegisterComponent] [ComponentReference(typeof(IDamageableComponent))] [ComponentReference(typeof(ISharedBodyManagerComponent))] + [ComponentReference(typeof(IBodyPartManager))] [ComponentReference(typeof(IBodyManagerComponent))] - public class BodyManagerComponent : SharedBodyManagerComponent, IBodyPartContainer, IRelayMoveInput, IBodyManagerComponent + public partial class BodyManagerComponent : SharedBodyManagerComponent, IBodyPartContainer, IRelayMoveInput, IBodyManagerComponent { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IBodyNetworkFactory _bodyNetworkFactory = default!; @@ -48,41 +42,10 @@ namespace Content.Server.GameObjects.Components.Body [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); @@ -92,14 +55,15 @@ namespace Content.Server.GameObjects.Components.Body "bodyTemplate.Humanoid", template => { - if (!_prototypeManager.TryIndex(template, out BodyTemplatePrototype templateData)) + if (!_prototypeManager.TryIndex(template, out BodyTemplatePrototype prototype)) { // Invalid prototype throw new InvalidOperationException( $"No {nameof(BodyTemplatePrototype)} found with name {template}"); } - Template = new BodyTemplate(templateData); + Template = new BodyTemplate(); + Template.Initialize(prototype); }, () => Template.Name); @@ -108,14 +72,15 @@ namespace Content.Server.GameObjects.Components.Body "bodyPreset.BasicHuman", preset => { - if (!_prototypeManager.TryIndex(preset, out BodyPresetPrototype presetData)) + if (!_prototypeManager.TryIndex(preset, out BodyPresetPrototype prototype)) { // Invalid prototype throw new InvalidOperationException( $"No {nameof(BodyPresetPrototype)} found with name {preset}"); } - Preset = new BodyPreset(presetData); + Preset = new BodyPreset(); + Preset.Initialize(prototype); }, () => _presetName); } @@ -156,7 +121,7 @@ namespace Content.Server.GameObjects.Components.Body } // Try and remove an existing limb if that exists. - RemoveBodyPart(slotName, false); + RemovePart(slotName, false); // Add a new BodyPart with the BodyPartPrototype as a baseline to our // BodyComponent. @@ -167,21 +132,21 @@ namespace Content.Server.GameObjects.Components.Body 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(); - } + // /// + // /// 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 @@ -201,7 +166,7 @@ namespace Content.Server.GameObjects.Components.Body foreach (var network in _networks.Values) { - network.Update(frameTime); + network.PreMetabolism(frameTime); } } @@ -223,73 +188,7 @@ namespace Content.Server.GameObjects.Components.Body 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) - { - playerMover.BaseWalkSpeed = 0.8f; - playerMover.BaseSprintSpeed = 2.0f; - } - else - { - // 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; + network.PostMetabolism(frameTime); } } @@ -301,482 +200,6 @@ namespace Content.Server.GameObjects.Components.Body } } - #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); - - var slotName = _parts.FirstOrDefault(x => x.Value == part).Key; - if (string.IsNullOrEmpty(slotName)) return; - DisconnectBodyPart(slotName, dropEntity); - - } - - /// - /// 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); - } - - if (CurrentDamageState == DamageState.Dead) return true; - - // creadth: fall down if no legs - if (part.PartType == BodyPartType.Leg && Parts.Count(x => x.Value.PartType == BodyPartType.Leg) == 0) - { - EntitySystem.Get().Down(Owner); - } - - // creadth: immediately kill entity if last vital part removed - if (part.IsVital && Parts.Count(x => x.Value.PartType == part.PartType) == 0) - { - CurrentDamageState = DamageState.Dead; - ForceHealthChangedEvent(); - } - - 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) @@ -854,81 +277,6 @@ namespace Content.Server.GameObjects.Components.Body } #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 diff --git a/Content.Server/GameObjects/Components/Body/DroppedBodyPartComponent.cs b/Content.Server/GameObjects/Components/Body/DroppedBodyPartComponent.cs index 8c16794a4f..71dae107ba 100644 --- a/Content.Server/GameObjects/Components/Body/DroppedBodyPartComponent.cs +++ b/Content.Server/GameObjects/Components/Body/DroppedBodyPartComponent.cs @@ -91,7 +91,7 @@ namespace Content.Server.GameObjects.Components.Body { if (!bodyManager.TryGetSlotType(slot, out var typeResult) || typeResult != ContainedBodyPart?.PartType || - !bodyManager.TryGetBodyPartConnections(slot, out var parts)) + !bodyManager.TryGetPartConnections(slot, out var parts)) { continue; } @@ -151,7 +151,7 @@ namespace Content.Server.GameObjects.Components.Body var target = (string) targetObject!; string message; - if (_bodyManagerComponentCache.InstallDroppedBodyPart(this, target)) + if (_bodyManagerComponentCache.TryAddPart(target, this)) { message = Loc.GetString("You attach {0:theName}.", ContainedBodyPart); } diff --git a/Content.Server/GameObjects/Components/Body/IBodyManagerComponent.cs b/Content.Server/GameObjects/Components/Body/IBodyManagerComponent.cs index 26414eefa4..da0c41ab36 100644 --- a/Content.Server/GameObjects/Components/Body/IBodyManagerComponent.cs +++ b/Content.Server/GameObjects/Components/Body/IBodyManagerComponent.cs @@ -6,20 +6,14 @@ using Content.Shared.GameObjects.Components.Body; namespace Content.Server.GameObjects.Components.Body { // TODO: Merge with ISharedBodyManagerComponent - public interface IBodyManagerComponent : ISharedBodyManagerComponent + public interface IBodyManagerComponent : ISharedBodyManagerComponent, IBodyPartManager { /// - /// The that this - /// is adhering to. + /// The that this + /// is adhering to. /// public BodyTemplate Template { get; } - /// - /// The that this - /// is adhering to. - /// - public BodyPreset Preset { get; } - /// /// Installs the given into the given slot. /// diff --git a/Content.Server/GameObjects/Components/Body/IBodyPartManager.cs b/Content.Server/GameObjects/Components/Body/IBodyPartManager.cs new file mode 100644 index 0000000000..86c015e74f --- /dev/null +++ b/Content.Server/GameObjects/Components/Body/IBodyPartManager.cs @@ -0,0 +1,154 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Content.Server.Body; +using Content.Shared.GameObjects.Components.Body; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.GameObjects.Components.Body +{ + public interface IBodyPartManager : IComponent + { + /// + /// The that this + /// + /// is adhering to. + /// + public BodyPreset Preset { get; } + + /// + /// Installs the given into the + /// given slot, deleting the afterwards. + /// + /// True if successful, false otherwise. + bool TryAddPart(string slot, DroppedBodyPartComponent part, bool force = false); + + bool TryAddPart(string slot, IBodyPart part, bool force = false); + + bool HasPart(string slot); + + /// + /// Removes the given reference, potentially + /// dropping other BodyParts if they + /// were hanging off of it. + /// + void RemovePart(IBodyPart part, bool drop); + + /// + /// 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 . + /// + /// True if the part was removed, false otherwise. + bool RemovePart(string slot, bool drop); + + /// + /// 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. + bool RemovePart(IBodyPart part, [NotNullWhen(true)] out string? slotName); + + /// + /// 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. + /// + IEntity? DropPart(IBodyPart part); + + /// + /// Recursively searches for if is connected to + /// the center. + /// + /// The body part to find the center for. + /// True if it is connected to the center, false otherwise. + bool ConnectedToCenter(IBodyPart part); + + /// + /// Finds the central , if any, of this body based on + /// the . For humans, this is the torso. + /// + /// The if one exists, null otherwise. + IBodyPart? CenterPart(); + + /// + /// Returns whether the given part slot name exists within the current + /// . + /// + /// The slot to check for. + /// True if the slot exists in this body, false otherwise. + bool HasSlot(string slot); + + /// + /// Finds the in the given if + /// one exists. + /// + /// The part slot to search in. + /// The body part in that slot, if any. + /// True if found, false otherwise. + bool TryGetPart(string slot, [NotNullWhen(true)] out IBodyPart? 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 + bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out string? slot); + + /// + /// Finds the in the given + /// if one exists. + /// + /// The slot to search in. + /// + /// The of that slot, if any. + /// + /// True if found, false otherwise. + bool TryGetSlotType(string slot, out BodyPartType 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. + bool TryGetSlotConnections(string slot, [NotNullWhen(true)] out List? 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 the slot couldn't be found on this body. + /// + bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List? connections); + + /// + /// Grabs all parts connected to the given , regardless + /// of whether the given is occupied. + /// + /// The part to find connections from. + /// The connected body parts, if any. + /// + /// True if successful, false if the part couldn't be found on this body. + /// + bool TryGetPartConnections(IBodyPart part, [NotNullWhen(true)] out List? connections); + + /// + /// Grabs all of the given type in this body. + /// + List GetPartsOfType(BodyPartType type); + } +} diff --git a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs index 88873c2c2a..17aa781a2f 100644 --- a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs +++ b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs @@ -479,7 +479,7 @@ namespace Content.Server.GameObjects.Components.Kitchen var headCount = 0; if (victim.TryGetComponent(out var bodyManagerComponent)) { - var heads = bodyManagerComponent.GetBodyPartsOfType(BodyPartType.Head); + var heads = bodyManagerComponent.GetPartsOfType(BodyPartType.Head); foreach (var head in heads) { var droppedHead = bodyManagerComponent.DropPart(head); diff --git a/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs b/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs index aafe33baf4..641632a2f8 100644 --- a/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs +++ b/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs @@ -101,8 +101,8 @@ namespace Content.Server.GameObjects.Components.Movement var bodyManager = user.GetComponent(); - if (bodyManager.GetBodyPartsOfType(Shared.GameObjects.Components.Body.BodyPartType.Leg).Count == 0 || - bodyManager.GetBodyPartsOfType(Shared.GameObjects.Components.Body.BodyPartType.Foot).Count == 0) + if (bodyManager.GetPartsOfType(Shared.GameObjects.Components.Body.BodyPartType.Leg).Count == 0 || + bodyManager.GetPartsOfType(Shared.GameObjects.Components.Body.BodyPartType.Foot).Count == 0) { reason = Loc.GetString("You are unable to climb!"); return false;