diff --git a/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerDisplay.cs b/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerDisplay.cs index 4efe1a96f3..119d31d21b 100644 --- a/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerDisplay.cs +++ b/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerDisplay.cs @@ -8,7 +8,6 @@ using Robust.Client.UserInterface.CustomControls; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; -using Robust.Shared.Maths; using static Robust.Client.UserInterface.Controls.ItemList; namespace Content.Client.GameObjects.Components.Body.Scanner @@ -114,9 +113,9 @@ namespace Content.Client.GameObjects.Components.Body.Scanner return; } - foreach (var slotName in body.Parts.Keys) + foreach (var (part, _) in body.Parts) { - BodyPartList.AddItem(Loc.GetString(slotName)); + BodyPartList.AddItem(Loc.GetString(part.Name)); } } @@ -129,12 +128,12 @@ namespace Content.Client.GameObjects.Components.Body.Scanner return; } - var slot = body.SlotAt(args.ItemIndex).Key; - _currentBodyPart = body.PartAt(args.ItemIndex).Value; + var slot = body.SlotAt(args.ItemIndex); + _currentBodyPart = body.PartAt(args.ItemIndex).Key; - if (body.Parts.TryGetValue(slot, out var part)) + if (slot.Part != null) { - UpdateBodyPartBox(part, slot); + UpdateBodyPartBox(slot.Part, slot.Id); } } diff --git a/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs b/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs index 6ee722c792..7c67aa3650 100644 --- a/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs +++ b/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs @@ -55,7 +55,7 @@ namespace Content.Client.GameObjects.Components.Mobs if (Owner.TryGetComponent(out IBody? body)) { - foreach (var part in body.Parts.Values) + foreach (var (part, _) in body.Parts) { if (!part.Owner.TryGetComponent(out SpriteComponent? partSprite)) { diff --git a/Content.IntegrationTests/Tests/Body/MechanismBehaviorEventsTest.cs b/Content.IntegrationTests/Tests/Body/MechanismBehaviorEventsTest.cs index 808ecb51c5..e25c6e7af2 100644 --- a/Content.IntegrationTests/Tests/Body/MechanismBehaviorEventsTest.cs +++ b/Content.IntegrationTests/Tests/Body/MechanismBehaviorEventsTest.cs @@ -131,7 +131,7 @@ namespace Content.IntegrationTests.Tests.Body Assert.That(human.TryGetComponent(out IBody? body)); Assert.NotNull(body); - var centerPart = body!.CenterPart(); + var centerPart = body!.CenterPart; Assert.NotNull(centerPart); Assert.That(body.TryGetSlot(centerPart!, out var centerSlot)); @@ -196,7 +196,7 @@ namespace Content.IntegrationTests.Tests.Body behavior.ResetAll(); - body.TryAddPart(centerSlot!, centerPart, true); + body.SetPart(centerSlot!.Id, centerPart); Assert.That(behavior.WasAddedToBody); Assert.False(behavior.WasAddedToPart); diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs index f63a39bc4b..3343380a5e 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs @@ -78,7 +78,8 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking // Test to ensure a player with 4 hands will still only have 2 hands cuffed AddHand(cuffed.Owner); AddHand(cuffed.Owner); - Assert.True(cuffed.CuffedHandCount == 2 && hands.Hands.Count() == 4, "Player doesn't have correct amount of hands cuffed"); + Assert.That(cuffed.CuffedHandCount, Is.EqualTo(2)); + Assert.That(hands.Hands.Count(), Is.EqualTo(4)); // Test to give a player with 4 hands 2 sets of cuffs cuffed.TryAddNewCuffs(human, secondCuffs); diff --git a/Content.Server/Commands/AttachBodyPartCommand.cs b/Content.Server/Commands/AttachBodyPartCommand.cs index 2dfe28136a..5539d61c20 100644 --- a/Content.Server/Commands/AttachBodyPartCommand.cs +++ b/Content.Server/Commands/AttachBodyPartCommand.cs @@ -1,6 +1,5 @@ #nullable enable using Content.Server.Administration; -using Content.Server.GameObjects.Components.Body.Part; using Content.Shared.Administration; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body.Part; @@ -8,6 +7,7 @@ using Robust.Server.Player; using Robust.Shared.Console; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using static Content.Server.GameObjects.Components.Body.Part.BodyPartComponent; namespace Content.Server.Commands { @@ -100,7 +100,7 @@ namespace Content.Server.Commands return; } - body.TryAddPart($"{nameof(BodyPartComponent.AttachBodyPartVerb)}-{partEntity.Uid}", part, true); + body.SetPart($"{nameof(AttachBodyPartVerb)}-{partEntity.Uid}", part); } } } diff --git a/Content.Server/Commands/Body/AddHandCommand.cs b/Content.Server/Commands/Body/AddHandCommand.cs index 8b854cf3e2..fdf99d0780 100644 --- a/Content.Server/Commands/Body/AddHandCommand.cs +++ b/Content.Server/Commands/Body/AddHandCommand.cs @@ -138,11 +138,9 @@ namespace Content.Server.Commands.Body } var slot = part.GetHashCode().ToString(); - var response = body.TryAddPart(slot, part, true) - ? $"Added hand to entity {entity.Name}" - : $"Error occurred trying to add a hand to entity {entity.Name}"; + body.SetPart(slot, part); - shell.WriteLine(response); + shell.WriteLine($"Added hand to entity {entity.Name}"); } } } diff --git a/Content.Server/Commands/Body/DestroyMechanismCommand.cs b/Content.Server/Commands/Body/DestroyMechanismCommand.cs index 2e3a2fec02..d78f8fbc56 100644 --- a/Content.Server/Commands/Body/DestroyMechanismCommand.cs +++ b/Content.Server/Commands/Body/DestroyMechanismCommand.cs @@ -48,7 +48,7 @@ namespace Content.Server.Commands.Body var mechanismName = string.Join(" ", args).ToLowerInvariant(); - foreach (var part in body.Parts.Values) + foreach (var (part, _) in body.Parts) foreach (var mechanism in part.Mechanisms) { if (mechanism.Name.ToLowerInvariant() == mechanismName) diff --git a/Content.Server/Commands/Body/RemoveHandCommand.cs b/Content.Server/Commands/Body/RemoveHandCommand.cs index 086f91f4c6..d5b27c6a0e 100644 --- a/Content.Server/Commands/Body/RemoveHandCommand.cs +++ b/Content.Server/Commands/Body/RemoveHandCommand.cs @@ -42,7 +42,8 @@ namespace Content.Server.Commands.Body return; } - var (_, hand) = body.Parts.FirstOrDefault(x => x.Value.PartType == BodyPartType.Hand); + var hand = body.GetPartsOfType(BodyPartType.Hand).FirstOrDefault(); + if (hand == null) { shell.WriteLine("You have no hands."); diff --git a/Content.Server/GameObjects/Components/Body/Behavior/MechanismExtensions.cs b/Content.Server/GameObjects/Components/Body/Behavior/MechanismExtensions.cs index 453b089c94..8a46a942c0 100644 --- a/Content.Server/GameObjects/Components/Body/Behavior/MechanismExtensions.cs +++ b/Content.Server/GameObjects/Components/Body/Behavior/MechanismExtensions.cs @@ -12,7 +12,7 @@ namespace Content.Server.GameObjects.Components.Body.Behavior { public static bool HasMechanismBehavior(this IBody body) where T : IMechanismBehavior { - return body.Parts.Values.Any(p => p.HasMechanismBehavior()); + return body.Parts.Any(p => p.Key.HasMechanismBehavior()); } public static bool HasMechanismBehavior(this IBodyPart part) where T : IMechanismBehavior @@ -22,7 +22,7 @@ namespace Content.Server.GameObjects.Components.Body.Behavior public static IEnumerable GetMechanismBehaviors(this IBody body) { - foreach (var part in body.Parts.Values) + foreach (var (part, _) in body.Parts) foreach (var mechanism in part.Mechanisms) foreach (var behavior in mechanism.Behaviors.Values) { @@ -46,7 +46,7 @@ namespace Content.Server.GameObjects.Components.Body.Behavior public static IEnumerable GetMechanismBehaviors(this IBody body) where T : class, IMechanismBehavior { - foreach (var part in body.Parts.Values) + foreach (var (part, _) in body.Parts) foreach (var mechanism in part.Mechanisms) foreach (var behavior in mechanism.Behaviors.Values) { diff --git a/Content.Server/GameObjects/Components/Body/BodyComponent.cs b/Content.Server/GameObjects/Components/Body/BodyComponent.cs index a4b8e3c452..3dc6ecb032 100644 --- a/Content.Server/GameObjects/Components/Body/BodyComponent.cs +++ b/Content.Server/GameObjects/Components/Body/BodyComponent.cs @@ -5,11 +5,11 @@ using Content.Server.GameObjects.Components.Observer; using Content.Shared.Audio; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.Body.Slot; using Content.Shared.GameObjects.Components.Mobs.State; using Content.Shared.GameObjects.Components.Movement; using Content.Shared.Utility; using Robust.Server.Console; -using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Console; using Robust.Shared.Containers; @@ -29,20 +29,20 @@ namespace Content.Server.GameObjects.Components.Body { private Container _partContainer = default!; - protected override bool CanAddPart(string slot, IBodyPart part) + protected override bool CanAddPart(string slotId, IBodyPart part) { - return base.CanAddPart(slot, part) && + return base.CanAddPart(slotId, part) && _partContainer.CanInsert(part.Owner); } - protected override void OnAddPart(string slot, IBodyPart part) + protected override void OnAddPart(BodyPartSlot slot, IBodyPart part) { base.OnAddPart(slot, part); _partContainer.Insert(part.Owner); } - protected override void OnRemovePart(string slot, IBodyPart part) + protected override void OnRemovePart(BodyPartSlot slot, IBodyPart part) { base.OnRemovePart(slot, part); @@ -54,21 +54,25 @@ namespace Content.Server.GameObjects.Components.Body { base.Initialize(); - _partContainer = ContainerHelpers.EnsureContainer(Owner, $"{Name}-{nameof(BodyComponent)}"); + _partContainer = Owner.EnsureContainer($"{Name}-{nameof(BodyComponent)}"); + var preset = Preset; - foreach (var (slot, partId) in PartIds) + if (preset != null) { - // Using MapPosition instead of Coordinates here prevents - // a crash within the character preview menu in the lobby - var entity = Owner.EntityManager.SpawnEntity(partId, Owner.Transform.MapPosition); - - if (!entity.TryGetComponent(out IBodyPart? part)) + foreach (var slot in Slots) { - Logger.Error($"Entity {partId} does not have a {nameof(IBodyPart)} component."); - continue; - } + // Using MapPosition instead of Coordinates here prevents + // a crash within the character preview menu in the lobby + var entity = Owner.EntityManager.SpawnEntity(preset.PartIDs[slot.Id], Owner.Transform.MapPosition); - TryAddPart(slot, part, true); + if (!entity.TryGetComponent(out IBodyPart? part)) + { + Logger.Error($"Entity {slot.Id} does not have a {nameof(IBodyPart)} component."); + continue; + } + + SetPart(slot.Id, part); + } } } @@ -79,7 +83,7 @@ namespace Content.Server.GameObjects.Components.Body // This is ran in Startup as entities spawned in Initialize // are not synced to the client since they are assumed to be // identical on it - foreach (var part in Parts.Values) + foreach (var (part, _) in Parts) { part.Dirty(); } diff --git a/Content.Server/GameObjects/Components/Body/MechanismComponent.cs b/Content.Server/GameObjects/Components/Body/MechanismComponent.cs index 1fe339b346..36e47a42ed 100644 --- a/Content.Server/GameObjects/Components/Body/MechanismComponent.cs +++ b/Content.Server/GameObjects/Components/Body/MechanismComponent.cs @@ -68,13 +68,13 @@ namespace Content.Server.GameObjects.Components.Body // Create dictionary to send to client (text to be shown : data sent back if selected) var toSend = new Dictionary(); - foreach (var (key, value) in body.Parts) + foreach (var (part, slot) in body.Parts) { // For each limb in the target, add it to our cache if it is a valid option. - if (value.CanAddMechanism(this)) + if (part.CanAddMechanism(this)) { - OptionsCache.Add(IdHash, value); - toSend.Add(key + ": " + value.Name, IdHash++); + OptionsCache.Add(IdHash, slot); + toSend.Add(part + ": " + part.Name, IdHash++); } } diff --git a/Content.Server/GameObjects/Components/Body/Part/BodyPartComponent.cs b/Content.Server/GameObjects/Components/Body/Part/BodyPartComponent.cs index 35ca191558..c09096dc7c 100644 --- a/Content.Server/GameObjects/Components/Body/Part/BodyPartComponent.cs +++ b/Content.Server/GameObjects/Components/Body/Part/BodyPartComponent.cs @@ -1,6 +1,5 @@ #nullable enable using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Content.Server.Utility; using Content.Shared.GameObjects.Components.Body; @@ -61,7 +60,7 @@ namespace Content.Server.GameObjects.Components.Body.Part { base.Initialize(); - _mechanismContainer = ContainerHelpers.EnsureContainer(Owner, $"{Name}-{nameof(BodyPartComponent)}"); + _mechanismContainer = Owner.EnsureContainer($"{Name}-{nameof(BodyPartComponent)}"); // This is ran in Startup as entities spawned in Initialize // are not synced to the client since they are assumed to be @@ -123,25 +122,23 @@ namespace Content.Server.GameObjects.Components.Body.Part // Here we are trying to grab a list of all empty BodySlots adjacent to an existing BodyPart that can be // attached to. i.e. an empty left hand slot, connected to an occupied left arm slot would be valid. - var unoccupiedSlots = body.Slots.Keys.ToList().Except(body.Parts.Keys.ToList()).ToList(); - foreach (var slot in unoccupiedSlots) + foreach (var slot in body.EmptySlots) { - if (!body.TryGetSlotType(slot, out var typeResult) || - typeResult != PartType || - !body.TryGetPartConnections(slot, out var parts)) + if (slot.PartType != PartType) { continue; } - foreach (var connectedPart in parts) + foreach (var connection in slot.Connections) { - if (!connectedPart.CanAttachPart(this)) + if (connection.Part == null || + !connection.Part.CanAttachPart(this)) { continue; } _optionsCache.Add(_idHash, slot); - toSend.Add(slot, _idHash++); + toSend.Add(slot.Id, _idHash++); } } @@ -269,7 +266,7 @@ namespace Content.Server.GameObjects.Components.Body.Part return; } - body.TryAddPart($"{nameof(AttachBodyPartVerb)}-{component.Owner.Uid}", component, true); + body.SetPart($"{nameof(AttachBodyPartVerb)}-{component.Owner.Uid}", component); } } } diff --git a/Content.Server/GameObjects/Components/Body/Surgery/SurgeryToolComponent.cs b/Content.Server/GameObjects/Components/Body/Surgery/SurgeryToolComponent.cs index be21308370..a70d424e22 100644 --- a/Content.Server/GameObjects/Components/Body/Surgery/SurgeryToolComponent.cs +++ b/Content.Server/GameObjects/Components/Body/Surgery/SurgeryToolComponent.cs @@ -69,13 +69,13 @@ namespace Content.Server.GameObjects.Components.Body.Surgery // Create dictionary to send to client (text to be shown : data sent back if selected) var toSend = new Dictionary(); - foreach (var (key, value) in body.Parts) + foreach (var (part, slot) in body.Parts) { // For each limb in the target, add it to our cache if it is a valid option. - if (value.SurgeryCheck(_surgeryType)) + if (part.SurgeryCheck(_surgeryType)) { - _optionsCache.Add(_idHash, value); - toSend.Add(key + ": " + value.Name, _idHash++); + _optionsCache.Add(_idHash, part); + toSend.Add(slot.Id + ": " + part.Name, _idHash++); } } diff --git a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs index 89c1d7a02d..0d902d378b 100644 --- a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs +++ b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs @@ -469,19 +469,26 @@ namespace Content.Server.GameObjects.Components.Kitchen if (victim.TryGetComponent(out var body)) { - var heads = body.GetPartsOfType(BodyPartType.Head); - foreach (var head in heads) + var headSlots = body.GetSlotsOfType(BodyPartType.Head); + + foreach (var slot in headSlots) { - if (!body.TryDropPart(head, out var dropped)) + var part = slot.Part; + + if (part == null || + !body.TryDropPart(slot, out var dropped)) { continue; } - var droppedHeads = dropped.Where(p => p.PartType == BodyPartType.Head); - - foreach (var droppedHead in droppedHeads) + foreach (var droppedPart in dropped.Values) { - _storage.Insert(droppedHead.Owner); + if (droppedPart.PartType != BodyPartType.Head) + { + continue; + } + + _storage.Insert(droppedPart.Owner); headCount++; } } diff --git a/Content.Server/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs b/Content.Server/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs index 19e8af7fa4..d74f9431c6 100644 --- a/Content.Server/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs @@ -18,7 +18,7 @@ namespace Content.Server.GameObjects.Components.Mobs if (Owner.TryGetComponent(out IBody? body)) { - foreach (var part in body.Parts.Values) + foreach (var (part, _) in body.Parts) { if (!part.Owner.TryGetComponent(out SpriteComponent? sprite)) { @@ -37,7 +37,7 @@ namespace Content.Server.GameObjects.Components.Mobs if (Appearance != null! && Owner.TryGetComponent(out IBody? body)) { - foreach (var part in body.Parts.Values) + foreach (var (part, _) in body.Parts) { if (!part.Owner.TryGetComponent(out SpriteComponent? sprite)) { diff --git a/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs b/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs index ccca0a69ae..0536824bb0 100644 --- a/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs +++ b/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs @@ -80,8 +80,8 @@ namespace Content.Server.GameObjects.Components.Movement return false; } - if (body.GetPartsOfType(BodyPartType.Leg).Count == 0 || - body.GetPartsOfType(BodyPartType.Foot).Count == 0) + if (!body.HasPartOfType(BodyPartType.Leg) || + !body.HasPartOfType(BodyPartType.Foot)) { reason = Loc.GetString("comp-climbable-cant-climb"); return false; diff --git a/Content.Server/GameObjects/Components/Watercloset/ToiletComponent.cs b/Content.Server/GameObjects/Components/Watercloset/ToiletComponent.cs index 64911aa701..deb5cf3005 100644 --- a/Content.Server/GameObjects/Components/Watercloset/ToiletComponent.cs +++ b/Content.Server/GameObjects/Components/Watercloset/ToiletComponent.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Threading.Tasks; using Content.Server.GameObjects.Components.Interactable; using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Strap; @@ -14,15 +15,14 @@ using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; using Robust.Server.GameObjects; +using Robust.Shared.Audio; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; -using System.Threading.Tasks; -using Robust.Shared.Audio; -using Robust.Shared.Player; namespace Content.Server.GameObjects.Components.Watercloset { @@ -144,7 +144,7 @@ namespace Content.Server.GameObjects.Components.Watercloset { // check that victim even have head if (victim.TryGetComponent(out var body) && - body.GetPartsOfType(BodyPartType.Head).Count != 0) + body.HasPartOfType(BodyPartType.Head)) { var othersMessage = Loc.GetString("{0:theName} sticks their head into {1:theName} and flushes it!", victim, Owner); victim.PopupMessageOtherClients(othersMessage); diff --git a/Content.Shared/GameObjects/Components/Body/IBody.cs b/Content.Shared/GameObjects/Components/Body/IBody.cs index b73bc01766..9e5ab1fcf0 100644 --- a/Content.Shared/GameObjects/Components/Body/IBody.cs +++ b/Content.Shared/GameObjects/Components/Body/IBody.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Body.Part.Property; using Content.Shared.GameObjects.Components.Body.Preset; +using Content.Shared.GameObjects.Components.Body.Slot; using Content.Shared.GameObjects.Components.Body.Template; using Robust.Shared.GameObjects; @@ -17,70 +18,81 @@ namespace Content.Shared.GameObjects.Components.Body public interface IBody : IComponent, IBodyPartContainer { /// - /// The name of the used by this + /// The used to create this /// . /// - public string? TemplateName { get; } + public BodyTemplatePrototype? Template { get; } /// - /// The name of the used by this + /// The used to create this /// . /// - public string? PresetName { get; } + public BodyPresetPrototype? Preset { get; } + + /// + /// An enumeration of the slots that make up this body, regardless + /// of if they contain a part or not. + /// + IEnumerable Slots { get; } + + /// + /// An enumeration of the parts on this body paired with the slots + /// that they are in. + /// + IEnumerable> Parts { get; } + + /// + /// An enumeration of the slots on this body without a part in them. + /// + IEnumerable EmptySlots { get; } + + /// + /// Finds the central , if any, + /// of this body. + /// + /// + /// The central if one exists, + /// null otherwise. + /// + BodyPartSlot? CenterSlot { get; } + + /// + /// Finds the central , if any, + /// of this body. + /// + /// + /// The central if one exists, + /// null otherwise. + /// + IBodyPart? CenterPart { get; } - // TODO BODY Part slots // TODO BODY Sensible templates - /// - /// Mapping of slots in this body to their - /// . - /// - public Dictionary Slots { get; } - - /// - /// Mapping of slots to the filling each one. - /// - public IReadOnlyDictionary Parts { get; } - - /// - /// Mapping of slots to which other slots they connect to. - /// For example, the torso could be mapped to a list containing - /// "right arm", "left arm", "left leg", and "right leg". - /// This is mapped both ways during runtime, but in the prototype - /// it only has to be defined one-way, "torso": "left arm" will automatically - /// map "left arm" to "torso" as well. - /// - public Dictionary> Connections { get; } - - /// - /// Mapping of template slots to the ID of the - /// that should fill it. E.g. "right arm" : "BodyPart.arm.basic_human". - /// - public IReadOnlyDictionary PartIds { get; } /// /// Attempts to add a part to the given slot. /// - /// The slot to add this part to. + /// The slot to add this part to. /// The part to add. - /// - /// Whether or not to check for the validity of the given . - /// Passing true does not guarantee it to be added, for example if it - /// had already been added before. + /// + /// Whether to check if the slot exists, or create one otherwise. /// /// - /// true if the part was added, false otherwise even if it was already added. + /// true if the part was added, false otherwise even if it was + /// already added. /// - bool TryAddPart(string slot, IBodyPart part, bool force = false); + bool TryAddPart(string slotId, IBodyPart part); + + void SetPart(string slotId, IBodyPart part); /// /// Checks if there is a in the given slot. /// - /// The slot to look in. + /// The slot to look in. /// - /// true if there is a part in the given , + /// true if there is a part in the given , /// false otherwise. /// - bool HasPart(string slot); + bool HasPart(string slotId); /// /// Checks if this contains the given . @@ -93,145 +105,130 @@ namespace Content.Shared.GameObjects.Components.Body bool HasPart(IBodyPart part); /// - /// Removes the given reference, potentially - /// dropping other BodyParts if they - /// were hanging off of it. + /// Removes the given from this body, + /// dropping other if they were hanging + /// off of it. + /// The part to remove. + /// + /// true if the part was removed, false otherwise + /// even if the part was already removed previously. + /// /// - void RemovePart(IBodyPart part); + bool RemovePart(IBodyPart part); /// - /// Removes the body part in slot from this body, + /// Removes the body part in slot from this body, /// if one exists. /// - /// The slot to remove it from. - /// True if the part was removed, false otherwise. - bool RemovePart(string slot); + /// The slot to remove it from. + /// true if the part was removed, false otherwise. + bool RemovePart(string slotId); /// /// 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); + /// The slot that the part was in, if any. + /// + /// true if was removed, false otherwise. + /// + bool RemovePart(IBodyPart part, [NotNullWhen(true)] out BodyPartSlot? slotId); /// /// Disconnects the given reference, potentially /// dropping other BodyParts if they /// were hanging off of it. /// - /// The part to drop. + /// The part to drop. /// - /// All of the parts that were dropped, including . + /// All of the parts that were dropped, including the one in + /// . /// /// - /// True if the part was dropped, false otherwise. + /// true if the part was dropped, false otherwise. /// - bool TryDropPart(IBodyPart part, [NotNullWhen(true)] out List? dropped); + bool TryDropPart(BodyPartSlot slot, [NotNullWhen(true)] out Dictionary? dropped); /// /// 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. + /// + /// 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 - /// . + /// Returns whether the given part slot exists in this body. /// /// The slot to check for. - /// True if the slot exists in this body, false otherwise. + /// 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); + BodyPartSlot? GetSlot(IBodyPart part); /// - /// Finds the slotName that the given resides in. + /// Finds the slot 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); + /// true if a slot was found, false otherwise + bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out BodyPartSlot? slot); /// - /// Finds the in the given - /// if one exists. + /// 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); + /// The part slot to search in. + /// The body part in that slot, if any. + /// true if found, false otherwise. + bool TryGetPart(string slotId, [NotNullWhen(true)] out IBodyPart? result); /// - /// Finds the names of all slots connected to the given - /// for the template. + /// Checks if a slot of the specified type exists on this body. /// - /// 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); + /// The type to check for. + /// true if present, false otherwise. + bool HasSlotOfType(BodyPartType type); /// - /// Grabs all occupied slots connected to the given slot, - /// regardless of whether the given is occupied. + /// Gets all slots of the specified type on this body. /// - /// 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); + /// The type to check for. + /// An enumerable of the found slots. + IEnumerable GetSlotsOfType(BodyPartType type); /// - /// Grabs all parts connected to the given , regardless - /// of whether the given is occupied. + /// Checks if a part of the specified type exists on this body. /// - /// 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); + /// The type to check for. + /// true if present, false otherwise. + bool HasPartOfType(BodyPartType type); /// - /// Finds all s of the given type in this body. + /// Gets all slots of the specified type on this body. /// - /// A list of parts of that type. - List GetPartsOfType(BodyPartType type); + /// The type to check for. + /// An enumerable of the found slots. + IEnumerable GetPartsOfType(BodyPartType type); /// - /// Finds all s with the given property in this body. + /// Finds all s with the given property in + /// this body. /// /// The property type to look for. /// A list of parts with that property. - List<(IBodyPart part, IBodyPartProperty property)> GetPartsWithProperty(Type type); + IEnumerable<(IBodyPart part, IBodyPartProperty property)> GetPartsWithProperty(Type type); /// /// Finds all s with the given property in this body. /// /// The property type to look for. /// A list of parts with that property. - List<(IBodyPart part, T property)> GetPartsWithProperty() where T : class, IBodyPartProperty; + IEnumerable<(IBodyPart part, T property)> GetPartsWithProperty() where T : class, IBodyPartProperty; // TODO BODY Make a slot object that makes sense to the human mind, and make it serializable. Imagine the possibilities! /// @@ -239,18 +236,21 @@ namespace Content.Shared.GameObjects.Components.Body /// /// The index to look in. /// A pair of the slot name and part type occupying it. - KeyValuePair SlotAt(int index); + BodyPartSlot SlotAt(int index); /// /// Retrieves the part at the given index. /// /// The index to look in. /// A pair of the part name and body part occupying it. - KeyValuePair PartAt(int index); + KeyValuePair PartAt(int index); /// /// Gibs this body. /// + /// + /// Whether or not to also gib this body's parts. + /// void Gib(bool gibParts = false); } } diff --git a/Content.Shared/GameObjects/Components/Body/Part/IBodyPart.cs b/Content.Shared/GameObjects/Components/Body/Part/IBodyPart.cs index 69bfbbc338..ff5aa061c0 100644 --- a/Content.Shared/GameObjects/Components/Body/Part/IBodyPart.cs +++ b/Content.Shared/GameObjects/Components/Body/Part/IBodyPart.cs @@ -15,6 +15,11 @@ namespace Content.Shared.GameObjects.Components.Body.Part /// IBody? Body { get; set; } + /// + /// The string to show when displaying this part's name to players. + /// + string DisplayName { get; } + /// /// that this is considered /// to be. diff --git a/Content.Shared/GameObjects/Components/Body/Part/IBodyPartAdded.cs b/Content.Shared/GameObjects/Components/Body/Part/IBodyPartAdded.cs index ff9f983891..29dcba143b 100644 --- a/Content.Shared/GameObjects/Components/Body/Part/IBodyPartAdded.cs +++ b/Content.Shared/GameObjects/Components/Body/Part/IBodyPartAdded.cs @@ -20,20 +20,20 @@ namespace Content.Shared.GameObjects.Components.Body.Part public class BodyPartAddedEventArgs : EventArgs { - public BodyPartAddedEventArgs(IBodyPart part, string slot) + public BodyPartAddedEventArgs(string slot, IBodyPart part) { - Part = part; Slot = slot; + Part = part; } - /// - /// The part that was added. - /// - public IBodyPart Part { get; } - /// /// The slot that was added to. /// public string Slot { get; } + + /// + /// The part that was added. + /// + public IBodyPart Part { get; } } } diff --git a/Content.Shared/GameObjects/Components/Body/Part/IBodyPartRemoved.cs b/Content.Shared/GameObjects/Components/Body/Part/IBodyPartRemoved.cs index c6350dcb0f..70173370c9 100644 --- a/Content.Shared/GameObjects/Components/Body/Part/IBodyPartRemoved.cs +++ b/Content.Shared/GameObjects/Components/Body/Part/IBodyPartRemoved.cs @@ -19,20 +19,20 @@ namespace Content.Shared.GameObjects.Components.Body.Part public class BodyPartRemovedEventArgs : EventArgs { - public BodyPartRemovedEventArgs(IBodyPart part, string slot) + public BodyPartRemovedEventArgs(string slot, IBodyPart part) { - Part = part; Slot = slot; + Part = part; } - /// - /// The part that was removed. - /// - public IBodyPart Part { get; } - /// /// The slot that was removed from. /// public string Slot { get; } + + /// + /// The part that was removed. + /// + public IBodyPart Part { get; } } } diff --git a/Content.Shared/GameObjects/Components/Body/Part/SharedBodyPartComponent.cs b/Content.Shared/GameObjects/Components/Body/Part/SharedBodyPartComponent.cs index 899d50c97e..0b8ae6f400 100644 --- a/Content.Shared/GameObjects/Components/Body/Part/SharedBodyPartComponent.cs +++ b/Content.Shared/GameObjects/Components/Body/Part/SharedBodyPartComponent.cs @@ -57,6 +57,9 @@ namespace Content.Shared.GameObjects.Components.Body.Part } } + [ViewVariables] + public string DisplayName => Name; + [ViewVariables] [DataField("partType")] public BodyPartType PartType { get; private set; } = BodyPartType.Other; diff --git a/Content.Shared/GameObjects/Components/Body/Preset/BodyPresetPrototype.cs b/Content.Shared/GameObjects/Components/Body/Preset/BodyPresetPrototype.cs index 7e040ca331..d2d929568b 100644 --- a/Content.Shared/GameObjects/Components/Body/Preset/BodyPresetPrototype.cs +++ b/Content.Shared/GameObjects/Components/Body/Preset/BodyPresetPrototype.cs @@ -20,13 +20,14 @@ namespace Content.Shared.GameObjects.Components.Body.Preset [field: DataField("id", required: true)] public string ID { get; } = default!; - [DataField("partIDs")] + [field: DataField("partIDs")] private Dictionary _partIDs = new(); [ViewVariables] [field: DataField("name")] public string Name { get; } = string.Empty; - [ViewVariables] public Dictionary PartIDs => new(_partIDs); + [ViewVariables] + public Dictionary PartIDs => new(_partIDs); } } diff --git a/Content.Shared/GameObjects/Components/Body/SharedBodyComponent.cs b/Content.Shared/GameObjects/Components/Body/SharedBodyComponent.cs index 29695fc610..05f0cceffb 100644 --- a/Content.Shared/GameObjects/Components/Body/SharedBodyComponent.cs +++ b/Content.Shared/GameObjects/Components/Body/SharedBodyComponent.cs @@ -7,15 +7,13 @@ using Content.Shared.Damage; using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Body.Part.Property; using Content.Shared.GameObjects.Components.Body.Preset; +using Content.Shared.GameObjects.Components.Body.Slot; using Content.Shared.GameObjects.Components.Body.Template; using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Movement; using Content.Shared.GameObjects.EntitySystems; using Robust.Shared.GameObjects; using Robust.Shared.IoC; -using Robust.Shared.Maths; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Collision; using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -34,35 +32,45 @@ namespace Content.Shared.GameObjects.Components.Body public override uint? NetID => ContentNetIDs.BODY; - [DataField("centerSlot", required: true)] - private string? _centerSlot; - - private Dictionary _partIds = new(); - - private readonly Dictionary _parts = new(); + [ViewVariables] + [field: DataField("template", required: true)] + private string? TemplateId { get; } = default; [ViewVariables] - [DataField("template", required: true)] - public string? TemplateName { get; private set; } + [field: DataField("preset", required: true)] + private string? PresetId { get; } = default; [ViewVariables] - [DataField("preset", required: true)] - public string? PresetName { get; private set; } + public BodyTemplatePrototype? Template => TemplateId == null + ? null + : _prototypeManager.Index(TemplateId); [ViewVariables] - public Dictionary Slots { get; private set; } = new(); + public BodyPresetPrototype? Preset => PresetId == null + ? null + : _prototypeManager.Index(PresetId); [ViewVariables] - public Dictionary> Connections { get; private set; } = new(); - - /// - /// Maps slots to the part filling each one. - /// - [ViewVariables] - public IReadOnlyDictionary Parts => _parts; + private Dictionary SlotIds { get; } = new(); [ViewVariables] - public IReadOnlyDictionary PartIds => _partIds; + private Dictionary SlotParts { get; } = new(); + + [ViewVariables] + public IEnumerable Slots => SlotIds.Values; + + [ViewVariables] + public IEnumerable> Parts => SlotParts; + + [ViewVariables] + public IEnumerable EmptySlots => Slots.Where(slot => slot.Part == null); + + public BodyPartSlot? CenterSlot => + Template?.CenterSlot is { } centerSlot + ? SlotIds.GetValueOrDefault(centerSlot) + : null; + + public IBodyPart? CenterPart => CenterSlot?.Part; public override void Initialize() { @@ -70,60 +78,66 @@ namespace Content.Shared.GameObjects.Components.Body // TODO BODY BeforeDeserialization // TODO BODY Move to template or somewhere else - if (TemplateName != null) + if (TemplateId != null) { - var template = _prototypeManager.Index(TemplateName); + var template = _prototypeManager.Index(TemplateId); - Connections = template.Connections; - Slots = template.Slots; - _centerSlot = template.CenterSlot; - } - - if (PresetName != null) - { - var preset = _prototypeManager.Index(PresetName); - - _partIds = preset.PartIDs; - } - - // Our prototypes don't force the user to define a BodyPart connection twice. E.g. Head: Torso v.s. Torso: Head. - // The user only has to do one. We want it to be that way in the code, though, so this cleans that up. - var cleanedConnections = new Dictionary>(); - - foreach (var targetSlotName in Slots.Keys) - { - var tempConnections = new List(); - foreach (var (slotName, slotConnections) in Connections) + foreach (var (id, partType) in template.Slots) { - if (slotName == targetSlotName) - { - foreach (var connection in slotConnections) - { - if (!tempConnections.Contains(connection)) - { - tempConnections.Add(connection); - } - } - } - else if (slotConnections.Contains(targetSlotName)) - { - tempConnections.Add(slotName); - } + SetSlot(id, partType); } - if (tempConnections.Count > 0) + foreach (var (slotId, connectionIds) in template.Connections) { - cleanedConnections.Add(targetSlotName, tempConnections); + var connections = connectionIds.Select(id => SlotIds[id]); + SlotIds[slotId].SetConnectionsInternal(connections); } } - Connections = cleanedConnections; CalculateSpeed(); } - protected virtual bool CanAddPart(string slot, IBodyPart part) + public override void OnRemove() { - if (!HasSlot(slot) || !_parts.TryAdd(slot, part)) + foreach (var slot in SlotIds.Values) + { + slot.Shutdown(); + } + + base.OnRemove(); + } + + private BodyPartSlot SetSlot(string id, BodyPartType type) + { + var slot = new BodyPartSlot(id, type); + + SlotIds[id] = slot; + slot.PartAdded += part => OnAddPart(slot, part); + slot.PartRemoved += part => OnRemovePart(slot, part); + + return slot; + } + + private Dictionary GetHangingParts(BodyPartSlot from) + { + var hanging = new Dictionary(); + + foreach (var connection in from.Connections) + { + if (connection.Part != null && + !ConnectedToCenter(connection.Part)) + { + hanging.Add(connection, connection.Part); + } + } + + return hanging; + } + + protected virtual bool CanAddPart(string slotId, IBodyPart part) + { + if (!SlotIds.TryGetValue(slotId, out var slot) || + slot.CanAddPart(part)) { return false; } @@ -131,11 +145,12 @@ namespace Content.Shared.GameObjects.Components.Body return true; } - protected virtual void OnAddPart(string slot, IBodyPart part) + protected virtual void OnAddPart(BodyPartSlot slot, IBodyPart part) { + SlotParts[part] = slot; part.Body = this; - var argsAdded = new BodyPartAddedEventArgs(part, slot); + var argsAdded = new BodyPartAddedEventArgs(slot.Id, part); foreach (var component in Owner.GetAllComponents().ToArray()) { @@ -146,11 +161,22 @@ namespace Content.Shared.GameObjects.Components.Body OnBodyChanged(); } - protected virtual void OnRemovePart(string slot, IBodyPart part) + protected virtual void OnRemovePart(BodyPartSlot slot, IBodyPart part) { + SlotParts.Remove(part); + + foreach (var connectedSlot in slot.Connections) + { + if (connectedSlot.Part != null && + !ConnectedToCenter(connectedSlot.Part)) + { + RemovePart(connectedSlot.Part); + } + } + part.Body = null; - var args = new BodyPartRemovedEventArgs(part, slot); + var args = new BodyPartRemovedEventArgs(slot.Id, part); foreach (var component in Owner.GetAllComponents()) { @@ -158,7 +184,8 @@ namespace Content.Shared.GameObjects.Components.Body } // creadth: fall down if no legs - if (part.PartType == BodyPartType.Leg && Parts.Count(x => x.Value.PartType == BodyPartType.Leg) == 0) + if (part.PartType == BodyPartType.Leg && + GetPartsOfType(BodyPartType.Leg).ToArray().Length == 0) { EntitySystem.Get().Down(Owner); } @@ -166,7 +193,7 @@ namespace Content.Shared.GameObjects.Components.Body // creadth: immediately kill entity if last vital part removed if (Owner.TryGetComponent(out IDamageableComponent? damageable)) { - if (part.IsVital && Parts.Count(x => x.Value.PartType == part.PartType) == 0) + if (part.IsVital && SlotParts.Count(x => x.Value.PartType == part.PartType) == 0) { damageable.ChangeDamage(DamageType.Bloodloss, 300, true); // TODO BODY KILL } @@ -175,174 +202,133 @@ namespace Content.Shared.GameObjects.Components.Body OnBodyChanged(); } - public bool TryAddPart(string slot, IBodyPart part, bool force = false) + public bool TryAddPart(string slotId, IBodyPart part) { DebugTools.AssertNotNull(part); - DebugTools.AssertNotNull(slot); + DebugTools.AssertNotNull(slotId); - if (force) + if (!CanAddPart(slotId, part)) { - if (!HasSlot(slot)) - { - Slots[slot] = part.PartType; - } - - _parts[slot] = part; - } - else - { - if (!CanAddPart(slot, part)) - { - return false; - } + return false; } - OnAddPart(slot, part); - - return true; + return SlotIds.TryGetValue(slotId, out var slot) && + slot.TryAddPart(part); } - public bool HasPart(string slot) + public void SetPart(string slotId, IBodyPart part) { - DebugTools.AssertNotNull(slot); + if (!SlotIds.TryGetValue(slotId, out var slot)) + { + slot = SetSlot(slotId, part.PartType); + SlotIds[slotId] = slot; + } - return _parts.ContainsKey(slot); + slot.SetPart(part); + } + + public bool HasPart(string slotId) + { + DebugTools.AssertNotNull(slotId); + + return SlotIds.TryGetValue(slotId, out var slot) && + slot.Part != null; } public bool HasPart(IBodyPart part) { DebugTools.AssertNotNull(part); - return _parts.ContainsValue(part); + return SlotParts.ContainsKey(part); } - public void RemovePart(IBodyPart part) + public bool RemovePart(IBodyPart part) { DebugTools.AssertNotNull(part); - var slotName = _parts.FirstOrDefault(x => x.Value == part).Key; - - if (string.IsNullOrEmpty(slotName)) - { - return; - } - - RemovePart(slotName); + return SlotParts.TryGetValue(part, out var slot) && + slot.RemovePart(); } - // TODO BODY invert this behavior with the one above - public bool RemovePart(string slot) + public bool RemovePart(string slotId) { - DebugTools.AssertNotNull(slot); + DebugTools.AssertNotNull(slotId); - if (!_parts.Remove(slot, out var part)) + return SlotIds.TryGetValue(slotId, out var slot) && + slot.RemovePart(); + } + + public bool RemovePart(IBodyPart part, [NotNullWhen(true)] out BodyPartSlot? slotId) + { + DebugTools.AssertNotNull(part); + + if (!SlotParts.TryGetValue(part, out var slot)) { + slotId = null; return false; } - OnRemovePart(slot, part); - - if (TryGetSlotConnections(slot, out var connections)) + if (!slot.RemovePart()) { - foreach (var connectionName in connections) - { - if (TryGetPart(connectionName, out var result) && !ConnectedToCenter(result)) - { - RemovePart(connectionName); - } - } + slotId = null; + return false; } + slotId = slot; return true; } - public bool RemovePart(IBodyPart part, [NotNullWhen(true)] out string? slotName) + public bool TryDropPart(BodyPartSlot slot, [NotNullWhen(true)] out Dictionary? dropped) { - DebugTools.AssertNotNull(part); + DebugTools.AssertNotNull(slot); - (slotName, _) = _parts.FirstOrDefault(kvPair => kvPair.Value == part); - - if (slotName == null) - { - return false; - } - - if (RemovePart(slotName)) - { - return true; - } - - slotName = null; - return false; - } - - public bool TryDropPart(IBodyPart part, [NotNullWhen(true)] out List? dropped) - { - DebugTools.AssertNotNull(part); - - if (!_parts.ContainsValue(part)) + if (!SlotIds.TryGetValue(slot.Id, out var ownedSlot) || + ownedSlot != slot || + slot.Part == null) { dropped = null; return false; } - if (!RemovePart(part, out var slotName)) + var oldPart = slot.Part; + dropped = GetHangingParts(slot); + + if (!slot.RemovePart()) { dropped = null; return false; } - dropped = new List {part}; - // Call disconnect on all limbs that were hanging off this limb. - if (TryGetSlotConnections(slotName, out var connections)) - { - // TODO BODY optimize - foreach (var connectionName in connections) - { - if (TryGetPart(connectionName, out var result) && - !ConnectedToCenter(result) && - RemovePart(connectionName)) - { - dropped.Add(result); - } - } - } - - OnBodyChanged(); + dropped[slot] = oldPart; return true; } public bool ConnectedToCenter(IBodyPart part) { - var searchedSlots = new List(); - return TryGetSlot(part, out var result) && - ConnectedToCenterPartRecursion(searchedSlots, result); + ConnectedToCenterPartRecursion(result); } - private bool ConnectedToCenterPartRecursion(ICollection searchedSlots, string slotName) + private bool ConnectedToCenterPartRecursion(BodyPartSlot slot, HashSet? searched = null) { - if (!TryGetPart(slotName, out var part)) + searched ??= new HashSet(); + + if (Template?.CenterSlot == null) { return false; } - if (part == CenterPart()) + if (slot.Part == CenterPart) { return true; } - searchedSlots.Add(slotName); + searched.Add(slot); - if (!TryGetSlotConnections(slotName, out var connections)) + foreach (var connection in slot.Connections) { - return false; - } - - foreach (var connection in connections) - { - if (!searchedSlots.Contains(connection) && - ConnectedToCenterPartRecursion(searchedSlots, connection)) + if (!searched.Contains(connection) && + ConnectedToCenterPartRecursion(connection, searched)) { return true; } @@ -351,56 +337,64 @@ namespace Content.Shared.GameObjects.Components.Body return false; } - public IBodyPart? CenterPart() - { - if (_centerSlot == null) return null; - - return Parts.GetValueOrDefault(_centerSlot); - } - public bool HasSlot(string slot) { - return Slots.ContainsKey(slot); + return SlotIds.ContainsKey(slot); } - public bool TryGetPart(string slot, [NotNullWhen(true)] out IBodyPart? result) + public IEnumerable GetParts() { - return Parts.TryGetValue(slot, out result); + foreach (var slot in SlotIds.Values) + { + if (slot.Part != null) + { + yield return slot.Part; + } + } } - public bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out string? slot) + public bool TryGetPart(string slotId, [NotNullWhen(true)] out IBodyPart? 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. - (slot, _) = Parts.FirstOrDefault(x => x.Value == part); + result = null; - return slot != null; + return SlotIds.TryGetValue(slotId, out var slot) && + (result = slot.Part) != null; } - public bool TryGetSlotType(string slot, out BodyPartType result) + public BodyPartSlot? GetSlot(string id) { - return Slots.TryGetValue(slot, out result); + return SlotIds.GetValueOrDefault(id); } - public bool TryGetSlotConnections(string slot, [NotNullWhen(true)] out List? connections) + public BodyPartSlot? GetSlot(IBodyPart part) { - return Connections.TryGetValue(slot, out connections); + return SlotParts.GetValueOrDefault(part); } - public bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List? connections) + public bool TryGetSlot(string slotId, [NotNullWhen(true)] out BodyPartSlot? slot) { - if (!Connections.TryGetValue(slot, out var slotConnections)) + return (slot = GetSlot(slotId)) != null; + } + + public bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out BodyPartSlot? slot) + { + return (slot = GetSlot(part)) != null; + } + + public bool TryGetPartConnections(string slotId, [NotNullWhen(true)] out List? connections) + { + if (!SlotIds.TryGetValue(slotId, out var slot)) { connections = null; return false; } connections = new List(); - foreach (var connection in slotConnections) + foreach (var connection in slot.Connections) { - if (TryGetPart(connection, out var part)) + if (connection.Part != null) { - connections.Add(part); + connections.Add(connection.Part); } } @@ -413,57 +407,68 @@ namespace Content.Shared.GameObjects.Components.Body return true; } - public bool TryGetPartConnections(IBodyPart part, [NotNullWhen(true)] out List? connections) + public bool HasSlotOfType(BodyPartType type) { - connections = null; - - return TryGetSlot(part, out var slotName) && - TryGetPartConnections(slotName, out connections); - } - - public List GetPartsOfType(BodyPartType type) - { - var parts = new List(); - - foreach (var part in Parts.Values) + foreach (var _ in GetSlotsOfType(type)) { - if (part.PartType == type) - { - parts.Add(part); - } + return true; } - return parts; + return false; } - public List<(IBodyPart part, IBodyPartProperty property)> GetPartsWithProperty(Type type) + public IEnumerable GetSlotsOfType(BodyPartType type) { - var parts = new List<(IBodyPart, IBodyPartProperty)>(); - - foreach (var part in Parts.Values) + foreach (var slot in SlotIds.Values) { - if (part.TryGetProperty(type, out var property)) + if (slot.PartType == type) { - parts.Add((part, property)); + yield return slot; } } - - return parts; } - public List<(IBodyPart part, T property)> GetPartsWithProperty() where T : class, IBodyPartProperty + public bool HasPartOfType(BodyPartType type) { - var parts = new List<(IBodyPart, T)>(); + foreach (var _ in GetPartsOfType(type)) + { + return true; + } - foreach (var part in Parts.Values) + return false; + } + + public IEnumerable GetPartsOfType(BodyPartType type) + { + foreach (var slot in GetSlotsOfType(type)) + { + if (slot.Part != null) + { + yield return slot.Part; + } + } + } + + public IEnumerable<(IBodyPart part, IBodyPartProperty property)> GetPartsWithProperty(Type type) + { + foreach (var slot in SlotIds.Values) + { + if (slot.Part != null && slot.Part.TryGetProperty(type, out var property)) + { + yield return (slot.Part, property); + } + } + } + + public IEnumerable<(IBodyPart part, T property)> GetPartsWithProperty() where T : class, IBodyPartProperty + { + foreach (var part in SlotParts.Keys) { if (part.TryGetProperty(out var property)) { - parts.Add((part, property)); + yield return (part, property); } } - - return parts; } private void CalculateSpeed() @@ -473,7 +478,7 @@ namespace Content.Shared.GameObjects.Components.Body return; } - var legs = GetPartsWithProperty(); + var legs = GetPartsWithProperty().ToArray(); float speedSum = 0; foreach (var leg in legs) @@ -497,7 +502,7 @@ namespace Content.Shared.GameObjects.Components.Body { // Extra legs stack diminishingly. playerMover.BaseWalkSpeed = - speedSum / (legs.Count - (float) Math.Log(legs.Count, 4.0)); + speedSum / (legs.Length - (float) Math.Log(legs.Length, 4.0)); playerMover.BaseSprintSpeed = playerMover.BaseWalkSpeed * 1.75f; } @@ -537,27 +542,29 @@ namespace Content.Shared.GameObjects.Components.Body return extension.Distance; } - return LookForFootRecursion(source, new List()); + return LookForFootRecursion(source); } - private float LookForFootRecursion(IBodyPart current, ICollection searchedParts) + private float LookForFootRecursion(IBodyPart current, HashSet? searched = null) { + searched ??= new HashSet(); + 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)) + if (!TryGetSlot(current, out var slot)) { return float.MinValue; } // If a connected BodyPart is a foot, return this BodyPart's length. - foreach (var connection in connections) + foreach (var connection in slot.Connections) { if (connection.PartType == BodyPartType.Foot && - !searchedParts.Contains(connection)) + !searched.Contains(connection)) { return extProperty.Distance; } @@ -566,14 +573,14 @@ namespace Content.Shared.GameObjects.Components.Body // Otherwise, get the recursion values of all connected BodyParts and // store them in a list. var distances = new List(); - foreach (var connection in connections) + foreach (var connection in slot.Connections) { - if (!searchedParts.Contains(connection)) + if (connection.Part == null || !searched.Contains(connection)) { continue; } - var result = LookForFootRecursion(connection, searchedParts); + var result = LookForFootRecursion(connection.Part, searched); if (Math.Abs(result - float.MinValue) > 0.001f) { @@ -592,24 +599,24 @@ namespace Content.Shared.GameObjects.Components.Body } // TODO BODY optimize this - public KeyValuePair SlotAt(int index) + public BodyPartSlot SlotAt(int index) { - return Slots.ElementAt(index); + return SlotIds.Values.ElementAt(index); } - public KeyValuePair PartAt(int index) + public KeyValuePair PartAt(int index) { - return Parts.ElementAt(index); + return SlotParts.ElementAt(index); } public override ComponentState GetComponentState(ICommonSession player) { - var parts = new (string slot, EntityUid partId)[_parts.Count]; + var parts = new (string slot, EntityUid partId)[SlotParts.Count]; var i = 0; - foreach (var (slot, part) in _parts) + foreach (var (part, slot) in SlotParts) { - parts[i] = (slot, part.Owner.Uid); + parts[i] = (slot.Id, part.Owner.Uid); i++; } @@ -627,28 +634,28 @@ namespace Content.Shared.GameObjects.Components.Body var newParts = state.Parts(); - foreach (var (slot, oldPart) in _parts) + foreach (var (oldPart, slot) in SlotParts) { - if (!newParts.TryGetValue(slot, out var newPart) || + if (!newParts.TryGetValue(slot.Id, out var newPart) || newPart != oldPart) { RemovePart(oldPart); } } - foreach (var (slot, newPart) in newParts) + foreach (var (slotId, newPart) in newParts) { - if (!_parts.TryGetValue(slot, out var oldPart) || - oldPart != newPart) + if (!SlotIds.TryGetValue(slotId, out var slot) || + slot.Part != newPart) { - TryAddPart(slot, newPart, true); + SetPart(slotId, newPart); } } } public virtual void Gib(bool gibParts = false) { - foreach (var (_, part) in Parts) + foreach (var part in SlotParts.Keys) { RemovePart(part); diff --git a/Content.Shared/GameObjects/Components/Body/Slot/BodyPartSlot.cs b/Content.Shared/GameObjects/Components/Body/Slot/BodyPartSlot.cs new file mode 100644 index 0000000000..e4682365b7 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Slot/BodyPartSlot.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using Content.Shared.GameObjects.Components.Body.Part; +using Robust.Shared.ViewVariables; + +namespace Content.Shared.GameObjects.Components.Body.Slot +{ + public class BodyPartSlot + { + public BodyPartSlot(string id, BodyPartType partType, IEnumerable connections) + { + Id = id; + PartType = partType; + Connections = new HashSet(connections); + } + + public BodyPartSlot(string id, BodyPartType partType) + { + Id = id; + PartType = partType; + Connections = new HashSet(); + } + + /// + /// The ID of this slot. + /// + [ViewVariables] + public string Id { get; } + + /// + /// The part type that this slot accepts. + /// + [ViewVariables] + public BodyPartType PartType { get; } + + /// + /// The part currently in this slot, if any. + /// + [ViewVariables] + public IBodyPart? Part { get; private set; } + + /// + /// List of slots that this slot connects to. + /// + [ViewVariables] + public HashSet Connections { get; private set; } + + public event Action? PartAdded; + + public event Action? PartRemoved; + + internal void SetConnectionsInternal(IEnumerable connections) + { + Connections = new HashSet(connections); + } + + public bool CanAddPart(IBodyPart part) + { + return Part == null && part.PartType == PartType; + } + + public bool TryAddPart(IBodyPart part) + { + if (!CanAddPart(part)) + { + return false; + } + + SetPart(part); + return true; + } + + public void SetPart(IBodyPart part) + { + if (Part != null) + { + RemovePart(); + } + + Part = part; + PartAdded?.Invoke(part); + } + + public bool RemovePart() + { + if (Part == null) + { + return false; + } + + var old = Part; + Part = null; + + PartRemoved?.Invoke(old); + + return true; + } + + public void Shutdown() + { + Part = null; + Connections.Clear(); + PartAdded = null; + PartRemoved = null; + } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Template/BodyTemplatePrototype.cs b/Content.Shared/GameObjects/Components/Body/Template/BodyTemplatePrototype.cs index b4fb11f4c8..b76d72c695 100644 --- a/Content.Shared/GameObjects/Components/Body/Template/BodyTemplatePrototype.cs +++ b/Content.Shared/GameObjects/Components/Body/Template/BodyTemplatePrototype.cs @@ -16,16 +16,16 @@ namespace Content.Shared.GameObjects.Components.Body.Template [Serializable, NetSerializable] public class BodyTemplatePrototype : IPrototype, ISerializationHooks { - [DataField("slots")] + [field: DataField("slots")] private Dictionary _slots = new(); - [DataField("connections")] + [field: DataField("connections")] private Dictionary> _rawConnections = new(); - [DataField("layers")] + [field: DataField("layers")] private Dictionary _layers = new(); - [DataField("mechanismLayers")] + [field: DataField("mechanismLayers")] private Dictionary _mechanismLayers = new(); [ViewVariables] @@ -44,7 +44,7 @@ namespace Content.Shared.GameObjects.Components.Body.Template public Dictionary Slots => new(_slots); [ViewVariables] - public Dictionary> Connections { get; set; } = new(); + public Dictionary> Connections { get; set; } = new(); [ViewVariables] public Dictionary Layers => new(_layers); @@ -56,11 +56,11 @@ namespace Content.Shared.GameObjects.Components.Body.Template { //Our prototypes don't force the user to define a BodyPart connection twice. E.g. Head: Torso v.s. Torso: Head. //The user only has to do one. We want it to be that way in the code, though, so this cleans that up. - var cleanedConnections = new Dictionary>(); + var cleanedConnections = new Dictionary>(); foreach (var targetSlotName in _slots.Keys) { - var tempConnections = new List(); + var tempConnections = new HashSet(); foreach (var (slotName, slotConnections) in _rawConnections) { if (slotName == targetSlotName) diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 100ad4c4b0..365f3bf603 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -163,7 +163,6 @@ - type: Body template: HumanoidTemplate preset: HumanPreset - centerSlot: torso - type: Damageable damageContainer: biologicalDamageContainer - type: Metabolism @@ -336,7 +335,6 @@ - type: Body template: HumanoidTemplate preset: HumanPreset - centerSlot: torso - type: Damageable damageContainer: biologicalDamageContainer - type: MobState diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index be06e8b53d..1be13828f2 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -141,7 +141,6 @@ - type: Body template: HumanoidTemplate preset: SlimePreset - centerSlot: torso - type: Damageable damageContainer: biologicalDamageContainer - type: Metabolism diff --git a/Resources/Prototypes/Entities/Mobs/Species/vox.yml b/Resources/Prototypes/Entities/Mobs/Species/vox.yml index bc6f378c65..6c9cdf5c99 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/vox.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/vox.yml @@ -82,7 +82,6 @@ - type: Body template: HumanoidTemplate preset: VoxPreset - centerSlot: torso - type: Metabolism needsGases: Nitrogen: 0.00060763888 diff --git a/Resources/Prototypes/EntityLists/Cow/cow_tools.yml b/Resources/Prototypes/EntityLists/Cow/cow_tools.yml new file mode 100644 index 0000000000..7c940b52d3 --- /dev/null +++ b/Resources/Prototypes/EntityLists/Cow/cow_tools.yml @@ -0,0 +1,10 @@ +- type: entityList + id: CowTools + entities: + - Haycutters + - Moodriver + - Wronch + - Cowbar + - Mooltitool + - Cowelder + - Milkalyzer