diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b7e3c040b7..1adc0b803d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -60,3 +60,6 @@ /Content.*/Atmos/ @Partmedia /Content.*/Botany/ @Partmedia +#Jezi +/Content.*/Medical @Jezithyr +/Content.*/Body @Jezithyr diff --git a/Content.IntegrationTests/Tests/Body/LegTest.cs b/Content.IntegrationTests/Tests/Body/LegTest.cs index 4d5078f2dc..e86966f8f5 100644 --- a/Content.IntegrationTests/Tests/Body/LegTest.cs +++ b/Content.IntegrationTests/Tests/Body/LegTest.cs @@ -38,6 +38,7 @@ namespace Content.IntegrationTests.Tests.Body var entityManager = server.ResolveDependency(); var mapManager = server.ResolveDependency(); var appearanceSystem = entityManager.System(); + var xformSystem = entityManager.System(); await server.WaitAssertion(() => { @@ -60,7 +61,7 @@ namespace Content.IntegrationTests.Tests.Body foreach (var leg in legs) { - bodySystem.DropPart(leg.Id, leg.Component); + xformSystem.DetachParentToNull(leg.Id, entityManager.GetComponent(leg.Id)); } }); diff --git a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs b/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs index 94591dd2b9..670ce1a474 100644 --- a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs +++ b/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs @@ -3,6 +3,7 @@ using System.Linq; using Content.Shared.Body.Components; using Content.Shared.Body.Systems; using Robust.Server.GameObjects; +using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -31,6 +32,7 @@ public sealed class SaveLoadReparentTest var maps = server.ResolveDependency(); var mapLoader = entities.System(); var bodySystem = entities.System(); + var containerSystem = entities.System(); await server.WaitAssertion(() => { @@ -40,7 +42,7 @@ public sealed class SaveLoadReparentTest Assert.That(entities.HasComponent(human), Is.True); - var parts = bodySystem.GetBodyChildren(human).ToArray(); + var parts = bodySystem.GetBodyChildren(human).Skip(1).ToArray(); var organs = bodySystem.GetBodyOrgans(human).ToArray(); Assert.Multiple(() => @@ -54,9 +56,17 @@ public sealed class SaveLoadReparentTest Assert.Multiple(() => { Assert.That(component.Body, Is.EqualTo(human)); - Assert.That(component.ParentSlot, Is.Not.Null); - Assert.That(component.ParentSlot.Parent, Is.Not.EqualTo(default(EntityUid))); - Assert.That(component.ParentSlot.Child, Is.EqualTo(id)); + Assert.That(component.Body, Is.Not.Null); + var parent = bodySystem.GetParentPartOrNull(id); + Assert.That(parent, Is.Not.EqualTo(default(EntityUid))); + if (!bodySystem.IsPartRoot(component.Body.Value, id, null, component)) + { + Assert.That(parent, Is.Not.Null); + } + else + { + Assert.That(parent, Is.Null); + } }); foreach (var (slotId, slot) in component.Children) @@ -64,19 +74,22 @@ public sealed class SaveLoadReparentTest Assert.Multiple(() => { Assert.That(slot.Id, Is.EqualTo(slotId)); - Assert.That(slot.Parent, Is.Not.EqualTo(default(EntityUid))); + var container = + containerSystem.GetContainer(id, SharedBodySystem.GetPartSlotContainerId(slotId)); + Assert.That(container.ContainedEntities, Is.Not.Empty); }); } } foreach (var (id, component) in organs) { + var parent = bodySystem.GetParentPartOrNull(id); + Assert.Multiple(() => { Assert.That(component.Body, Is.EqualTo(human)); - Assert.That(component.ParentSlot, Is.Not.Null); - Assert.That(component.ParentSlot.Parent, Is.Not.EqualTo(default(EntityUid))); - Assert.That(component.ParentSlot.Child, Is.EqualTo(id)); + Assert.That(parent, Is.Not.Null); + Assert.That(parent.Value, Is.Not.EqualTo(default(EntityUid))); }); } @@ -115,7 +128,7 @@ public sealed class SaveLoadReparentTest foreach (var (uid, body) in query) { human = uid; - parts = bodySystem.GetBodyChildren(human).ToArray(); + parts = bodySystem.GetBodyChildren(human).Skip(1).ToArray(); organs = bodySystem.GetBodyOrgans(human).ToArray(); Assert.Multiple(() => @@ -126,12 +139,13 @@ public sealed class SaveLoadReparentTest foreach (var (id, component) in parts) { + var parent = bodySystem.GetParentPartOrNull(id); + Assert.Multiple(() => { Assert.That(component.Body, Is.EqualTo(human)); - Assert.That(component.ParentSlot, Is.Not.Null); - Assert.That(component.ParentSlot.Parent, Is.Not.EqualTo(default(EntityUid))); - Assert.That(component.ParentSlot.Child, Is.EqualTo(id)); + Assert.That(parent, Is.Not.Null); + Assert.That(parent.Value, Is.Not.EqualTo(default(EntityUid))); }); foreach (var (slotId, slot) in component.Children) @@ -139,19 +153,22 @@ public sealed class SaveLoadReparentTest Assert.Multiple(() => { Assert.That(slot.Id, Is.EqualTo(slotId)); - Assert.That(slot.Parent, Is.Not.EqualTo(default(EntityUid))); + var container = + containerSystem.GetContainer(id, SharedBodySystem.GetPartSlotContainerId(slotId)); + Assert.That(container.ContainedEntities, Is.Not.Empty); }); } } foreach (var (id, component) in organs) { + var parent = bodySystem.GetParentPartOrNull(id); + Assert.Multiple(() => { Assert.That(component.Body, Is.EqualTo(human)); - Assert.That(component.ParentSlot, Is.Not.Null); - Assert.That(component.ParentSlot.Parent, Is.Not.EqualTo(default(EntityUid))); - Assert.That(component.ParentSlot.Child, Is.EqualTo(id)); + Assert.That(parent, Is.Not.Null); + Assert.That(parent.Value, Is.Not.EqualTo(default(EntityUid))); }); } diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs index 2b6c1947b6..58a7156764 100644 --- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs +++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs @@ -258,6 +258,7 @@ namespace Content.IntegrationTests.Tests.Buckle var entityManager = server.ResolveDependency(); var handsSys = entityManager.EntitySysManager.GetEntitySystem(); var buckleSystem = entityManager.EntitySysManager.GetEntitySystem(); + var xformSystem = entityManager.System(); await server.WaitAssertion(() => { @@ -309,7 +310,7 @@ namespace Content.IntegrationTests.Tests.Buckle // Break our guy's kneecaps foreach (var leg in legs) { - bodySystem.DropPart(leg.Id, leg.Component); + xformSystem.DetachParentToNull(leg.Id, entityManager.GetComponent(leg.Id)); } }); diff --git a/Content.IntegrationTests/Tests/EncryptionKeys/RemoveEncryptionKeys.cs b/Content.IntegrationTests/Tests/EncryptionKeys/RemoveEncryptionKeys.cs index 742a968d2d..9e3dbd8863 100644 --- a/Content.IntegrationTests/Tests/EncryptionKeys/RemoveEncryptionKeys.cs +++ b/Content.IntegrationTests/Tests/EncryptionKeys/RemoveEncryptionKeys.cs @@ -30,7 +30,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest Assert.That(comp.Channels, Has.Count.EqualTo(0)); }); - // Checkl that the key was ejected and not just deleted or something. + // Check that the key was ejected and not just deleted or something. await AssertEntityLookup(("EncryptionKeyCommon", 1)); // Re-insert a key. diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index a3119a1c30..b4b6c2239f 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -218,12 +218,13 @@ public abstract partial class InteractionTest // Ensure that the player only has one hand, so that they do not accidentally pick up deconstruction products await Server.WaitPost(() => { + // I lost an hour of my life trying to track down how the hell interaction tests were breaking + // so greatz to this. Just make your own body prototype! var bodySystem = SEntMan.System(); var hands = bodySystem.GetBodyChildrenOfType(SEntMan.GetEntity(Player), BodyPartType.Hand).ToArray(); for (var i = 1; i < hands.Length; i++) { - bodySystem.DropPart(hands[i].Id); SEntMan.DeleteEntity(hands[i].Id); } }); diff --git a/Content.Server/Administration/Commands/AddBodyPartCommand.cs b/Content.Server/Administration/Commands/AddBodyPartCommand.cs index 235a1ed871..892a88d41a 100644 --- a/Content.Server/Administration/Commands/AddBodyPartCommand.cs +++ b/Content.Server/Administration/Commands/AddBodyPartCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Body.Systems; using Content.Shared.Administration; +using Content.Shared.Body.Part; using Robust.Shared.Console; namespace Content.Server.Administration.Commands @@ -11,7 +12,7 @@ namespace Content.Server.Administration.Commands public string Command => "addbodypart"; public string Description => "Adds a given entity to a containing body."; - public string Help => "Usage: addbodypart "; + public string Help => "Usage: addbodypart "; public void Execute(IConsoleShell shell, string argStr, string[] args) { @@ -37,7 +38,10 @@ namespace Content.Server.Administration.Commands var parentId = _entManager.GetEntity(parentNetId); var bodySystem = _entManager.System(); - if (bodySystem.TryCreatePartSlotAndAttach(parentId, args[2], childId)) + + + if (Enum.TryParse(args[3], out var partType) && + bodySystem.TryCreatePartSlotAndAttach(parentId, args[2], childId, partType)) { shell.WriteLine($@"Added {childId} to {parentId}."); } diff --git a/Content.Server/Administration/Commands/AddMechanismCommand.cs b/Content.Server/Administration/Commands/AddMechanismCommand.cs index 0f7a442bb1..e62ddea47b 100644 --- a/Content.Server/Administration/Commands/AddMechanismCommand.cs +++ b/Content.Server/Administration/Commands/AddMechanismCommand.cs @@ -35,7 +35,7 @@ namespace Content.Server.Administration.Commands var bodySystem = _entManager.System(); - if (bodySystem.AddOrganToFirstValidSlot(organId, partId)) + if (bodySystem.AddOrganToFirstValidSlot(partId.Value, organId.Value)) { shell.WriteLine($@"Added {organId} to {partId}."); } diff --git a/Content.Server/Administration/Commands/RemoveBodyPartCommand.cs b/Content.Server/Administration/Commands/RemoveBodyPartCommand.cs index 179f75d62d..6484405162 100644 --- a/Content.Server/Administration/Commands/RemoveBodyPartCommand.cs +++ b/Content.Server/Administration/Commands/RemoveBodyPartCommand.cs @@ -27,16 +27,10 @@ namespace Content.Server.Administration.Commands return; } - var bodySystem = _entManager.System(); - - if (bodySystem.DropPart(entityUid)) - { - shell.WriteLine($"Removed body part {_entManager.ToPrettyString(entityUid.Value)}."); - } - else - { - shell.WriteError("Was not a body part, or did not have a parent."); - } + // TODO: THIS IS JUST A MECHANISM COPYPASTE + var xformSystem = _entManager.System(); + xformSystem.AttachToGridOrMap(entityUid.Value); + shell.WriteLine($"Removed body part {_entManager.ToPrettyString(entityUid.Value)}"); } } } diff --git a/Content.Server/Administration/Commands/RemoveMechanismCommand.cs b/Content.Server/Administration/Commands/RemoveMechanismCommand.cs index cabf6c582b..fadf14464e 100644 --- a/Content.Server/Administration/Commands/RemoveMechanismCommand.cs +++ b/Content.Server/Administration/Commands/RemoveMechanismCommand.cs @@ -27,16 +27,9 @@ namespace Content.Server.Administration.Commands return; } - var bodySystem = _entManager.System(); - - if (bodySystem.DropOrgan(entityUid)) - { - shell.WriteLine($"Removed organ {_entManager.ToPrettyString(entityUid.Value)}"); - } - else - { - shell.WriteError("Was not a mechanism, or did not have a parent."); - } + var xformSystem = _entManager.System(); + xformSystem.AttachToGridOrMap(entityUid.Value); + shell.WriteLine($"Removed organ {_entManager.ToPrettyString(entityUid.Value)}"); } } } diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index 5d9eaa0f29..e7b028cb94 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -76,6 +76,7 @@ public sealed partial class AdminVerbSystem [Dependency] private readonly VomitSystem _vomitSystem = default!; [Dependency] private readonly WeldableSystem _weldableSystem = default!; [Dependency] private readonly SharedContentEyeSystem _eyeSystem = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; // All smite verbs have names so invokeverb works. private void AddSmiteVerbs(GetVerbsEvent args) @@ -293,8 +294,7 @@ public sealed partial class AdminVerbSystem if (HasComp(xform.Owner) || HasComp(xform.Owner)) continue; - var coordinates = baseXform.Coordinates.Offset(_random.NextVector2(0.5f, 0.75f)); - _bodySystem.DropOrganAt(organ.Owner, coordinates, organ); + _transformSystem.AttachToGridOrMap(organ.Owner); } _popupSystem.PopupEntity(Loc.GetString("admin-smite-vomit-organs-self"), args.Target, @@ -317,7 +317,7 @@ public sealed partial class AdminVerbSystem var baseXform = Transform(args.Target); foreach (var part in _bodySystem.GetBodyChildrenOfType(args.Target, BodyPartType.Hand)) { - _bodySystem.DropPartAt(part.Id, baseXform.Coordinates, part.Component); + _transformSystem.AttachToGridOrMap(part.Id); } _popupSystem.PopupEntity(Loc.GetString("admin-smite-remove-hands-self"), args.Target, args.Target, PopupType.LargeCaution); @@ -339,7 +339,7 @@ public sealed partial class AdminVerbSystem var baseXform = Transform(args.Target); foreach (var part in _bodySystem.GetBodyChildrenOfType(body.Owner, BodyPartType.Hand, body)) { - _bodySystem.DropPartAt(part.Id, baseXform.Coordinates, part.Component); + _transformSystem.AttachToGridOrMap(part.Id); break; } _popupSystem.PopupEntity(Loc.GetString("admin-smite-remove-hands-self"), args.Target, diff --git a/Content.Server/Body/Commands/AddHandCommand.cs b/Content.Server/Body/Commands/AddHandCommand.cs index bdb07ca7e1..308295c06c 100644 --- a/Content.Server/Body/Commands/AddHandCommand.cs +++ b/Content.Server/Body/Commands/AddHandCommand.cs @@ -118,7 +118,7 @@ namespace Content.Server.Body.Commands } } - if (!_entManager.TryGetComponent(entity, out BodyComponent? body) || body.Root == null) + if (!_entManager.TryGetComponent(entity, out BodyComponent? body) || body.RootContainer.ContainedEntity == null) { var text = $"You have no body{(_random.Prob(0.2f) ? " and you must scream." : ".")}"; @@ -140,7 +140,7 @@ namespace Content.Server.Body.Commands var slotId = part.GetHashCode().ToString(); - if (!bodySystem.TryCreatePartSlotAndAttach(attachAt.Id, slotId, hand, attachAt.Component, part)) + if (!bodySystem.TryCreatePartSlotAndAttach(attachAt.Id, slotId, hand, BodyPartType.Hand,attachAt.Component, part)) { shell.WriteError($"Couldn't create a slot with id {slotId} on entity {_entManager.ToPrettyString(entity)}"); return; diff --git a/Content.Server/Body/Commands/AttachBodyPartCommand.cs b/Content.Server/Body/Commands/AttachBodyPartCommand.cs index 9a80a200e0..267b520808 100644 --- a/Content.Server/Body/Commands/AttachBodyPartCommand.cs +++ b/Content.Server/Body/Commands/AttachBodyPartCommand.cs @@ -94,7 +94,7 @@ namespace Content.Server.Body.Commands } var bodySystem = _entManager.System(); - if (bodySystem.BodyHasChild(bodyId, partUid, body, part)) + if (bodySystem.BodyHasChild(bodyId, partUid.Value, body, part)) { shell.WriteLine($"Body part {_entManager.GetComponent(partUid.Value).EntityName} with uid {partUid} is already attached to entity {_entManager.GetComponent(bodyId).EntityName} with uid {bodyId}"); return; @@ -103,16 +103,14 @@ namespace Content.Server.Body.Commands var slotId = $"AttachBodyPartVerb-{partUid}"; // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (bodySystem.TryCreateBodyRootSlot(bodyId, slotId, out var rootSlot, body)) + if (body.RootContainer.ContainedEntity != null) { - bodySystem.DropPart(partUid, part); - bodySystem.AttachPart(partUid, rootSlot, part); + bodySystem.AttachPartToRoot(bodyId,partUid.Value, body ,part); } else { - var attachAt = bodySystem.GetBodyChildren(bodyId, body).First(); - - if (!bodySystem.TryCreatePartSlotAndAttach(attachAt.Id, slotId, partUid, attachAt.Component, part)) + var (rootPartId,rootPart) = bodySystem.GetRootPartOrNull(bodyId, body)!.Value; + if (!bodySystem.TryCreatePartSlotAndAttach(rootPartId, slotId, partUid.Value, part.PartType, rootPart, part)) { shell.WriteError($"Could not create slot {slotId} on entity {_entManager.ToPrettyString(bodyId)}"); return; diff --git a/Content.Server/Body/Commands/DestroyMechanismCommand.cs b/Content.Server/Body/Commands/DestroyMechanismCommand.cs index 57cd2452ab..f4e299fac9 100644 --- a/Content.Server/Body/Commands/DestroyMechanismCommand.cs +++ b/Content.Server/Body/Commands/DestroyMechanismCommand.cs @@ -55,7 +55,7 @@ namespace Content.Server.Body.Commands { if (fac.GetComponentName(organ.Component.GetType()).ToLowerInvariant() == mechanismName) { - bodySystem.DeleteOrgan(organ.Id, organ.Component); + entityManager.QueueDeleteEntity(organ.Id); shell.WriteLine($"Mechanism with name {mechanismName} has been destroyed."); return; } diff --git a/Content.Server/Body/Commands/RemoveHandCommand.cs b/Content.Server/Body/Commands/RemoveHandCommand.cs index e4f8ec66f9..729db4bc44 100644 --- a/Content.Server/Body/Commands/RemoveHandCommand.cs +++ b/Content.Server/Body/Commands/RemoveHandCommand.cs @@ -11,8 +11,11 @@ using Robust.Shared.Random; namespace Content.Server.Body.Commands { [AdminCommand(AdminFlags.Fun)] - sealed class RemoveHandCommand : IConsoleCommand + public sealed class RemoveHandCommand : IConsoleCommand { + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + public string Command => "removehand"; public string Description => "Removes a hand from your entity."; public string Help => $"Usage: {Command}"; @@ -32,18 +35,16 @@ namespace Content.Server.Body.Commands return; } - var entityManager = IoCManager.Resolve(); - if (!entityManager.TryGetComponent(player.AttachedEntity, out BodyComponent? body)) + if (!_entManager.TryGetComponent(player.AttachedEntity, out BodyComponent? body)) { - var random = IoCManager.Resolve(); - var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}"; + var text = $"You have no body{(_random.Prob(0.2f) ? " and you must scream." : ".")}"; shell.WriteLine(text); return; } - var bodySystem = entityManager.System(); - var hand = bodySystem.GetBodyChildrenOfType(player.AttachedEntity, BodyPartType.Hand, body).FirstOrDefault(); + var bodySystem = _entManager.System(); + var hand = bodySystem.GetBodyChildrenOfType(player.AttachedEntity.Value, BodyPartType.Hand, body).FirstOrDefault(); if (hand == default) { @@ -51,7 +52,7 @@ namespace Content.Server.Body.Commands } else { - bodySystem.DropPart(hand.Id, hand.Component); + _entManager.System().AttachToGridOrMap(hand.Id); } } } diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs index aa9fd4dd91..512d22d01b 100644 --- a/Content.Server/Body/Systems/BodySystem.cs +++ b/Content.Server/Body/Systems/BodySystem.cs @@ -17,6 +17,7 @@ using Content.Shared.Movement.Events; using Content.Shared.Random.Helpers; using Robust.Shared.Audio; using Robust.Shared.Containers; +using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -32,80 +33,16 @@ public sealed class BodySystem : SharedBodySystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnPartStartup); - SubscribeLocalEvent(OnBodyStartup); SubscribeLocalEvent(OnRelayMoveInput); SubscribeLocalEvent(OnApplyMetabolicMultiplier); SubscribeLocalEvent(OnBeingMicrowaved); } - private void OnPartStartup(EntityUid uid, BodyPartComponent component, ComponentStartup args) - { - // This inter-entity relationship makes be deeply uncomfortable because its probably going to re-encounter - // all of the networking & startup ordering issues that containers and joints have. - // TODO just use containers. Please. - - foreach (var slot in component.Children.Values) - { - DebugTools.Assert(slot.Parent == uid); - if (slot.Child == null) - continue; - - if (TryComp(slot.Child, out BodyPartComponent? child)) - { - child.ParentSlot = slot; - Dirty(slot.Child.Value, child); - continue; - } - - Log.Error($"Body part encountered missing limbs: {ToPrettyString(uid)}. Slot: {slot.Id}"); - slot.Child = null; - } - - foreach (var slot in component.Organs.Values) - { - DebugTools.Assert(slot.Parent == uid); - if (slot.Child == null) - continue; - - if (TryComp(slot.Child, out OrganComponent? child)) - { - child.ParentSlot = slot; - Dirty(slot.Child.Value, child); - continue; - } - - Log.Error($"Body part encountered missing organ: {ToPrettyString(uid)}. Slot: {slot.Id}"); - slot.Child = null; - } - } - - private void OnBodyStartup(EntityUid uid, BodyComponent component, ComponentStartup args) - { - if (component.Root is not { } slot) - return; - - DebugTools.Assert(slot.Parent == uid); - if (slot.Child == null) - return; - - if (!TryComp(slot.Child, out BodyPartComponent? child)) - { - Log.Error($"Body part encountered missing limbs: {ToPrettyString(uid)}. Slot: {slot.Id}"); - slot.Child = null; - return; - } - - child.ParentSlot = slot; - Dirty(slot.Child.Value, child); - } - private void OnRelayMoveInput(EntityUid uid, BodyComponent component, ref MoveInputEvent args) { if (_mobState.IsDead(uid) && _mindSystem.TryGetMind(uid, out var mindId, out var mind)) @@ -130,98 +67,95 @@ public sealed class BodySystem : SharedBodySystem return; // Don't microwave animals, kids - _transform.AttachToGridOrMap(uid); + SharedTransform.AttachToGridOrMap(uid); _appearance.SetData(args.Microwave, MicrowaveVisualState.Bloody, true); GibBody(uid, false, component); args.Handled = true; } - public override bool AttachPart( - EntityUid? partId, - BodyPartSlot slot, - [NotNullWhen(true)] BodyPartComponent? part = null) + protected override void AddPart( + EntityUid bodyUid, + EntityUid partUid, + string slotId, + BodyPartComponent component, + BodyComponent? bodyComp = null) { - if (!base.AttachPart(partId, slot, part)) - return false; + // TODO: Predict this probably. + base.AddPart(bodyUid, partUid, slotId, component, bodyComp); - if (part.Body is { } body && - TryComp(body, out var humanoid)) + if (TryComp(bodyUid, out var humanoid)) { - var layer = part.ToHumanoidLayers(); + var layer = component.ToHumanoidLayers(); if (layer != null) { var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value); - _humanoidSystem.SetLayersVisibility(body, layers, true, true, humanoid); + _humanoidSystem.SetLayersVisibility(bodyUid, layers, true, true, humanoid); } } - - return true; } - public override bool DropPart(EntityUid? partId, BodyPartComponent? part = null) + protected override void RemovePart( + EntityUid bodyUid, + EntityUid partUid, + string slotId, + BodyPartComponent component, + BodyComponent? bodyComp = null) { - if (partId == null || !Resolve(partId.Value, ref part)) - return false; + base.RemovePart(bodyUid, partUid, slotId, component, bodyComp); - if (!base.DropPart(partId, part)) - return false; + if (!TryComp(bodyUid, out var humanoid)) + return; - var oldBody = part.Body; - if (oldBody == null || !TryComp(oldBody, out var humanoid)) - return true; + var layer = component.ToHumanoidLayers(); - var layer = part.ToHumanoidLayers(); if (layer == null) - return true; + return; var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value); - _humanoidSystem.SetLayersVisibility(oldBody.Value, layers, false, true, humanoid); - return true; + _humanoidSystem.SetLayersVisibility(bodyUid, layers, false, true, humanoid); } - public override HashSet GibBody(EntityUid? bodyId, bool gibOrgans = false, BodyComponent? body = null, bool deleteItems = false) + public override HashSet GibBody(EntityUid bodyId, bool gibOrgans = false, BodyComponent? body = null, bool deleteItems = false) { - if (bodyId == null || !Resolve(bodyId.Value, ref body, false)) + if (!Resolve(bodyId, ref body, false)) return new HashSet(); - if (LifeStage(bodyId.Value) >= EntityLifeStage.Terminating || EntityManager.IsQueuedForDeletion(bodyId.Value)) + if (LifeStage(bodyId) >= EntityLifeStage.Terminating || EntityManager.IsQueuedForDeletion(bodyId)) return new HashSet(); - var xform = Transform(bodyId.Value); + var xform = Transform(bodyId); if (xform.MapUid == null) return new HashSet(); var gibs = base.GibBody(bodyId, gibOrgans, body, deleteItems); var coordinates = xform.Coordinates; - var filter = Filter.Pvs(bodyId.Value, entityManager: EntityManager); + var filter = Filter.Pvs(bodyId, entityManager: EntityManager); var audio = AudioParams.Default.WithVariation(0.025f); _audio.Play(body.GibSound, filter, coordinates, true, audio); - if (TryComp(bodyId, out ContainerManagerComponent? container)) + var containers = GetBodyContainers(bodyId, body: body).ToList(); + + foreach (var container in containers) { - foreach (var cont in container.GetAllContainers().ToArray()) + foreach (var entity in container.ContainedEntities) { - foreach (var ent in cont.ContainedEntities.ToArray()) + if (deleteItems) { - if (deleteItems) - { - QueueDel(ent); - } - else - { - cont.Remove(ent, EntityManager, force: true); - Transform(ent).Coordinates = coordinates; - ent.RandomOffset(0.25f); - } + QueueDel(entity); + } + else + { + container.Remove(entity, EntityManager, force: true); + SharedTransform.SetCoordinates(entity,coordinates); + entity.RandomOffset(0.25f); } } } - - RaiseLocalEvent(bodyId.Value, new BeingGibbedEvent(gibs)); - QueueDel(bodyId.Value); + RaiseLocalEvent(bodyId, new BeingGibbedEvent(gibs)); + QueueDel(bodyId); return gibs; } diff --git a/Content.Server/Body/Systems/BrainSystem.cs b/Content.Server/Body/Systems/BrainSystem.cs index 9b5da508dd..629db0a1fb 100644 --- a/Content.Server/Body/Systems/BrainSystem.cs +++ b/Content.Server/Body/Systems/BrainSystem.cs @@ -30,17 +30,18 @@ namespace Content.Server.Body.Systems private void OnRemovedFromBody(EntityUid uid, BrainComponent component, RemovedFromBodyEvent args) { // This one needs to be special, okay? - if (!EntityManager.TryGetComponent(uid, out OrganComponent? organ) || - organ.ParentSlot is not {Parent: var parent}) + if (!EntityManager.TryGetComponent(uid, out OrganComponent? organ)) + { return; + } - HandleMind(parent, args.Old); + HandleMind(uid, args.Old); } private void HandleMind(EntityUid newEntity, EntityUid oldEntity) { EnsureComp(newEntity); - var oldMind = EnsureComp(oldEntity); + EnsureComp(oldEntity); var ghostOnMove = EnsureComp(newEntity); if (HasComp(newEntity)) diff --git a/Content.Server/Cargo/Systems/PricingSystem.cs b/Content.Server/Cargo/Systems/PricingSystem.cs index d1f6684bc3..f66e6f02ac 100644 --- a/Content.Server/Cargo/Systems/PricingSystem.cs +++ b/Content.Server/Cargo/Systems/PricingSystem.cs @@ -94,8 +94,9 @@ public sealed class PricingSystem : EntitySystem return; } - var partList = _bodySystem.GetBodyAllSlots(uid, body).ToList(); - var totalPartsPresent = partList.Sum(x => x.Child != null ? 1 : 0); + // TODO: Better handling of missing. + var partList = _bodySystem.GetBodyChildren(uid, body).ToList(); + var totalPartsPresent = partList.Sum(_ => 1); var totalParts = partList.Count; var partRatio = totalPartsPresent / (double) totalParts; diff --git a/Content.Server/Climbing/ClimbSystem.cs b/Content.Server/Climbing/ClimbSystem.cs index 08830fc89c..73c0eda3f3 100644 --- a/Content.Server/Climbing/ClimbSystem.cs +++ b/Content.Server/Climbing/ClimbSystem.cs @@ -300,8 +300,8 @@ public sealed class ClimbSystem : SharedClimbSystem if (!HasComp(user) || !TryComp(user, out BodyComponent? body) - || !_bodySystem.BodyHasChildOfType(user, BodyPartType.Leg, body) - || !_bodySystem.BodyHasChildOfType(user, BodyPartType.Foot, body)) + || !_bodySystem.BodyHasPartType(user, BodyPartType.Leg, body) + || !_bodySystem.BodyHasPartType(user, BodyPartType.Foot, body)) { reason = Loc.GetString("comp-climbable-cant-climb"); return false; diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs index 878a14ef73..4af1584eae 100644 --- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs @@ -192,11 +192,6 @@ namespace Content.Server.Kitchen.EntitySystems foreach (var part in headSlots) { - if (!_bodySystem.OrphanPart(part.Id, part.Component)) - { - continue; - } - component.Storage.Insert(part.Id); headCount++; } diff --git a/Content.Server/Toilet/ToiletSystem.cs b/Content.Server/Toilet/ToiletSystem.cs index e749260e94..57467397c6 100644 --- a/Content.Server/Toilet/ToiletSystem.cs +++ b/Content.Server/Toilet/ToiletSystem.cs @@ -53,7 +53,7 @@ namespace Content.Server.Toilet // Check that victim has a head // FIXME: since suiciding turns you into a ghost immediately, both messages are seen, not sure how this can be fixed if (TryComp(args.Victim, out var body) && - _body.BodyHasChildOfType(args.Victim, BodyPartType.Head, body)) + _body.BodyHasPartType(args.Victim, BodyPartType.Head, body)) { var othersMessage = Loc.GetString("toilet-component-suicide-head-message-others", ("victim", Identity.Entity(args.Victim, EntityManager)), ("owner", uid)); diff --git a/Content.Shared/Body/Components/BodyComponent.cs b/Content.Shared/Body/Components/BodyComponent.cs index 84027d2c37..a93f1e2fd8 100644 --- a/Content.Shared/Body/Components/BodyComponent.cs +++ b/Content.Shared/Body/Components/BodyComponent.cs @@ -1,9 +1,10 @@ -using Content.Shared.Body.Part; using Content.Shared.Body.Prototypes; using Content.Shared.Body.Systems; using Robust.Shared.Audio; +using Robust.Shared.Containers; using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; namespace Content.Shared.Body.Components; @@ -11,19 +12,52 @@ namespace Content.Shared.Body.Components; [Access(typeof(SharedBodySystem))] public sealed partial class BodyComponent : Component { - [DataField("prototype", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? Prototype; + /// + /// Relevant template to spawn for this body. + /// + [DataField] + public ProtoId? Prototype; - [DataField("root")] - public BodyPartSlot? Root; + /// + /// Container that holds the root body part. + /// + /// + /// Typically is the torso. + /// + [ViewVariables] public ContainerSlot RootContainer = default!; - [DataField("gibSound")] - public SoundSpecifier GibSound = new SoundCollectionSpecifier("gib"); + [ViewVariables] + public string RootPartSlot => RootContainer.ID; + + [DataField] public SoundSpecifier GibSound = new SoundCollectionSpecifier("gib"); /// /// The amount of legs required to move at full speed. /// If 0, then legs do not impact speed. /// - [DataField("requiredLegs")] - public int RequiredLegs; + [DataField] public int RequiredLegs; + + [ViewVariables] + [DataField] + public HashSet LegEntities = new(); +} + +[Serializable, NetSerializable] +public sealed class BodyComponentState : ComponentState +{ + public string? Prototype; + public string? RootPartSlot; + public SoundSpecifier GibSound; + public int RequiredLegs; + public HashSet LegNetEntities; + + public BodyComponentState(string? prototype, string? rootPartSlot, SoundSpecifier gibSound, + int requiredLegs, HashSet legNetEntities) + { + Prototype = prototype; + RootPartSlot = rootPartSlot; + GibSound = gibSound; + RequiredLegs = requiredLegs; + LegNetEntities = legNetEntities; + } } diff --git a/Content.Shared/Body/Components/BodyComponentState.cs b/Content.Shared/Body/Components/BodyComponentState.cs deleted file mode 100644 index 615b76fdbc..0000000000 --- a/Content.Shared/Body/Components/BodyComponentState.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Shared.Body.Part; -using Robust.Shared.Audio; -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Components; - -[Serializable, NetSerializable] -public sealed class BodyComponentState : ComponentState -{ - public readonly BodyPartSlot? Root; - public readonly SoundSpecifier GibSound; - - public BodyComponentState(BodyPartSlot? root, SoundSpecifier gibSound) - { - Root = root; - GibSound = gibSound; - } -} diff --git a/Content.Shared/Body/Organ/OrganComponent.cs b/Content.Shared/Body/Organ/OrganComponent.cs index e48d6da300..9e1de6b355 100644 --- a/Content.Shared/Body/Organ/OrganComponent.cs +++ b/Content.Shared/Body/Organ/OrganComponent.cs @@ -1,16 +1,16 @@ using Content.Shared.Body.Systems; +using Robust.Shared.Containers; using Robust.Shared.GameStates; namespace Content.Shared.Body.Organ; -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access(typeof(SharedBodySystem))] public sealed partial class OrganComponent : Component { - [DataField("body")] + /// + /// Relevant body this organ is attached to. + /// + [DataField("body"), AutoNetworkedField] public EntityUid? Body; - - // TODO use containers. See comments in BodyPartComponent. - // Do not rely on this in client-side code. - public OrganSlot? ParentSlot; } diff --git a/Content.Shared/Body/Organ/OrganComponentState.cs b/Content.Shared/Body/Organ/OrganComponentState.cs deleted file mode 100644 index 6ca323fd13..0000000000 --- a/Content.Shared/Body/Organ/OrganComponentState.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Organ; - -[Serializable, NetSerializable] -public sealed class OrganComponentState : ComponentState -{ - public readonly NetEntity? Body; - public readonly OrganSlot? Parent; - - public OrganComponentState(NetEntity? body, OrganSlot? parent) - { - Body = body; - Parent = parent; - } -} diff --git a/Content.Shared/Body/Organ/OrganSlot.cs b/Content.Shared/Body/Organ/OrganSlot.cs deleted file mode 100644 index e67c9852bc..0000000000 --- a/Content.Shared/Body/Organ/OrganSlot.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Shared.Body.Systems; -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Organ; - -[Serializable, NetSerializable] -[Access(typeof(SharedBodySystem))] -[DataDefinition] -public sealed partial record OrganSlot -{ - [DataField("id")] - public string Id = string.Empty; - - [NonSerialized] - [DataField("parent")] - public EntityUid Parent; - - public NetEntity NetParent; - - [NonSerialized] - [DataField("child")] - public EntityUid? Child; - - public NetEntity? NetChild; - - // Rider doesn't suggest explicit properties during deconstruction without this - public void Deconstruct(out EntityUid? child, out string id, out EntityUid parent) - { - child = Child; - id = Id; - parent = Parent; - } -} diff --git a/Content.Shared/Body/Part/BodyPartComponent.cs b/Content.Shared/Body/Part/BodyPartComponent.cs index dd200c491a..cbbfef4d73 100644 --- a/Content.Shared/Body/Part/BodyPartComponent.cs +++ b/Content.Shared/Body/Part/BodyPartComponent.cs @@ -1,32 +1,23 @@ using Content.Shared.Body.Components; -using Content.Shared.Body.Organ; using Content.Shared.Body.Systems; +using Robust.Shared.Containers; using Robust.Shared.GameStates; +using Robust.Shared.Serialization; namespace Content.Shared.Body.Part; -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access(typeof(SharedBodySystem))] public sealed partial class BodyPartComponent : Component { - [DataField("body")] + // Need to set this on container changes as it may be several transform parents up the hierarchy. + /// + /// Parent body for this part. + /// + [DataField, AutoNetworkedField] public EntityUid? Body; - // This inter-entity relationship makes be deeply uncomfortable because its probably going to re-encounter all of the - // networking issues that containers and joints have. - // TODO just use containers. Please. - // Do not use set or get data from this in client-side code. - public BodyPartSlot? ParentSlot; - - // Do not use set or get data from this in client-side code. - [DataField("children")] - public Dictionary Children = new(); - - // See all the above ccomments. - [DataField("organs")] - public Dictionary Organs = new(); - - [DataField("partType")] + [DataField, AutoNetworkedField] public BodyPartType PartType = BodyPartType.Other; // TODO BODY Replace with a simulation of organs @@ -34,9 +25,90 @@ public sealed partial class BodyPartComponent : Component /// Whether or not the owning will die if all /// s of this type are removed from it. /// - [DataField("vital")] + [DataField("vital"), AutoNetworkedField] public bool IsVital; - [DataField("symmetry")] + [DataField, AutoNetworkedField] public BodyPartSymmetry Symmetry = BodyPartSymmetry.None; + + /// + /// Child body parts attached to this body part. + /// + [DataField, AutoNetworkedField(CloneData = true)] + public Dictionary Children = new(); + + /// + /// Organs attached to this body part. + /// + [DataField, AutoNetworkedField(CloneData = true)] + public Dictionary Organs = new(); + + /// + /// These are only for VV/Debug do not use these for gameplay/systems + /// + [ViewVariables] + private List BodyPartSlotsVV + { + get + { + List temp = new(); + var containerSystem = IoCManager.Resolve().System(); + + foreach (var slotId in Children.Keys) + { + temp.Add((ContainerSlot) containerSystem.GetContainer(Owner, slotId)); + } + + return temp; + } + } + + [ViewVariables] + private List OrganSlotsVV + { + get + { + List temp = new(); + var containerSystem = IoCManager.Resolve().System(); + + foreach (var slotId in Organs.Keys) + { + temp.Add((ContainerSlot) containerSystem.GetContainer(Owner, slotId)); + } + + return temp; + } + } } + +/// +/// Contains metadata about a body part in relation to its slot. +/// +[NetSerializable, Serializable] +[DataRecord] +public partial struct BodyPartSlot +{ + public string Id; + public BodyPartType Type; + + public BodyPartSlot(string id, BodyPartType type) + { + Id = id; + Type = type; + } +}; + +/// +/// Contains metadata about an organ part in relation to its slot. +/// +[NetSerializable, Serializable] +[DataRecord] +public partial struct OrganSlot +{ + public string Id; + + public OrganSlot(string id) + { + Id = id; + } +}; diff --git a/Content.Shared/Body/Part/BodyPartComponentState.cs b/Content.Shared/Body/Part/BodyPartComponentState.cs deleted file mode 100644 index e50ecdda12..0000000000 --- a/Content.Shared/Body/Part/BodyPartComponentState.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Content.Shared.Body.Organ; -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Part; - -[Serializable, NetSerializable] -public sealed class BodyPartComponentState : ComponentState -{ - public readonly NetEntity? Body; - public readonly BodyPartSlot? ParentSlot; - public readonly Dictionary Children; - public readonly Dictionary Organs; - public readonly BodyPartType PartType; - public readonly bool IsVital; - public readonly BodyPartSymmetry Symmetry; - - public BodyPartComponentState( - NetEntity? body, - BodyPartSlot? parentSlot, - Dictionary children, - Dictionary organs, - BodyPartType partType, - bool isVital, - BodyPartSymmetry symmetry) - { - ParentSlot = parentSlot; - Children = children; - Organs = organs; - PartType = partType; - IsVital = isVital; - Symmetry = symmetry; - Body = body; - } -} diff --git a/Content.Shared/Body/Part/BodyPartSlot.cs b/Content.Shared/Body/Part/BodyPartSlot.cs deleted file mode 100644 index 8f6348ce33..0000000000 --- a/Content.Shared/Body/Part/BodyPartSlot.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Content.Shared.Body.Systems; -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Part; - -[Serializable, NetSerializable] -[Access(typeof(SharedBodySystem))] -[DataDefinition] -public sealed partial record BodyPartSlot -{ - [DataField("id")] - public string Id = string.Empty; - - [DataField("type")] - public BodyPartType? Type; - - [NonSerialized] - [DataField("parent")] - public EntityUid Parent; - - public NetEntity NetParent; - - [NonSerialized] - [DataField("child")] - public EntityUid? Child; - - public NetEntity? NetChild; - - public void SetChild(EntityUid? child, NetEntity? netChild) - { - Child = child; - NetChild = netChild; - } - - // Rider doesn't suggest explicit properties during deconstruction without this - public void Deconstruct(out EntityUid? child, out string id, out EntityUid parent, out BodyPartType? type) - { - child = Child; - id = Id; - parent = Parent; - type = Type; - } -} diff --git a/Content.Shared/Body/Prototypes/BodyPrototype.cs b/Content.Shared/Body/Prototypes/BodyPrototype.cs index e3a8d01ce0..97ee0ba217 100644 --- a/Content.Shared/Body/Prototypes/BodyPrototype.cs +++ b/Content.Shared/Body/Prototypes/BodyPrototype.cs @@ -27,28 +27,4 @@ public sealed class BodyPrototype : IPrototype } [DataRecord] -public sealed record BodyPrototypeSlot -{ - [DataField("part", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? Part; - public readonly HashSet Connections = new(); - public readonly Dictionary Organs = new(); - - public BodyPrototypeSlot() : this(null, null, null) - { - } - - public BodyPrototypeSlot(string? part, HashSet? connections, Dictionary? organs) - { - Part = part; - Connections = connections ?? new HashSet(); - Organs = organs ?? new Dictionary(); - } - - public void Deconstruct(out string? part, out HashSet connections, out Dictionary organs) - { - part = Part; - connections = Connections; - organs = Organs; - } -} +public sealed record BodyPrototypeSlot(EntProtoId? Part, HashSet Connections, Dictionary Organs); diff --git a/Content.Shared/Body/Prototypes/BodyPrototypeSerializer.cs b/Content.Shared/Body/Prototypes/BodyPrototypeSerializer.cs index c149b84c63..e2b54bf951 100644 --- a/Content.Shared/Body/Prototypes/BodyPrototypeSerializer.cs +++ b/Content.Shared/Body/Prototypes/BodyPrototypeSerializer.cs @@ -179,9 +179,10 @@ public sealed class BodyPrototypeSerializer : ITypeReader(); + foreach (var (slotId, (part, connections, organs)) in allConnections) { - var slot = new BodyPrototypeSlot(part, connections, organs); + var slot = new BodyPrototypeSlot(part != null ? new EntProtoId(part) : null!, connections ?? new HashSet(), organs ?? new Dictionary()); slots.Add(slotId, slot); } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs index 9519faf0f1..f68899c8ee 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs @@ -1,28 +1,141 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Linq; +using System.Numerics; using Content.Shared.Body.Components; using Content.Shared.Body.Organ; using Content.Shared.Body.Part; using Content.Shared.Body.Prototypes; -using Content.Shared.Coordinates; using Content.Shared.DragDrop; using Robust.Shared.Containers; using Robust.Shared.GameStates; -using Robust.Shared.Network; +using Robust.Shared.Map; +using MapInitEvent = Robust.Shared.GameObjects.MapInitEvent; namespace Content.Shared.Body.Systems; public partial class SharedBodySystem { - [Dependency] private readonly INetManager _netManager = default!; + /* + * tl;dr of how bobby works + * - BodyComponent uses a BodyPrototype as a template. + * - On MapInit we spawn the root entity in the prototype and spawn all connections outwards from here + * - Each "connection" is a body part (e.g. arm, hand, etc.) and each part can also contain organs. + */ - public void InitializeBody() + private void InitializeBody() { - SubscribeLocalEvent(OnBodyInit); + // Body here to handle root body parts. + SubscribeLocalEvent(OnBodyInserted); + SubscribeLocalEvent(OnBodyRemoved); + SubscribeLocalEvent(OnBodyInit); + SubscribeLocalEvent(OnBodyMapInit); + SubscribeLocalEvent(OnBodyCanDrag); SubscribeLocalEvent(OnBodyGetState); SubscribeLocalEvent(OnBodyHandleState); - SubscribeLocalEvent(OnBodyCanDrag); + } + + private void OnBodyInserted(EntityUid uid, BodyComponent component, EntInsertedIntoContainerMessage args) + { + // Root body part? + var slotId = args.Container.ID; + + if (slotId != BodyRootContainerId) + return; + + var entity = args.Entity; + + if (TryComp(entity, out BodyPartComponent? childPart)) + { + AddPart(uid, entity, slotId, childPart); + RecursiveBodyUpdate(entity, uid, childPart); + } + + if (TryComp(entity, out OrganComponent? organ)) + { + AddOrgan(entity, uid, uid, organ); + } + } + + private void OnBodyRemoved(EntityUid uid, BodyComponent component, EntRemovedFromContainerMessage args) + { + // TODO: lifestage shenanigans + if (LifeStage(uid) >= EntityLifeStage.Terminating) + return; + + // Root body part? + var slotId = args.Container.ID; + + if (slotId != BodyRootContainerId) + return; + + var entity = args.Entity; + + if (TryComp(entity, out BodyPartComponent? childPart)) + { + RemovePart(uid, entity, slotId, childPart); + RecursiveBodyUpdate(entity, null, childPart); + } + + if (TryComp(entity, out OrganComponent? organ)) + { + RemoveOrgan(entity, uid, uid, organ); + } + } + + private void OnBodyHandleState(EntityUid uid, BodyComponent component, ref ComponentHandleState args) + { + if (args.Current is not BodyComponentState state) + return; + + component.Prototype = state.Prototype != null ? state.Prototype : null!; + component.GibSound = state.GibSound; + component.RequiredLegs = state.RequiredLegs; + component.LegEntities = EntityManager.EnsureEntitySet(state.LegNetEntities, uid); + } + + private void OnBodyGetState(EntityUid uid, BodyComponent component, ref ComponentGetState args) + { + args.State = new BodyComponentState( + component.Prototype, + component.RootPartSlot, + component.GibSound, + component.RequiredLegs, + EntityManager.GetNetEntitySet(component.LegEntities) + ); + } + + private void OnBodyInit(EntityUid bodyId, BodyComponent body, ComponentInit args) + { + // Setup the initial container. + body.RootContainer = Containers.EnsureContainer(bodyId, BodyRootContainerId); + } + + private void OnBodyMapInit(EntityUid bodyId, BodyComponent body, MapInitEvent args) + { + if (body.Prototype == null) + return; + + // One-time setup + // Obviously can't run in Init to avoid double-spawns on save / load. + var prototype = Prototypes.Index(body.Prototype.Value); + MapInitBody(bodyId, prototype); + } + + private void MapInitBody(EntityUid bodyEntity, BodyPrototype prototype) + { + var protoRoot = prototype.Slots[prototype.Root]; + if (protoRoot.Part == null) + return; + + // This should already handle adding the entity to the root. + var rootPartEntity = SpawnInContainerOrDrop(protoRoot.Part, bodyEntity, BodyRootContainerId); + var rootPart = Comp(rootPartEntity); + rootPart.Body = bodyEntity; + Dirty(rootPartEntity, rootPart); + + // Setup the rest of the body entities. + SetupOrgans(rootPartEntity, rootPart, protoRoot.Organs); + MapInitParts(rootPartEntity, prototype); } private void OnBodyCanDrag(EntityUid uid, BodyComponent component, ref CanDragEvent args) @@ -30,148 +143,114 @@ public partial class SharedBodySystem args.Handled = true; } - private void OnBodyInit(EntityUid bodyId, BodyComponent body, ComponentInit args) + /// + /// Sets up all of the relevant body parts for a particular body entity and root part. + /// + private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype) { - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (body.Prototype == null || body.Root != null) - return; + // Start at the root part and traverse the body graph, setting up parts as we go. + // Basic BFS pathfind. + var rootSlot = prototype.Root; + var frontier = new Queue(); + frontier.Enqueue(rootSlot); - var prototype = Prototypes.Index(body.Prototype); + // Child -> Parent connection. + var cameFrom = new Dictionary(); + // Maps slot to its relevant entity. + var cameFromEntities = new Dictionary(); + cameFromEntities[rootSlot] = rootPartId; - if (!_netManager.IsClient || IsClientSide(bodyId)) - InitBody(body, prototype); - - Dirty(body); // Client doesn't actually spawn the body, need to sync it - } - - private void OnBodyGetState(EntityUid uid, BodyComponent body, ref ComponentGetState args) - { - args.State = new BodyComponentState(body.Root, body.GibSound); - } - - private void OnBodyHandleState(EntityUid uid, BodyComponent body, ref ComponentHandleState args) - { - if (args.Current is not BodyComponentState state) - return; - - body.Root = state.Root; // TODO use containers. This is broken and does not work. - body.GibSound = state.GibSound; - } - - public bool TryCreateBodyRootSlot( - EntityUid? bodyId, - string slotId, - [NotNullWhen(true)] out BodyPartSlot? slot, - BodyComponent? body = null) - { - slot = null; - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (bodyId == null || - !Resolve(bodyId.Value, ref body, false) || - body.Root != null) - return false; - - slot = new BodyPartSlot + while (frontier.TryDequeue(out var currentSlotId)) { - Id = slotId, - Parent = bodyId.Value, - NetParent = GetNetEntity(bodyId.Value), - }; - body.Root = slot; + var currentSlot = prototype.Slots[currentSlotId]; - return true; + foreach (var connection in currentSlot.Connections) + { + // Already been handled + if (!cameFrom.TryAdd(connection, currentSlotId)) + continue; + + // Setup part + var connectionSlot = prototype.Slots[connection]; + var parentEntity = cameFromEntities[currentSlotId]; + var parentPartComponent = Comp(parentEntity); + + // Spawn the entity on the target + // then get the body part type, create the slot, and finally + // we can insert it into the container. + var childPart = Spawn(connectionSlot.Part, new EntityCoordinates(parentEntity, Vector2.Zero)); + cameFromEntities[connection] = childPart; + + var childPartComponent = Comp(childPart); + var partSlot = CreatePartSlot(parentEntity, connection, childPartComponent.PartType, parentPartComponent); + var cont = Containers.GetContainer(parentEntity, GetPartSlotContainerId(connection)); + + if (partSlot == null || !cont.Insert(childPart)) + { + Log.Error($"Could not create slot for connection {connection} in body {prototype.ID}"); + QueueDel(childPart); + continue; + } + + // Add organs + SetupOrgans(childPart, childPartComponent, connectionSlot.Organs); + + // Enqueue it so we can also get its neighbors. + frontier.Enqueue(connection); + } + } } - protected void InitBody(BodyComponent body, BodyPrototype prototype) + private void SetupOrgans(EntityUid partId, BodyPartComponent partComponent, Dictionary organs) { - var root = prototype.Slots[prototype.Root]; - Containers.EnsureContainer(body.Owner, BodyContainerId); - if (root.Part == null) - return; - var bodyId = Spawn(root.Part, body.Owner.ToCoordinates()); - var partComponent = Comp(bodyId); - var slot = new BodyPartSlot + foreach (var (organSlotId, organProto) in organs) { - Id = root.Part, - Type = partComponent.PartType, - Parent = body.Owner, - NetParent = GetNetEntity(body.Owner), - }; - body.Root = slot; - partComponent.Body = bodyId; + var slot = CreateOrganSlot(organSlotId, partId, partComponent); + SpawnInContainerOrDrop(organProto, partId, GetOrganContainerId(organSlotId)); - AttachPart(bodyId, slot, partComponent); - InitPart(partComponent, prototype, prototype.Root); - } - - protected void InitPart(BodyPartComponent parent, BodyPrototype prototype, string slotId, HashSet? initialized = null) - { - initialized ??= new HashSet(); - - if (initialized.Contains(slotId)) - return; - - initialized.Add(slotId); - - var (_, connections, organs) = prototype.Slots[slotId]; - connections = new HashSet(connections); - connections.ExceptWith(initialized); - - var coordinates = parent.Owner.ToCoordinates(); - var subConnections = new List<(BodyPartComponent child, string slotId)>(); - - Containers.EnsureContainer(parent.Owner, BodyContainerId); - - foreach (var connection in connections) - { - var childSlot = prototype.Slots[connection]; - if (childSlot.Part == null) - continue; - - var childPart = Spawn(childSlot.Part, coordinates); - var childPartComponent = Comp(childPart); - var slot = CreatePartSlot(connection, parent.Owner, childPartComponent.PartType, parent); if (slot == null) { - Logger.Error($"Could not create slot for connection {connection} in body {prototype.ID}"); - continue; + Log.Error($"Could not create organ for slot {organSlotId} in {ToPrettyString(partId)}"); } - - AttachPart(childPart, slot, childPartComponent); - subConnections.Add((childPartComponent, connection)); - } - - foreach (var (organSlotId, organId) in organs) - { - var organ = Spawn(organId, coordinates); - var organComponent = Comp(organ); - - var slot = CreateOrganSlot(organSlotId, parent.Owner, parent); - if (slot == null) - { - Logger.Error($"Could not create slot for connection {organSlotId} in body {prototype.ID}"); - continue; - } - - InsertOrgan(organ, slot, organComponent); - } - - foreach (var connection in subConnections) - { - InitPart(connection.child, prototype, connection.slotId, initialized); } } - public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyChildren(EntityUid? id, BodyComponent? body = null) + + /// + /// Gets all body containers on this entity including the root one. + /// + public IEnumerable GetBodyContainers(EntityUid id, BodyComponent? body = null, + BodyPartComponent? rootPart = null) + { + if (!Resolve(id, ref body, false) || + body.RootContainer.ContainedEntity == null || + !Resolve(body.RootContainer.ContainedEntity.Value, ref rootPart)) + { + yield break; + } + + yield return body.RootContainer; + + foreach (var childContainer in GetPartContainers(body.RootContainer.ContainedEntity.Value, rootPart)) + { + yield return childContainer; + } + } + + /// + /// Gets all child body parts of this entity, including the root entity. + /// + public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyChildren(EntityUid? id, BodyComponent? body = null, + BodyPartComponent? rootPart = null) { if (id == null || !Resolve(id.Value, ref body, false) || - !TryComp(body.Root?.Child, out BodyPartComponent? part)) + body.RootContainer.ContainedEntity == null || + !Resolve(body.RootContainer.ContainedEntity.Value, ref rootPart)) + { yield break; + } - yield return (body.Root.Child.Value, part); - - foreach (var child in GetPartChildren(body.Root.Child)) + foreach (var child in GetBodyPartChildren(body.RootContainer.ContainedEntity.Value, rootPart)) { yield return child; } @@ -191,55 +270,37 @@ public partial class SharedBodySystem } } - public IEnumerable GetBodyAllSlots(EntityUid? bodyId, BodyComponent? body = null) - { - if (bodyId == null || !Resolve(bodyId.Value, ref body, false)) - yield break; - - foreach (var slot in GetPartAllSlots(body.Root?.Child)) - { - yield return slot; - } - } - /// - /// Returns all body part slots in the graph, including ones connected by - /// body parts which are null. + /// Returns all body part slots for this entity. /// - /// - /// + /// + /// /// - public IEnumerable GetAllBodyPartSlots(EntityUid partId, BodyPartComponent? part = null) + public IEnumerable GetBodyAllSlots(EntityUid bodyId, BodyComponent? body = null) { - if (!Resolve(partId, ref part, false)) + if (!Resolve(bodyId, ref body, false) || body.RootContainer.ContainedEntity == null) yield break; - foreach (var slot in part.Children.Values) + foreach (var slot in GetAllBodyPartSlots(body.RootContainer.ContainedEntity.Value)) { - if (!TryComp(slot.Child, out var childPart)) - continue; - yield return slot; - - foreach (var child in GetAllBodyPartSlots(slot.Child.Value, childPart)) - { - yield return child; - } } } - public virtual HashSet GibBody(EntityUid? partId, bool gibOrgans = false, + public virtual HashSet GibBody(EntityUid bodyId, bool gibOrgans = false, BodyComponent? body = null, bool deleteItems = false) { - if (partId == null || !Resolve(partId.Value, ref body, false)) - return new HashSet(); + var gibs = new HashSet(); - var parts = GetBodyChildren(partId, body).ToArray(); - var gibs = new HashSet(parts.Length); + if (!Resolve(bodyId, ref body, false)) + return gibs; + + var parts = GetBodyChildren(bodyId, body).ToArray(); + gibs.EnsureCapacity(parts.Length); foreach (var part in parts) { - DropPart(part.Id, part.Component); + SharedTransform.AttachToGridOrMap(part.Id); gibs.Add(part.Id); if (!gibOrgans) @@ -247,7 +308,7 @@ public partial class SharedBodySystem foreach (var organ in GetPartOrgans(part.Id, part.Component)) { - DropOrgan(organ.Id, organ.Component); + SharedTransform.AttachToGridOrMap(organ.Id); gibs.Add(organ.Id); } } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs b/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs index 57fea15ba4..7ee39da00b 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs @@ -3,194 +3,142 @@ using Content.Shared.Body.Components; using Content.Shared.Body.Events; using Content.Shared.Body.Organ; using Content.Shared.Body.Part; -using Content.Shared.Random.Helpers; using Robust.Shared.Containers; -using Robust.Shared.GameStates; -using Robust.Shared.Map; namespace Content.Shared.Body.Systems; public partial class SharedBodySystem { - [Dependency] private readonly SharedContainerSystem _container = default!; - - private void InitializeOrgans() + private void AddOrgan(EntityUid uid, EntityUid bodyUid, EntityUid parentPartUid, OrganComponent component) { - SubscribeLocalEvent(OnOrganGetState); - SubscribeLocalEvent(OnOrganHandleState); + component.Body = bodyUid; + RaiseLocalEvent(uid, new AddedToPartEvent(bodyUid)); + + if (component.Body != null) + RaiseLocalEvent(uid, new AddedToPartInBodyEvent(component.Body.Value, parentPartUid)); + + Dirty(uid, component); } + private void RemoveOrgan(EntityUid uid, EntityUid bodyUid, EntityUid parentPartUid, OrganComponent component) + { + RaiseLocalEvent(uid, new RemovedFromPartEvent(bodyUid)); + + if (component.Body != null) + RaiseLocalEvent(uid, new RemovedFromPartInBodyEvent(component.Body.Value, parentPartUid)); + + component.Body = null; + Dirty(uid, component); + } + + /// + /// Creates the specified organ slot on the parent entity. + /// private OrganSlot? CreateOrganSlot(string slotId, EntityUid parent, BodyPartComponent? part = null) { if (!Resolve(parent, ref part, false)) return null; - var slot = new OrganSlot() - { - Id = slotId, - Parent = parent, - NetParent = GetNetEntity(parent), - }; + Containers.EnsureContainer(parent, GetOrganContainerId(slotId)); + var slot = new OrganSlot(slotId); part.Organs.Add(slotId, slot); - return slot; } - private bool CanInsertOrgan(EntityUid? organId, OrganSlot slot, OrganComponent? organ = null) + /// + /// Attempts to create the specified organ slot on the specified parent if it exists. + /// + public bool TryCreateOrganSlot( + EntityUid? parent, + string slotId, + [NotNullWhen(true)] out OrganSlot? slot, + BodyPartComponent? part = null) { - return organId != null && - slot.Child == null && - Resolve(organId.Value, ref organ, false) && - Containers.TryGetContainer(slot.Parent, BodyContainerId, out var container) && - _container.CanInsert(organId.Value, container); - } + slot = null; - private void OnOrganGetState(EntityUid uid, OrganComponent organ, ref ComponentGetState args) - { - args.State = new OrganComponentState(GetNetEntity(organ.Body), organ.ParentSlot); - } - - private void OnOrganHandleState(EntityUid uid, OrganComponent organ, ref ComponentHandleState args) - { - if (args.Current is not OrganComponentState state) - return; - - organ.Body = EnsureEntity(state.Body, uid); - organ.ParentSlot = state.Parent; - } - - public bool InsertOrgan(EntityUid? organId, OrganSlot slot, OrganComponent? organ = null) - { - if (organId == null || - !Resolve(organId.Value, ref organ, false) || - !CanInsertOrgan(organId, slot, organ)) - return false; - - DropOrgan(slot.Child); - DropOrgan(organId, organ); - - var container = Containers.EnsureContainer(slot.Parent, BodyContainerId); - if (!container.Insert(organId.Value)) - return false; - - slot.Child = organId; - organ.ParentSlot = slot; - organ.Body = CompOrNull(slot.Parent)?.Body; - - DirtyAllComponents(slot.Parent); - Dirty(organId.Value, organ); - - if (organ.Body == null) + if (parent == null || !Resolve(parent.Value, ref part, false)) { - RaiseLocalEvent(organId.Value, new AddedToPartEvent(slot.Parent)); - } - else - { - RaiseLocalEvent(organId.Value, new AddedToPartInBodyEvent(organ.Body.Value, slot.Parent)); + return false; } - return true; + Containers.EnsureContainer(parent.Value, GetOrganContainerId(slotId)); + slot = new OrganSlot(slotId); + return part.Organs.TryAdd(slotId,slot.Value); } - public void DirtyAllComponents(EntityUid uid) + /// + /// Returns whether the slotId exists on the partId. + /// + public bool CanInsertOrgan(EntityUid partId, string slotId, BodyPartComponent? part = null) { - // TODO just use containers. Please - if (TryComp(uid, out BodyPartComponent? part)) - Dirty(uid, part); - - if (TryComp(uid, out OrganComponent? organ)) - Dirty(uid, organ); - - if (TryComp(uid, out BodyComponent? body)) - Dirty(uid, body); + return Resolve(partId, ref part) && part.Organs.ContainsKey(slotId); } + /// + /// Returns whether the specified organ slot exists on the partId. + /// + public bool CanInsertOrgan(EntityUid partId, OrganSlot slot, BodyPartComponent? part = null) + { + return CanInsertOrgan(partId, slot.Id, part); + } + public bool InsertOrgan(EntityUid partId, EntityUid organId, string slotId, BodyPartComponent? part = null, OrganComponent? organ = null) + { + if (!Resolve(organId, ref organ, false) || + !Resolve(partId, ref part, false) || + !CanInsertOrgan(partId, slotId, part)) + { + return false; + } + + var containerId = GetOrganContainerId(slotId); + + if (!Containers.TryGetContainer(partId, containerId, out var container)) + return false; + + return container.Insert(organId); + } + + /// + /// Removes the organ if it is inside of a body part. + /// + public bool RemoveOrgan(EntityUid organId, OrganComponent? organ = null) + { + if (!Containers.TryGetContainingContainer(organId, out var container)) + return false; + + var parent = container.Owner; + + if (!HasComp(parent)) + return false; + + return container.Remove(organId); + } + + /// + /// Tries to add this organ to any matching slot on this body part. + /// public bool AddOrganToFirstValidSlot( - EntityUid? childId, - EntityUid? parentId, - OrganComponent? child = null, - BodyPartComponent? parent = null) + EntityUid partId, + EntityUid organId, + BodyPartComponent? part = null, + OrganComponent? organ = null) { - if (childId == null || - !Resolve(childId.Value, ref child, false) || - parentId == null || - !Resolve(parentId.Value, ref parent, false)) - return false; - - foreach (var slot in parent.Organs.Values) + if (!Resolve(partId, ref part, false) || + !Resolve(organId, ref organ, false)) { - if (slot.Child == null) - continue; + return false; + } - InsertOrgan(childId, slot, child); + foreach (var slotId in part.Organs.Keys) + { + InsertOrgan(partId, organId, slotId, part, organ); return true; } return false; } - public bool DropOrgan(EntityUid? organId, OrganComponent? organ = null) - { - if (organId == null || - !Resolve(organId.Value, ref organ, false) || - organ.ParentSlot is not { } slot) - return false; - - var oldParent = CompOrNull(organ.ParentSlot.Parent); - - slot.Child = null; - organ.ParentSlot = null; - organ.Body = null; - - if (Containers.TryGetContainer(slot.Parent, BodyContainerId, out var container)) - container.Remove(organId.Value); - - if (TryComp(organId, out TransformComponent? transform)) - transform.AttachToGridOrMap(); - - organ.Owner.RandomOffset(0.25f); - - if (oldParent == null) - return true; - - if (oldParent.Body != null) - { - RaiseLocalEvent(organId.Value, new RemovedFromPartInBodyEvent(oldParent.Body.Value, oldParent.Owner)); - } - else - { - RaiseLocalEvent(organId.Value, new RemovedFromPartEvent(oldParent.Owner)); - } - - return true; - } - - public bool DropOrganAt(EntityUid? organId, EntityCoordinates dropAt, OrganComponent? organ = null) - { - if (organId == null || !DropOrgan(organId, organ)) - return false; - - if (TryComp(organId.Value, out TransformComponent? transform)) - transform.Coordinates = dropAt; - - return true; - } - - public bool DeleteOrgan(EntityUid? id, OrganComponent? part = null) - { - if (id == null || !Resolve(id.Value, ref part, false)) - return false; - - DropOrgan(id, part); - - if (Deleted(id.Value)) - return false; - - Del(id.Value); - return true; - } - /// /// Returns a list of ValueTuples of and OrganComponent on each organ /// in the given body. diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs index b58a9cefd2..2d96c14863 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs @@ -1,16 +1,13 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Body.Components; -using Content.Shared.Body.Events; using Content.Shared.Body.Organ; using Content.Shared.Body.Part; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; using Content.Shared.Movement.Components; -using Content.Shared.Random.Helpers; using Robust.Shared.Containers; -using Robust.Shared.GameStates; -using Robust.Shared.Map; +using Robust.Shared.Utility; namespace Content.Shared.Body.Systems; @@ -18,365 +15,638 @@ public partial class SharedBodySystem { private void InitializeParts() { - SubscribeLocalEvent(OnPartRemoved); - SubscribeLocalEvent(OnPartGetState); - SubscribeLocalEvent(OnPartHandleState); + // TODO: This doesn't handle comp removal on child ents. + + // If you modify this also see the Body partial for root parts. + SubscribeLocalEvent(OnBodyPartInserted); + SubscribeLocalEvent(OnBodyPartRemoved); } - private void OnPartGetState(EntityUid uid, BodyPartComponent part, ref ComponentGetState args) + private void OnBodyPartInserted(EntityUid uid, BodyPartComponent component, EntInsertedIntoContainerMessage args) { - args.State = new BodyPartComponentState( - GetNetEntity(part.Body), - part.ParentSlot, - part.Children, - part.Organs, - part.PartType, - part.IsVital, - part.Symmetry - ); + // Body part inserted into another body part. + var entity = args.Entity; + var slotId = args.Container.ID; + + if (component.Body != null) + { + if (TryComp(entity, out BodyPartComponent? childPart)) + { + AddPart(component.Body.Value, entity, slotId, childPart); + RecursiveBodyUpdate(entity, component.Body.Value, childPart); + } + + if (TryComp(entity, out OrganComponent? organ)) + { + AddOrgan(entity, component.Body.Value, uid, organ); + } + } } - private void OnPartHandleState(EntityUid uid, BodyPartComponent part, ref ComponentHandleState args) + private void OnBodyPartRemoved(EntityUid uid, BodyPartComponent component, EntRemovedFromContainerMessage args) { - if (args.Current is not BodyPartComponentState state) + // TODO: lifestage shenanigans + if (LifeStage(uid) >= EntityLifeStage.Terminating) return; - part.Body = EnsureEntity(state.Body, uid); - part.ParentSlot = state.ParentSlot; // TODO use containers. This is broken and does not work. - part.Children = state.Children; // TODO use containers. This is broken and does not work. - part.Organs = state.Organs; // TODO end my suffering. - part.PartType = state.PartType; - part.IsVital = state.IsVital; - part.Symmetry = state.Symmetry; + // Body part removed from another body part. + var entity = args.Entity; + var slotId = args.Container.ID; + + if (TryComp(entity, out BodyPartComponent? childPart) && childPart.Body != null) + { + RemovePart(childPart.Body.Value, entity, slotId, childPart); + RecursiveBodyUpdate(entity, null, childPart); + } + + if (TryComp(entity, out OrganComponent? organ)) + { + RemoveOrgan(entity, organ); + } } - private void OnPartRemoved(EntityUid uid, BodyPartComponent part, ComponentRemove args) + private void RecursiveBodyUpdate(EntityUid uid, EntityUid? bodyUid, BodyPartComponent component) { - if (part.ParentSlot is { } slot) + foreach (var children in GetBodyPartChildren(uid, component)) { - slot.SetChild(null, GetNetEntity(null)); - DirtyAllComponents(slot.Parent); - } + if (children.Component.Body != bodyUid) + { + children.Component.Body = bodyUid; + Dirty(children.Id, children.Component); - foreach (var childSlot in part.Children.Values.ToArray()) - { - DropPart(childSlot.Child); + foreach (var slotId in children.Component.Organs.Keys) + { + var organContainerId = GetOrganContainerId(slotId); + + if (!Containers.TryGetContainer(children.Id, organContainerId, out var container)) + continue; + + foreach (var organ in container.ContainedEntities) + { + if (TryComp(organ, out OrganComponent? organComp)) + { + organComp.Body = bodyUid; + Dirty(organ, organComp); + } + } + } + } } } - private BodyPartSlot? CreatePartSlot( + protected virtual void AddPart( + EntityUid bodyUid, + EntityUid partUid, + string slotId, + BodyPartComponent component, + BodyComponent? bodyComp = null) + { + DebugTools.AssertOwner(partUid, component); + Dirty(partUid, component); + component.Body = bodyUid; + + var ev = new BodyPartAddedEvent(slotId, component); + RaiseLocalEvent(bodyUid, ref ev); + + AddLeg(partUid, bodyUid, component, bodyComp); + } + + protected virtual void RemovePart( + EntityUid bodyUid, + EntityUid partUid, + string slotId, + BodyPartComponent component, + BodyComponent? bodyComp = null) + { + DebugTools.AssertOwner(partUid, component); + Resolve(bodyUid, ref bodyComp, false); + Dirty(partUid, component); + component.Body = null; + + var ev = new BodyPartRemovedEvent(slotId, component); + RaiseLocalEvent(bodyUid, ref ev); + + RemoveLeg(partUid, bodyUid, component); + PartRemoveDamage(bodyUid, component, bodyComp); + } + + private void AddLeg(EntityUid uid, EntityUid bodyUid, BodyPartComponent component, BodyComponent? bodyComp = null) + { + if (!Resolve(bodyUid, ref bodyComp, false)) + return; + + if (component.PartType == BodyPartType.Leg) + { + bodyComp.LegEntities.Add(uid); + UpdateMovementSpeed(bodyUid); + Dirty(bodyUid, bodyComp); + } + } + + private void RemoveLeg(EntityUid uid, EntityUid bodyUid, BodyPartComponent component, BodyComponent? bodyComp = null) + { + if (!Resolve(bodyUid, ref bodyComp, false)) + return; + + if (component.PartType == BodyPartType.Leg) + { + bodyComp.LegEntities.Remove(uid); + UpdateMovementSpeed(bodyUid); + Dirty(bodyUid, bodyComp); + + if (!bodyComp.LegEntities.Any()) + { + Standing.Down(bodyUid); + } + } + } + + private void PartRemoveDamage(EntityUid parent, BodyPartComponent component, BodyComponent? bodyComp = null) + { + if (!Resolve(parent, ref bodyComp, false)) + return; + + if (component.IsVital && !GetBodyChildrenOfType(parent, component.PartType, bodyComp).Any()) + { + // TODO BODY SYSTEM KILL : remove this when wounding and required parts are implemented properly + var damage = new DamageSpecifier(Prototypes.Index("Bloodloss"), 300); + Damageable.TryChangeDamage(parent, damage); + } + } + + /// + /// Tries to get the parent body part to this if applicable. + /// Doesn't validate if it's a part of body system. + /// + public EntityUid? GetParentPartOrNull(EntityUid uid) + { + if (!Containers.TryGetContainingContainer(uid, out var container)) + return null; + + var parent = container.Owner; + + if (!HasComp(parent)) + return null; + + return parent; + } + + /// + /// Tries to get the parent body part and slot to this if applicable. + /// + public (EntityUid Parent, string Slot)? GetParentPartAndSlotOrNull(EntityUid uid) + { + if (!Containers.TryGetContainingContainer(uid, out var container)) + return null; + + var slotId = GetPartSlotContainerIdFromContainer(container.ID); + + if (string.IsNullOrEmpty(slotId)) + return null; + + var parent = container.Owner; + + if (!TryComp(parent, out var parentBody) || !parentBody.Children.ContainsKey(slotId)) + return null; + + return (parent, slotId); + } + + /// + /// Tries to get the relevant parent body part to this if it exists. + /// It won't exist if this is the root body part or if it's not in a body. + /// + public bool TryGetParentBodyPart( + EntityUid partUid, + [NotNullWhen(true)] out EntityUid? parentUid, + [NotNullWhen(true)] out BodyPartComponent? parentComponent) + { + DebugTools.Assert(HasComp(partUid)); + parentUid = null; + parentComponent = null; + + if (Containers.TryGetContainingContainer(partUid, out var container) && + TryComp(container.Owner, out parentComponent)) + { + parentUid = container.Owner; + return true; + } + + return false; + } + + #region Slots + + /// + /// Creates a BodyPartSlot on the specified partUid. + /// + private BodyPartSlot? CreatePartSlot( + EntityUid partUid, string slotId, - EntityUid parent, BodyPartType partType, BodyPartComponent? part = null) { - if (!Resolve(parent, ref part, false)) + if (!Resolve(partUid, ref part, false)) return null; - var slot = new BodyPartSlot - { - Id = slotId, - Type = partType, - Parent = parent, - NetParent = GetNetEntity(parent), - }; - part.Children.Add(slotId, slot); - - return slot; + Containers.EnsureContainer(partUid, GetPartSlotContainerId(slotId)); + var partSlot = new BodyPartSlot(slotId, partType); + part.Children.Add(slotId, partSlot); + Dirty(partUid, part); + return partSlot; } + /// + /// Tries to create a BodyPartSlot on the specified partUid. + /// + /// false if not relevant or can't add it. public bool TryCreatePartSlot( - EntityUid? parentId, - string id, + EntityUid? partId, + string slotId, + BodyPartType partType, [NotNullWhen(true)] out BodyPartSlot? slot, - BodyPartComponent? parent = null) + BodyPartComponent? part = null) { slot = null; - if (parentId == null || - !Resolve(parentId.Value, ref parent, false)) - return false; - - slot = new BodyPartSlot + if (partId == null || + !Resolve(partId.Value, ref part, false)) { - Id = id, - Parent = parentId.Value, - NetParent = GetNetEntity(parentId.Value), - }; - if (!parent.Children.TryAdd(id, slot)) - { - slot = null; return false; } + Containers.EnsureContainer(partId.Value, GetPartSlotContainerId(slotId)); + slot = new BodyPartSlot(slotId, partType); + + if (!part.Children.TryAdd(slotId, slot.Value)) + return false; + + Dirty(partId.Value, part); return true; } public bool TryCreatePartSlotAndAttach( - EntityUid? parentId, - string id, - EntityUid? childId, + EntityUid parentId, + string slotId, + EntityUid childId, + BodyPartType partType, BodyPartComponent? parent = null, BodyPartComponent? child = null) { - return TryCreatePartSlot(parentId, id, out var slot, parent) && AttachPart(childId, slot, child); + return TryCreatePartSlot(parentId, slotId, partType, out _, parent) + && AttachPart(parentId, slotId, childId, parent, child); } - public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetPartChildren(EntityUid? id, BodyPartComponent? part = null) + #endregion + + #region RootPartManagement + + /// + /// Returns true if the partId is the root body container for the specified bodyId. + /// + public bool IsPartRoot(EntityUid bodyId, EntityUid partId, BodyComponent? body = null, BodyPartComponent? part = null) { - if (id == null || !Resolve(id.Value, ref part, false)) - yield break; + if (!Resolve(partId, ref part)|| !Resolve(bodyId, ref body)) + return false; - foreach (var slot in part.Children.Values) - { - if (!TryComp(slot.Child, out BodyPartComponent? childPart)) - continue; - - yield return (slot.Child.Value, childPart); - - foreach (var subChild in GetPartChildren(slot.Child, childPart)) - { - yield return subChild; - } - } + return Containers.TryGetContainingContainer(bodyId, partId, out var container) && container.ID == BodyRootContainerId; } - public IEnumerable<(EntityUid Id, OrganComponent Component)> GetPartOrgans(EntityUid? partId, BodyPartComponent? part = null) + /// + /// Returns true if we can attach the partId to the bodyId as the root entity. + /// + public bool CanAttachToRoot(EntityUid bodyId, EntityUid partId, BodyComponent? body = null, + BodyPartComponent? part = null) { - if (partId == null || !Resolve(partId.Value, ref part, false)) - yield break; - - foreach (var slot in part.Organs.Values) - { - if (!TryComp(slot.Child, out OrganComponent? organ)) - continue; - - yield return (slot.Child.Value, organ); - } + return Resolve(bodyId, ref body) && + Resolve(partId, ref part) && + body.RootContainer.ContainedEntity == null && + bodyId != part.Body; } - public IEnumerable GetPartAllSlots(EntityUid? partId, BodyPartComponent? part = null) + /// + /// Returns the root part of this body if it exists. + /// + public (EntityUid Entity, BodyPartComponent BodyPart)? GetRootPartOrNull(EntityUid bodyId, BodyComponent? body = null) { - if (partId == null || - !Resolve(partId.Value, ref part, false)) - yield break; + if (!Resolve(bodyId, ref body) || body.RootContainer.ContainedEntity == null) + return null; - foreach (var slot in part.Children.Values) - { - yield return slot; - - if (!TryComp(slot.Child, out BodyComponent? childPart)) - continue; - - foreach (var subChild in GetBodyAllSlots(slot.Child, childPart)) - { - yield return subChild; - } - } + return (body.RootContainer.ContainedEntity.Value, + Comp(body.RootContainer.ContainedEntity.Value)); } - public bool CanAttachPart([NotNullWhen(true)] EntityUid? partId, BodyPartSlot slot, BodyPartComponent? part = null) - { - return partId != null && - slot.Child == null && - Resolve(partId.Value, ref part, false) && - (slot.Type == null || slot.Type == part.PartType) && - Containers.TryGetContainer(slot.Parent, BodyContainerId, out var container) && - _container.CanInsert(partId.Value, container); - } - - public virtual bool AttachPart( - EntityUid? partId, + /// + /// Returns true if the partId can be attached to the parentId in the specified slot. + /// + public bool CanAttachPart( + EntityUid parentId, BodyPartSlot slot, - [NotNullWhen(true)] BodyPartComponent? part = null) + EntityUid partId, + BodyPartComponent? parentPart = null, + BodyPartComponent? part = null) { - if (partId == null || - !Resolve(partId.Value, ref part, false) || - !CanAttachPart(partId, slot, part)) + if (!Resolve(partId, ref part, false) || + !Resolve(parentId, ref parentPart, false)) + { return false; - - DropPart(slot.Child); - DropPart(partId, part); - - var container = Containers.EnsureContainer(slot.Parent, BodyContainerId); - if (!container.Insert(partId.Value)) - return false; - - slot.SetChild(partId, GetNetEntity(partId)); - part.ParentSlot = slot; - - if (TryComp(slot.Parent, out BodyPartComponent? parentPart)) - { - part.Body = parentPart.Body; - } - else if (TryComp(slot.Parent, out BodyComponent? parentBody)) - { - part.Body = parentBody.Owner; - } - else - { - part.Body = null; } - DirtyAllComponents(slot.Parent); - DirtyAllComponents(partId.Value); - - if (part.Body is { } newBody) - { - if (part.PartType == BodyPartType.Leg) - UpdateMovementSpeed(newBody); - - var partAddedEvent = new BodyPartAddedEvent(slot.Id, part); - RaiseLocalEvent(newBody, ref partAddedEvent); - - // TODO: Body refactor. Somebody is doing it - // EntitySystem.Get().BodyPartAdded(Owner, argsAdded); - - foreach (var organ in GetPartOrgans(partId, part)) - { - RaiseLocalEvent(organ.Id, new AddedToBodyEvent(newBody), true); - } - - DirtyAllComponents(newBody); - } - - return true; + return CanAttachPart(parentId, slot.Id, partId, parentPart, part); } - public virtual bool DropPart(EntityUid? partId, BodyPartComponent? part = null) + /// + /// Returns true if we can attach the specified partId to the parentId in the specified slot. + /// + public bool CanAttachPart( + EntityUid parentId, + string slotId, + EntityUid partId, + BodyPartComponent? parentPart = null, + BodyPartComponent? part = null) { - if (partId == null || - !Resolve(partId.Value, ref part, false) || - part.ParentSlot is not { } slot) - return false; - - var oldBodyNullable = part.Body; - - slot.SetChild(null, null); - part.ParentSlot = null; - part.Body = null; - - if (Containers.TryGetContainer(slot.Parent, BodyContainerId, out var container)) - container.Remove(partId.Value); - - if (TryComp(partId, out TransformComponent? transform)) - transform.AttachToGridOrMap(); - - part.Owner.RandomOffset(0.25f); - - if (oldBodyNullable is { } oldBody) + if (!Resolve(partId, ref part, false) || + !Resolve(parentId, ref parentPart, false) || + !parentPart.Children.TryGetValue(slotId, out var parentSlotData)) { - var args = new BodyPartRemovedEvent(slot.Id, part); - RaiseLocalEvent(oldBody, ref args); - - if (part.PartType == BodyPartType.Leg) - { - UpdateMovementSpeed(oldBody); - if(!GetBodyChildrenOfType(oldBody, BodyPartType.Leg).Any()) - Standing.Down(oldBody); - } - - if (part.IsVital && !GetBodyChildrenOfType(oldBody, part.PartType).Any()) - { - // TODO BODY SYSTEM KILL : Find a more elegant way of killing em than just dumping bloodloss damage. - var damage = new DamageSpecifier(Prototypes.Index("Bloodloss"), 300); - Damageable.TryChangeDamage(part.Owner, damage); - } - - foreach (var organSlot in part.Organs.Values) - { - if (organSlot.Child is not { } child) - continue; - - RaiseLocalEvent(child, new RemovedFromBodyEvent(oldBody), true); - } + return false; } - DirtyAllComponents(slot.Parent); - DirtyAllComponents(partId.Value); - - return true; + return part.PartType == parentSlotData.Type && + Containers.TryGetContainer(parentId, GetPartSlotContainerId(slotId), out var container) && + Containers.CanInsert(partId, container); } - public void UpdateMovementSpeed(EntityUid body, BodyComponent? component = null, MovementSpeedModifierComponent? movement = null) + public bool AttachPartToRoot( + EntityUid bodyId, + EntityUid partId, + BodyComponent? body = null, + BodyPartComponent? part = null) { - if (!Resolve(body, ref component, ref movement, false)) - return; - - if (component.RequiredLegs <= 0) - return; - - if (component.Root?.Child is not { } root) - return; - - var allSlots = GetAllBodyPartSlots(root).ToHashSet(); - var allLegs = new HashSet(); - foreach (var slot in allSlots) + if (!Resolve(bodyId, ref body) || + !Resolve(partId, ref part) || + !CanAttachToRoot(bodyId, partId, body, part)) { - if (slot.Type == BodyPartType.Leg && slot.Child is { } child) - allLegs.Add(child); + return false; } + return body.RootContainer.Insert(partId); + } + + #endregion + + #region Attach/Detach + + /// + /// Attaches a body part to the specified body part parent. + /// + public bool AttachPart( + EntityUid parentPartId, + string slotId, + EntityUid partId, + BodyPartComponent? parentPart = null, + BodyPartComponent? part = null) + { + if (!Resolve(parentPartId, ref parentPart, false) || + !parentPart.Children.TryGetValue(slotId, out var slot)) + { + return false; + } + + return AttachPart(parentPartId, slot, partId, parentPart, part); + } + + /// + /// Attaches a body part to the specified body part parent. + /// + public bool AttachPart( + EntityUid parentPartId, + BodyPartSlot slot, + EntityUid partId, + BodyPartComponent? parentPart = null, + BodyPartComponent? part = null) + { + if (!Resolve(parentPartId, ref parentPart, false) || + !Resolve(partId, ref part, false) || + !CanAttachPart(parentPartId, slot.Id, partId, parentPart, part) || + !parentPart.Children.ContainsKey(slot.Id)) + { + return false; + } + + if (!Containers.TryGetContainer(parentPartId, GetPartSlotContainerId(slot.Id), out var container)) + { + DebugTools.Assert($"Unable to find body slot {slot.Id} for {ToPrettyString(parentPartId)}"); + return false; + } + + return container.Insert(partId); + } + + #endregion + + #region Misc + + public void UpdateMovementSpeed(EntityUid bodyId, BodyComponent? body = null, MovementSpeedModifierComponent? movement = null) + { + if (!Resolve(bodyId, ref body, ref movement, false)) + return; + + if (body.RequiredLegs <= 0) + return; + var walkSpeed = 0f; var sprintSpeed = 0f; var acceleration = 0f; - foreach (var leg in allLegs) + foreach (var legEntity in body.LegEntities) { - if (!TryComp(leg, out var legModifier)) + if (!TryComp(legEntity, out var legModifier)) continue; walkSpeed += legModifier.WalkSpeed; sprintSpeed += legModifier.SprintSpeed; acceleration += legModifier.Acceleration; } - - walkSpeed /= component.RequiredLegs; - sprintSpeed /= component.RequiredLegs; - acceleration /= component.RequiredLegs; - Movement.ChangeBaseSpeed(body, walkSpeed, sprintSpeed, acceleration, movement); + walkSpeed /= body.RequiredLegs; + sprintSpeed /= body.RequiredLegs; + acceleration /= body.RequiredLegs; + Movement.ChangeBaseSpeed(bodyId, walkSpeed, sprintSpeed, acceleration, movement); } - public bool DropPartAt(EntityUid? partId, EntityCoordinates dropAt, BodyPartComponent? part = null) + #endregion + + #region Queries + + /// + /// Get all organs for the specified body part. + /// + public IEnumerable<(EntityUid Id, OrganComponent Component)> GetPartOrgans(EntityUid partId, BodyPartComponent? part = null) { - if (partId == null || !DropPart(partId, part)) - return false; + if (!Resolve(partId, ref part, false)) + yield break; - if (TryComp(partId.Value, out TransformComponent? transform)) - transform.Coordinates = dropAt; - - return true; - } - - public bool OrphanPart(EntityUid? partId, BodyPartComponent? part = null) - { - if (partId == null || !Resolve(partId.Value, ref part, false)) - return false; - - DropPart(partId, part); - - foreach (var slot in part.Children.Values) + foreach (var slotId in part.Organs.Keys) { - DropPart(slot.Child); + var containerSlotId = GetOrganContainerId(slotId); + + if (!Containers.TryGetContainer(partId, containerSlotId, out var container)) + continue; + + foreach (var containedEnt in container.ContainedEntities) + { + if (!TryComp(containedEnt, out OrganComponent? organ)) + continue; + + yield return (containedEnt, organ); + } + } + } + + /// + /// Gets all BaseContainers for body parts on this entity and its child entities. + /// + public IEnumerable GetPartContainers(EntityUid id, BodyPartComponent? part = null) + { + if (!Resolve(id, ref part, false) || + part.Children.Count == 0) + { + yield break; } + foreach (var slotId in part.Children.Keys) + { + var containerSlotId = GetPartSlotContainerId(slotId); + + if (!Containers.TryGetContainer(id, containerSlotId, out var container)) + continue; + + yield return container; + + foreach (var ent in container.ContainedEntities) + { + foreach (var childContainer in GetPartContainers(ent)) + { + yield return childContainer; + } + } + } + } + + /// + /// Returns all body part components for this entity including itself. + /// + public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyPartChildren(EntityUid partId, BodyPartComponent? part = null) + { + if (!Resolve(partId, ref part, false)) + yield break; + + yield return (partId, part); + + foreach (var slotId in part.Children.Keys) + { + var containerSlotId = GetPartSlotContainerId(slotId); + + if (Containers.TryGetContainer(partId, containerSlotId, out var container)) + { + foreach (var containedEnt in container.ContainedEntities) + { + if (!TryComp(containedEnt, out BodyPartComponent? childPart)) + continue; + + foreach (var value in GetBodyPartChildren(containedEnt, childPart)) + { + yield return value; + } + } + } + } + } + + /// + /// Returns all body part slots for this entity. + /// + public IEnumerable GetAllBodyPartSlots(EntityUid partId, BodyPartComponent? part = null) + { + if (!Resolve(partId, ref part, false)) + yield break; + + foreach (var (slotId, slot) in part.Children) + { + yield return slot; + + var containerSlotId = GetOrganContainerId(slotId); + + if (Containers.TryGetContainer(partId, containerSlotId, out var container)) + { + foreach (var containedEnt in container.ContainedEntities) + { + if (!TryComp(containedEnt, out BodyPartComponent? childPart)) + continue; + + foreach (var subSlot in GetAllBodyPartSlots(containedEnt, childPart)) + { + yield return subSlot; + } + } + } + } + } + + /// + /// Returns true if the bodyId has any parts of this type. + /// + public bool BodyHasPartType(EntityUid bodyId, BodyPartType type, BodyComponent? body = null) + { + return GetBodyChildrenOfType(bodyId, type, body).Any(); + } + + /// + /// Returns true if the parentId has the specified childId. + /// + public bool PartHasChild( + EntityUid parentId, + EntityUid childId, + BodyPartComponent? parent, + BodyPartComponent? child) + { + if (!Resolve(parentId, ref parent, false) || + !Resolve(childId, ref child, false)) + { + return false; + } + + foreach (var (foundId, _) in GetBodyPartChildren(parentId, parent)) + { + if (foundId == childId) + return true; + } return false; } - public bool DeletePart(EntityUid? id, BodyPartComponent? part = null) + /// + /// Returns true if the bodyId has the specified partId. + /// + public bool BodyHasChild( + EntityUid bodyId, + EntityUid partId, + BodyComponent? body = null, + BodyPartComponent? part = null) { - if (id == null || !Resolve(id.Value, ref part, false)) + if (!Resolve(bodyId, ref body, false) || + body.RootContainer.ContainedEntity == null || + !Resolve(partId, ref part, false) || + !TryComp(body.RootContainer.ContainedEntity, out BodyPartComponent? rootPart)) + { return false; + } - DropPart(id, part); - - if (Deleted(id.Value)) - return false; - - Del(id.Value); - return true; + return PartHasChild(body.RootContainer.ContainedEntity.Value, partId, rootPart, part); } - public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyChildrenOfType(EntityUid? bodyId, BodyPartType type, BodyComponent? body = null) + public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyChildrenOfType( + EntityUid bodyId, + BodyPartType type, + BodyComponent? body = null) { foreach (var part in GetBodyChildren(bodyId, body)) { @@ -385,84 +655,6 @@ public partial class SharedBodySystem } } - public bool BodyHasChildOfType(EntityUid? bodyId, BodyPartType type, BodyComponent? body = null) - { - return GetBodyChildrenOfType(bodyId, type, body).Any(); - } - - public bool BodyHasChild( - EntityUid? parentId, - EntityUid? childId, - BodyComponent? parent = null, - BodyPartComponent? child = null) - { - if (parentId == null || - !Resolve(parentId.Value, ref parent, false) || - childId == null || - !Resolve(childId.Value, ref child, false)) - return false; - - return child.ParentSlot?.Child == parentId; - } - - public IEnumerable GetBodyPartAdjacentParts(EntityUid partId, BodyPartComponent? part = null) - { - if (!Resolve(partId, ref part, false)) - yield break; - - if (part.ParentSlot != null) - yield return part.ParentSlot.Parent; - - foreach (var slot in part.Children.Values) - { - if (slot.Child != null) - yield return slot.Child.Value; - } - } - - public IEnumerable<(EntityUid AdjacentId, T Component)> GetBodyPartAdjacentPartsComponents( - EntityUid partId, - BodyPartComponent? part = null) - where T : Component - { - if (!Resolve(partId, ref part, false)) - yield break; - - var query = GetEntityQuery(); - foreach (var adjacentId in GetBodyPartAdjacentParts(partId, part)) - { - if (query.TryGetComponent(adjacentId, out var component)) - yield return (adjacentId, component); - } - } - - public bool TryGetBodyPartAdjacentPartsComponents( - EntityUid partId, - [NotNullWhen(true)] out List<(EntityUid AdjacentId, T Component)>? comps, - BodyPartComponent? part = null) - where T : Component - { - if (!Resolve(partId, ref part, false)) - { - comps = null; - return false; - } - - var query = GetEntityQuery(); - comps = new List<(EntityUid AdjacentId, T Component)>(); - foreach (var adjacentId in GetBodyPartAdjacentParts(partId, part)) - { - if (query.TryGetComponent(adjacentId, out var component)) - comps.Add((adjacentId, component)); - } - - if (comps.Count != 0) - return true; - - comps = null; - return false; - } - /// /// Returns a list of ValueTuples of and OrganComponent on each organ /// in the given part. @@ -480,6 +672,7 @@ public partial class SharedBodySystem var query = GetEntityQuery(); var list = new List<(T Comp, OrganComponent Organ)>(); + foreach (var organ in GetPartOrgans(uid, part)) { if (query.TryGetComponent(organ.Id, out var comp)) @@ -518,4 +711,72 @@ public partial class SharedBodySystem comps = null; return false; } + + /// + /// Gets the parent body part and all immediate child body parts for the partId. + /// + public IEnumerable GetBodyPartAdjacentParts(EntityUid partId, BodyPartComponent? part = null) + { + if (!Resolve(partId, ref part, false)) + yield break; + + if (TryGetParentBodyPart(partId, out var parentUid, out _)) + yield return parentUid.Value; + + foreach (var slotId in part.Children.Keys) + { + var container = Containers.GetContainer(partId, GetPartSlotContainerId(slotId)); + + foreach (var containedEnt in container.ContainedEntities) + { + yield return containedEnt; + } + } + } + + public IEnumerable<(EntityUid AdjacentId, T Component)> GetBodyPartAdjacentPartsComponents( + EntityUid partId, + BodyPartComponent? part = null) + where T : Component + { + if (!Resolve(partId, ref part, false)) + yield break; + + var query = GetEntityQuery(); + foreach (var adjacentId in GetBodyPartAdjacentParts(partId, part)) + { + if (query.TryGetComponent(adjacentId, out var component)) + yield return (adjacentId, component); + } + } + + public bool TryGetBodyPartAdjacentPartsComponents( + EntityUid partId, + [NotNullWhen(true)] out List<(EntityUid AdjacentId, T Component)>? comps, + BodyPartComponent? part = null) + where T : Component + { + if (!Resolve(partId, ref part, false)) + { + comps = null; + return false; + } + + var query = GetEntityQuery(); + comps = new List<(EntityUid AdjacentId, T Component)>(); + + foreach (var adjacentId in GetBodyPartAdjacentParts(partId, part)) + { + if (query.TryGetComponent(adjacentId, out var component)) + comps.Add((adjacentId, component)); + } + + if (comps.Count != 0) + return true; + + comps = null; + return false; + } + + #endregion } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.cs b/Content.Shared/Body/Systems/SharedBodySystem.cs index 4d195e0a50..1cc891f05d 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.cs @@ -8,14 +8,31 @@ namespace Content.Shared.Body.Systems; public abstract partial class SharedBodySystem : EntitySystem { - protected const string BodyContainerId = "BodyContainer"; + /* + * See the body partial for how this works. + */ + + /// + /// Container ID prefix for any body parts. + /// + protected const string PartSlotContainerIdPrefix = "body_part_slot_"; + + /// + /// Container ID for the ContainerSlot on the body entity itself. + /// + protected const string BodyRootContainerId = "body_root_part"; + + /// + /// Container ID prefix for any body organs. + /// + protected const string OrganSlotContainerIdPrefix = "body_organ_slot_"; [Dependency] protected readonly IPrototypeManager Prototypes = default!; - - [Dependency] protected readonly SharedContainerSystem Containers = default!; [Dependency] protected readonly DamageableSystem Damageable = default!; - [Dependency] protected readonly StandingStateSystem Standing = default!; [Dependency] protected readonly MovementSpeedModifierSystem Movement = default!; + [Dependency] protected readonly SharedContainerSystem Containers = default!; + [Dependency] protected readonly SharedTransformSystem SharedTransform = default!; + [Dependency] protected readonly StandingStateSystem Standing = default!; public override void Initialize() { @@ -23,6 +40,36 @@ public abstract partial class SharedBodySystem : EntitySystem InitializeBody(); InitializeParts(); - InitializeOrgans(); + } + + /// + /// Inverse of + /// + protected static string? GetPartSlotContainerIdFromContainer(string containerSlotId) + { + // This is blursed + var slotIndex = containerSlotId.IndexOf(PartSlotContainerIdPrefix, StringComparison.Ordinal); + + if (slotIndex < -1) + return null; + + var slotId = containerSlotId.Remove(slotIndex, PartSlotContainerIdPrefix.Length); + return slotId; + } + + /// + /// Gets the container Id for the specified slotId. + /// + public static string GetPartSlotContainerId(string slotId) + { + return PartSlotContainerIdPrefix + slotId; + } + + /// + /// Gets the container Id for the specified slotId. + /// + public static string GetOrganContainerId(string slotId) + { + return OrganSlotContainerIdPrefix + slotId; } } diff --git a/Resources/Prototypes/Body/Prototypes/a_ghost.yml b/Resources/Prototypes/Body/Prototypes/a_ghost.yml index 3a8a615ffe..0a2c585f96 100644 --- a/Resources/Prototypes/Body/Prototypes/a_ghost.yml +++ b/Resources/Prototypes/Body/Prototypes/a_ghost.yml @@ -6,17 +6,17 @@ torso: part: TorsoHuman connections: - - left arm - - right arm - right arm: + - left_arm + - right_arm + right_arm: part: RightArmHuman connections: - - right hand - left arm: + - right_hand + left_arm: part: LeftArmHuman connections: - - left hand - right hand: + - left_hand + right_hand: part: RightHandHuman - left hand: + left_hand: part: LeftHandHuman diff --git a/Resources/Prototypes/Body/Prototypes/human.yml b/Resources/Prototypes/Body/Prototypes/human.yml index 587ccffe80..76d46abcc5 100644 --- a/Resources/Prototypes/Body/Prototypes/human.yml +++ b/Resources/Prototypes/Body/Prototypes/human.yml @@ -13,37 +13,37 @@ torso: part: TorsoHuman connections: - - left arm - - right arm - - left leg - - right leg + - left_arm + - right_arm + - left_leg + - right_leg organs: heart: OrganHumanHeart lungs: OrganHumanLungs stomach: OrganHumanStomach liver: OrganHumanLiver kidneys: OrganHumanKidneys - right arm: + right_arm: part: RightArmHuman connections: - - right hand - left arm: + - right_hand + left_arm: part: LeftArmHuman connections: - - left hand - right hand: + - left_hand + right_hand: part: RightHandHuman - left hand: + left_hand: part: LeftHandHuman - right leg: + right_leg: part: RightLegHuman connections: - - right foot - left leg: + - right_foot + left_leg: part: LeftLegHuman connections: - - left foot - right foot: + - left_foot + right_foot: part: RightFootHuman - left foot: + left_foot: part: LeftFootHuman diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_livestock.yml b/Resources/Prototypes/Catalog/Cargo/cargo_livestock.yml index b23797d590..d82e4d8d26 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_livestock.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_livestock.yml @@ -4,7 +4,7 @@ sprite: Mobs/Animals/bee.rsi state: 0 product: CrateNPCBee - cost: 4200 + cost: 7000 category: Livestock group: market @@ -14,7 +14,7 @@ sprite: Mobs/Animals/butterfly.rsi state: butterfly product: CrateNPCButterflies - cost: 2700 + cost: 4400 category: Livestock group: market @@ -24,7 +24,7 @@ sprite: Mobs/Pets/cat.rsi state: cat product: CrateNPCCat - cost: 1000 + cost: 1200 category: Livestock group: market @@ -34,7 +34,7 @@ sprite: Mobs/Animals/chicken.rsi state: icon-1 product: CrateNPCChicken - cost: 2600 + cost: 4000 category: Livestock group: market @@ -44,7 +44,7 @@ sprite: Mobs/Animals/crab.rsi state: crab product: CrateNPCCrab - cost: 2000 + cost: 3000 category: Livestock group: market @@ -54,7 +54,7 @@ sprite: Mobs/Animals/duck.rsi state: icon-0 product: CrateNPCDuck - cost: 3800 + cost: 6000 category: Livestock group: market @@ -64,7 +64,7 @@ sprite: Mobs/Pets/corgi.rsi state: corgi product: CrateNPCCorgi - cost: 1000 + cost: 1200 category: Livestock group: market @@ -84,7 +84,7 @@ sprite: Mobs/Animals/goat.rsi state: goat product: CrateNPCGoat - cost: 1000 + cost: 1200 category: Livestock group: market @@ -94,7 +94,7 @@ sprite: Mobs/Animals/goose.rsi state: goose product: CrateNPCGoose - cost: 1500 + cost: 2100 category: Livestock group: market @@ -104,7 +104,7 @@ sprite: Mobs/Animals/gorilla.rsi state: icon product: CrateNPCGorilla - cost: 1000 + cost: 1100 category: Livestock group: market @@ -124,7 +124,7 @@ sprite: Mobs/Animals/mouse.rsi state: icon-0 product: CrateNPCMouse - cost: 2700 + cost: 4400 category: Livestock group: market @@ -134,7 +134,7 @@ sprite: Mobs/Animals/parrot.rsi state: parrot product: CrateNPCParrot - cost: 2000 + cost: 3000 category: Livestock group: market @@ -144,7 +144,7 @@ sprite: Mobs/Animals/penguin.rsi state: penguin product: CrateNPCPenguin - cost: 1500 + cost: 2100 category: Livestock group: market @@ -154,7 +154,7 @@ sprite: Mobs/Animals/pig.rsi state: pig product: CrateNPCPig - cost: 1000 + cost: 1100 category: Livestock group: market @@ -164,7 +164,7 @@ sprite: Mobs/Animals/snake.rsi state: snake product: CrateNPCSnake - cost: 2000 + cost: 3000 category: Livestock group: market @@ -184,7 +184,7 @@ sprite: Mobs/Animals/lizard.rsi state: lizard product: CrateNPCLizard - cost: 1000 + cost: 1100 category: Livestock group: market