Organ relations (#44812)

* Organ relations

* address reviews
This commit is contained in:
pathetic meowmeow 2026-07-26 05:20:50 -04:00 • committed by GitHub
parent edfe6ed580
commit 63c90e4d4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1004 additions and 22 deletions

View file

@ -0,0 +1,111 @@
using System.Linq;
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.Shared.Body;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Body;
[TestFixture]
[TestOf(typeof(DetachableOrganSystem))]
public sealed class DetachableOrganSystemTest : GameTest
{
private const string DetachTestOldBody = "DetachTestOldBody";
private const string DetachTestNewBody = "DetachTestNewBody";
private const string DetachTestGrandParentOrgan = "DetachTestGrandParentOrgan";
private const string DetachTestRootOrgan = "DetachTestRootOrgan";
private const string DetachTestChildOrgan = "DetachTestChildOrgan";
private const string DetachTestSiblingOrgan = "DetachTestSiblingOrgan";
[TestPrototypes]
private const string Prototypes = $@"
- type: entity
id: {DetachTestOldBody}
components:
- type: Body
- type: entity
id: {DetachTestNewBody}
components:
- type: Body
- type: entity
id: {DetachTestGrandParentOrgan}
components:
- type: Organ
- type: ParentOrgan
- type: ChildOrgan
- type: entity
id: {DetachTestRootOrgan}
components:
- type: Organ
- type: ParentOrgan
- type: ChildOrgan
- type: DetachableOrgan
detachedBody: DetachTestNewBody
- type: entity
id: {DetachTestChildOrgan}
components:
- type: Organ
- type: ChildOrgan
- type: entity
id: {DetachTestSiblingOrgan}
components:
- type: Organ
- type: ChildOrgan
";
[SidedDependency(Side.Server)] private SharedContainerSystem _container = default!;
[SidedDependency(Side.Server)] private OrganRelationSystem _organRelation = default!;
[SidedDependency(Side.Server)] private DetachableOrganSystem _detachableOrgan = default!;
[Test]
[RunOnSide(Side.Server)]
public void Detach()
{
var oldBody = SSpawn(DetachTestOldBody);
var oldBodyContainer = _container.GetContainer(oldBody, BodyComponent.ContainerID);
var grandParent = SSpawn(DetachTestGrandParentOrgan);
var root = SSpawn(DetachTestRootOrgan);
var child = SSpawn(DetachTestChildOrgan);
var sibling = SSpawn(DetachTestSiblingOrgan);
_container.Insert(grandParent, oldBodyContainer, force: true);
_container.Insert(root, oldBodyContainer, force: true);
_container.Insert(child, oldBodyContainer, force: true);
_container.Insert(sibling, oldBodyContainer, force: true);
_organRelation.Relate(grandParent, root);
_organRelation.Relate(grandParent, sibling);
_organRelation.Relate(root, child);
var newBody = _detachableOrgan.Detach(root);
Assert.That(newBody, Is.Not.Null);
var newBodyContained = _container.GetContainer(newBody!.Value, BodyComponent.ContainerID).ContainedEntities.ToList();
Assert.That(newBodyContained, Is.EquivalentTo(new[] { root, child }));
var oldBodyContained = oldBodyContainer.ContainedEntities.ToList();
Assert.That(oldBodyContained, Is.EquivalentTo(new[] { grandParent, sibling }));
var grandParentComp = SComp<ParentOrganComponent>(grandParent);
Assert.That(grandParentComp.Children, Does.Not.Contain(root));
Assert.That(grandParentComp.Children, Does.Contain(sibling));
Assert.That(SComp<ChildOrganComponent>(root).Parent, Is.Null);
Assert.That(SComp<ParentOrganComponent>(root).Children, Does.Contain(child));
Assert.That(SComp<ChildOrganComponent>(child).Parent, Is.EqualTo(root));
Assert.That(SComp<OrganComponent>(root).Body, Is.EqualTo(newBody));
Assert.That(SComp<OrganComponent>(child).Body, Is.EqualTo(newBody));
Assert.That(SComp<OrganComponent>(grandParent).Body, Is.EqualTo(oldBody));
Assert.That(SComp<OrganComponent>(sibling).Body, Is.EqualTo(oldBody));
}
}

View file

@ -0,0 +1,67 @@
using System.Linq;
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.Shared.Body;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests.Body;
[TestFixture]
[TestOf(typeof(InitialBodySystem))]
public sealed class InitialBodySpawnTest : GameTest
{
[TestPrototypes]
private const string Prototypes = @"
- type: organCategory
id: InitialBodySpawnTestParent
- type: organCategory
id: InitialBodySpawnTestChild
- type: entity
id: InitialBodySpawnTestBody
components:
- type: Body
- type: InitialBody
organs:
InitialBodySpawnTestParent: InitialBodySpawnTestParentOrgan
InitialBodySpawnTestChild: InitialBodySpawnTestChildOrgan
relationships:
InitialBodySpawnTestParent: [InitialBodySpawnTestChild]
- type: entity
id: InitialBodySpawnTestParentOrgan
components:
- type: Organ
- type: ParentOrgan
- type: entity
id: InitialBodySpawnTestChildOrgan
components:
- type: Organ
- type: ChildOrgan
";
[SidedDependency(Side.Server)] private SharedContainerSystem _container = default!;
[Test]
[RunOnSide(Side.Server)]
public void SpawningWiresUpOrganRelations()
{
var body = SSpawn("InitialBodySpawnTestBody");
var bodyContainer = _container.GetContainer(body, BodyComponent.ContainerID);
var contained = bodyContainer.ContainedEntities.ToList();
Assert.That(contained, Has.Count.EqualTo(2));
var parent = contained.Single(SEntMan.HasComponent<ParentOrganComponent>);
var child = contained.Single(SEntMan.HasComponent<ChildOrganComponent>);
var parentComp = SComp<ParentOrganComponent>(parent);
var childComp = SComp<ChildOrganComponent>(child);
Assert.That(parentComp.Children, Does.Contain(child));
Assert.That(childComp.Parent, Is.EqualTo(parent));
}
}

View file

@ -0,0 +1,179 @@
using System.Collections.Generic;
using System.Linq;
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.Shared.Body;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Body;
[TestFixture]
[TestOf(typeof(InitialBodySystem))]
public sealed class InitialBodyValidationTest : GameTest
{
[SidedDependency(Side.Server)] private IComponentFactory _componentFactory = default!;
[SidedDependency(Side.Server)] private IPrototypeManager _prototype = default!;
[Test]
[RunOnSide(Side.Server)]
public void RelatedOrgansCanBeRelated()
{
using var scope = Assert.EnterMultipleScope();
foreach (var proto in _prototype.EnumeratePrototypes<EntityPrototype>())
{
if (proto.Abstract || Pair.IsTestEntityPrototype(proto.ID))
continue;
if (!proto.TryComp<InitialBodyComponent>(out var initial, _componentFactory))
continue;
if (initial.Relationships is not { } relationships)
continue;
var organs = initial.Organs;
foreach (var (parent, children) in relationships)
{
if (!organs.TryGetValue(parent, out var parentProtoId))
continue;
var parentProto = _prototype.Index(parentProtoId);
if (!parentProto.HasComp<ParentOrganComponent>(_componentFactory))
{
Assert.Fail($"{proto.ID}'s parent organ {parent} {parentProtoId} is missing {nameof(ParentOrganComponent)}");
}
foreach (var child in children)
{
if (!organs.TryGetValue(child, out var childProtoId))
continue;
var childProto = _prototype.Index(childProtoId);
if (!childProto.HasComp<ChildOrganComponent>(_componentFactory))
{
Assert.Fail($"{proto.ID}'s child organ {child} {childProtoId} (child of {parent} {parentProtoId}) is missing {nameof(ChildOrganComponent)}");
}
}
}
}
}
[Test]
[RunOnSide(Side.Server)]
public void NoMultipleParents()
{
using var scope = Assert.EnterMultipleScope();
foreach (var proto in _prototype.EnumeratePrototypes<EntityPrototype>())
{
if (proto.Abstract || Pair.IsTestEntityPrototype(proto.ID))
continue;
if (!proto.TryComp<InitialBodyComponent>(out var initial, _componentFactory))
continue;
if (initial.Relationships is not { } relationships)
continue;
var parentOf = new Dictionary<ProtoId<OrganCategoryPrototype>, ProtoId<OrganCategoryPrototype>>();
foreach (var (parent, children) in relationships)
{
foreach (var child in children)
{
if (parentOf.TryGetValue(child, out var existingParent))
{
Assert.Fail($"{proto.ID}'s organ category {child} is claimed as a child by both {existingParent} and {parent}");
}
parentOf[child] = parent;
}
}
}
}
[Test]
[RunOnSide(Side.Server)]
public void InternalChildOrgansAreNotDetachable()
{
using var scope = Assert.EnterMultipleScope();
foreach (var proto in _prototype.EnumeratePrototypes<EntityPrototype>())
{
if (proto.Abstract || Pair.IsTestEntityPrototype(proto.ID))
continue;
if (!proto.HasComp<InternalChildOrganComponent>(_componentFactory))
continue;
if (proto.HasComp<DetachableOrganComponent>(_componentFactory))
{
Assert.Fail($"{proto.ID} has both {nameof(InternalChildOrganComponent)} and {nameof(DetachableOrganComponent)}. Pick a lane, make your organs internal or detachable, but not both.");
}
}
}
[Test]
[RunOnSide(Side.Server)]
public void NoCyclicalRelationships()
{
using var scope = Assert.EnterMultipleScope();
foreach (var proto in _prototype.EnumeratePrototypes<EntityPrototype>())
{
if (proto.Abstract || Pair.IsTestEntityPrototype(proto.ID))
continue;
if (!proto.TryComp<InitialBodyComponent>(out var initial, _componentFactory))
continue;
if (initial.Relationships is not { } relationships)
continue;
var visited = new HashSet<ProtoId<OrganCategoryPrototype>>();
var stack = new List<ProtoId<OrganCategoryPrototype>>();
foreach (var parent in relationships.Keys)
{
if (TryFindCycle(parent, relationships, visited, stack, out var cycle))
{
Assert.Fail($"{proto.ID} has a cycle in its InitialBodyComponent relationships: {string.Join(" -> ", cycle)}");
}
}
}
}
private static bool TryFindCycle(
ProtoId<OrganCategoryPrototype> node,
Dictionary<ProtoId<OrganCategoryPrototype>, HashSet<ProtoId<OrganCategoryPrototype>>> relationships,
HashSet<ProtoId<OrganCategoryPrototype>> visited,
List<ProtoId<OrganCategoryPrototype>> stack,
out List<ProtoId<OrganCategoryPrototype>> cycle)
{
if (stack.Contains(node))
{
var start = stack.IndexOf(node);
cycle = stack.Skip(start).Append(node).ToList();
return true;
}
cycle = new List<ProtoId<OrganCategoryPrototype>>();
if (!visited.Add(node))
return false;
if (!relationships.TryGetValue(node, out var children))
return false;
stack.Add(node);
foreach (var child in children)
{
if (TryFindCycle(child, relationships, visited, stack, out cycle))
return true;
}
stack.RemoveAt(stack.Count - 1);
return false;
}
}

View file

@ -0,0 +1,93 @@
using System.Linq;
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.Shared.Body;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Body;
[TestFixture]
[TestOf(typeof(OrganRelationSystem))]
public sealed class OrganRelationSystemTest : GameTest
{
[TestPrototypes]
private const string Prototypes = @"
- type: entity
id: OrganRelationTestOrgan
components:
- type: Organ
- type: ParentOrgan
- type: ChildOrgan
";
[SidedDependency(Side.Server)] private OrganRelationSystem _organRelation = default!;
[Test]
[RunOnSide(Side.Server)]
public void RelateAndOrphan()
{
var parent = SSpawn("OrganRelationTestOrgan");
var child = SSpawn("OrganRelationTestOrgan");
_organRelation.Relate(parent, child);
var parentComp = SComp<ParentOrganComponent>(parent);
var childComp = SComp<ChildOrganComponent>(child);
Assert.That(parentComp.Children, Does.Contain(child));
Assert.That(childComp.Parent, Is.EqualTo(parent));
_organRelation.Orphan(child);
Assert.That(parentComp.Children, Does.Not.Contain(child));
Assert.That(childComp.Parent, Is.Null);
}
[Test]
[RunOnSide(Side.Server)]
public void Traversal()
{
var grandParent = SSpawn("OrganRelationTestOrgan");
var parent = SSpawn("OrganRelationTestOrgan");
var child = SSpawn("OrganRelationTestOrgan");
_organRelation.Relate(grandParent, parent);
_organRelation.Relate(parent, child);
var allChildren = _organRelation.AllChildren(grandParent).Select(e => e.Owner).ToList();
Assert.That(allChildren, Is.EquivalentTo(new[] { parent, child }));
var allParents = _organRelation.AllParents(child).Select(e => e.Owner).ToList();
Assert.That(allParents, Is.EquivalentTo(new[] { parent, grandParent }));
}
[Test]
[RunOnSide(Side.Server)]
public void ParentDeletion()
{
var parent = SSpawn("OrganRelationTestOrgan");
var child = SSpawn("OrganRelationTestOrgan");
_organRelation.Relate(parent, child);
SDeleteNow(parent);
Assert.That(SEntMan.Deleted(child), Is.False);
Assert.That(SComp<ChildOrganComponent>(child).Parent, Is.Null);
}
[Test]
[RunOnSide(Side.Server)]
public void ChildDeletion()
{
var parent = SSpawn("OrganRelationTestOrgan");
var child = SSpawn("OrganRelationTestOrgan");
_organRelation.Relate(parent, child);
SDeleteNow(child);
Assert.That(SComp<ParentOrganComponent>(parent).Children, Does.Not.Contain(child));
}
}

View file

@ -29,7 +29,8 @@ public sealed class PrototypeSaveTest : GameTest
{
// The only prototypes that should get ignored are those that REQUIRE setup to get a sprite. At that point it is
// the responsibility of the spawner to ensure that a valid sprite is set.
"VirtualItem"
"VirtualItem",
"DetachedBody"
};
[Test]

View file

@ -0,0 +1,42 @@
using System.Linq;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Body;
using Robust.Shared.Containers;
using Robust.Shared.Toolshed;
namespace Content.Server.Body;
[ToolshedCommand, AdminCommand(AdminFlags.Debug)]
public sealed class BodyCommand : ToolshedCommand
{
private SharedContainerSystem? _container;
[CommandImplementation("insert")]
public void Insert([PipedArgument] EntityUid body, EntityUid organ)
{
_container ??= GetSys<SharedContainerSystem>();
if (!_container.TryGetContainer(body, BodyComponent.ContainerID, out var container))
return;
_container.Insert(organ, container, force: true);
}
[CommandImplementation("organs")]
public IEnumerable<EntityUid> Organs([PipedArgument] EntityUid body)
{
_container ??= GetSys<SharedContainerSystem>();
if (!_container.TryGetContainer(body, BodyComponent.ContainerID, out var container))
return Enumerable.Empty<EntityUid>();
return container.ContainedEntities.ToList();
}
[CommandImplementation("organs")]
public IEnumerable<EntityUid> Organs([PipedArgument] IEnumerable<EntityUid> body)
{
return body.SelectMany(Organs);
}
}

View file

@ -0,0 +1,112 @@
using System.Linq;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Body;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Toolshed;
namespace Content.Server.Body;
[ToolshedCommand, AdminCommand(AdminFlags.Debug)]
public sealed class OrganCommand : ToolshedCommand
{
private DetachableOrganSystem? _detachableOrgan;
private SharedContainerSystem? _container;
private OrganRelationSystem? _organRelation;
[CommandImplementation("parent")]
public EntityUid? Parent([PipedArgument] EntityUid child)
{
if (!TryComp<ChildOrganComponent>(child, out var childOrgan))
return null;
return childOrgan.Parent;
}
[CommandImplementation("parent")]
public IEnumerable<EntityUid?> Parent([PipedArgument] IEnumerable<EntityUid> children)
{
return children.Select(Parent);
}
[CommandImplementation("children")]
public IEnumerable<EntityUid> Children([PipedArgument] EntityUid parent)
{
if (!TryComp<ParentOrganComponent>(parent, out var parentOrgan))
yield break;
foreach (var child in parentOrgan.Children)
{
yield return child;
}
}
[CommandImplementation("children")]
public IEnumerable<EntityUid> Children([PipedArgument] IEnumerable<EntityUid> parents)
{
return parents.SelectMany(Children);
}
[CommandImplementation("detach")]
public EntityUid? Detach([PipedArgument] EntityUid organ)
{
_detachableOrgan ??= GetSys<DetachableOrganSystem>();
return _detachableOrgan.Detach(organ);
}
[CommandImplementation("detach")]
public IEnumerable<EntityUid?> Detach([PipedArgument] IEnumerable<EntityUid> organs)
{
return organs.Select(Detach);
}
[CommandImplementation("attach")]
public void Attach([PipedArgument] EntityUid parent, EntityUid child)
{
_container ??= GetSys<SharedContainerSystem>();
_organRelation ??= GetSys<OrganRelationSystem>();
if (!TryComp<ParentOrganComponent>(parent, out var parentOrgan) ||
!TryComp<ChildOrganComponent>(child, out var childOrgan) ||
!TryComp<OrganComponent>(parent, out var parentOrganComponent) ||
parentOrganComponent.Body is not { } body)
return;
if (!_container.TryGetContainer(body, BodyComponent.ContainerID, out var container))
return;
_container.Insert(child, container, force: true);
_organRelation.Relate((parent, parentOrgan), (child, childOrgan));
}
[CommandImplementation("attach")]
public void Attach([PipedArgument] IEnumerable<EntityUid> parents, EntityUid child)
{
foreach (var parent in parents)
{
Attach(parent, child);
}
}
[CommandImplementation("is")]
public bool Is([PipedArgument] EntityUid organ, ProtoId<OrganCategoryPrototype> category)
{
if (!TryComp<OrganComponent>(organ, out var organComp))
return false;
return organComp.Category == category;
}
[CommandImplementation("is")]
public IEnumerable<bool> Is([PipedArgument] IEnumerable<EntityUid> organs, ProtoId<OrganCategoryPrototype> category)
{
return organs.Select(it => Is(it, category));
}
[CommandImplementation("of_type")]
public IEnumerable<EntityUid> Typed([PipedArgument] IEnumerable<EntityUid> organs, ProtoId<OrganCategoryPrototype> category)
{
return organs.Where(it => Is(it, category));
}
}

View file

@ -0,0 +1,17 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Body;
/// <summary>
/// An organ that can be a child to a parent organ within a body.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(OrganRelationSystem))]
public sealed partial class ChildOrganComponent : Component
{
/// <summary>
/// The current organ that's a parent to this one, if any
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Parent;
}

View file

@ -0,0 +1,18 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Body;
/// <summary>
/// An organ that can be detached with all of its children into a new body
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(DetachableOrganSystem))]
public sealed partial class DetachableOrganComponent : Component
{
/// <summary>
/// The body to spawn upon detaching
/// </summary>
[DataField(required: true)]
public EntProtoId DetachedBody;
}

View file

@ -0,0 +1,52 @@
using Content.Shared.Body;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Network;
namespace Content.Shared.Body;
public sealed partial class DetachableOrganSystem : EntitySystem
{
[Dependency] private EntityQuery<DetachableOrganComponent> _detachableOrgan;
[Dependency] private EntityQuery<OrganComponent> _organ;
[Dependency] private OrganRelationSystem _organRelation = default!;
[Dependency] private SharedContainerSystem _container = default!;
/// <summary>
/// Detaches an organ from its containing body.
/// </summary>
/// <param name="organ">The organ to detach</param>
/// <returns>The body that spawned when this organ was detached</returns>
[PublicAPI]
public EntityUid? Detach(Entity<DetachableOrganComponent?> organ)
{
if (!_detachableOrgan.Resolve(organ, ref organ.Comp) || !_organ.TryComp(organ, out var organComp) || organComp.Body is not { } oldBody)
return null;
_organRelation.Orphan(organ.Owner);
var body = PredictedSpawnNextToOrDrop(organ.Comp.DetachedBody, oldBody);
if (!_container.TryGetContainer(body, BodyComponent.ContainerID, out var container))
{
Log.Error($"Entity {ToPrettyString(body)} relied on by {nameof(DetachableOrganComponent)} on {ToPrettyString(organ)} is missing a container ({BodyComponent.ContainerID}).");
Del(body);
return null;
}
if (!_container.Insert(organ.Owner, container, force: true))
{
Log.Error($"{ToPrettyString(organ)} could not be transferred to new body {ToPrettyString(body)}.");
}
foreach (var child in _organRelation.AllChildren(organ.Owner))
{
if (!_container.Insert(child.Owner, container, force: true))
{
Log.Error($"{ToPrettyString(child)} could not be transferred to new body {ToPrettyString(body)}.");
_organRelation.Orphan(child.AsNullable());
}
}
return body;
}
}

View file

@ -15,4 +15,10 @@ public sealed partial class InitialBodyComponent : Component
/// </summary>
[DataField(required: true)]
public Dictionary<ProtoId<OrganCategoryPrototype>, EntProtoId<OrganComponent>> Organs;
/// <summary>
/// Organ relationships, based on their categories. Keys are parents to their values.
/// </summary>
[DataField]
public Dictionary<ProtoId<OrganCategoryPrototype>, HashSet<ProtoId<OrganCategoryPrototype>>>? Relationships;
}

View file

@ -1,12 +1,14 @@
using System.Numerics;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Shared.Body;
public sealed partial class InitialBodySystem : EntitySystem
{
[Dependency] private SharedContainerSystem _container = default!;
[Dependency] private OrganRelationSystem _organRelation = default!;
public override void Initialize()
{
@ -31,8 +33,9 @@ public sealed partial class InitialBodySystem : EntitySystem
var xform = Transform(ent);
var coords = new EntityCoordinates(ent, Vector2.Zero);
var spawned = new Dictionary<ProtoId<OrganCategoryPrototype>, EntityUid>();
foreach (var proto in ent.Comp.Organs.Values)
foreach (var (part, proto) in ent.Comp.Organs)
{
// TODO: When e#6192 is merged replace this all with TrySpawnInContainer...
var spawn = Spawn(proto, coords);
@ -41,6 +44,26 @@ public sealed partial class InitialBodySystem : EntitySystem
{
Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(InitialBodyComponent)} failed to insert an entity: {ToPrettyString(spawn)}.\n");
Del(spawn);
continue;
}
spawned[part] = spawn;
}
if (ent.Comp.Relationships is null)
return;
foreach (var (partId, parentUid) in spawned)
{
if (!ent.Comp.Relationships.TryGetValue(partId, out var children))
continue;
foreach (var childId in children)
{
if (!spawned.TryGetValue(childId, out var childUid))
continue;
_organRelation.Relate(parentUid, childUid);
}
}
}

View file

@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Body;
/// <summary>
/// Marker components for child organs that are considered "internal" to their parent. e.g. kidneys are internal to a torso, but an arm isn't.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class InternalChildOrganComponent : Component;

View file

@ -0,0 +1,120 @@
using JetBrains.Annotations;
using Robust.Shared.Utility;
namespace Content.Shared.Body;
public sealed partial class OrganRelationSystem : EntitySystem
{
[Dependency] private EntityQuery<ChildOrganComponent> _child;
[Dependency] private EntityQuery<ParentOrganComponent> _parent;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ParentOrganComponent, ComponentShutdown>(OnParentShutdown);
SubscribeLocalEvent<ChildOrganComponent, ComponentShutdown>(OnChildShutdown);
}
private void OnChildShutdown(Entity<ChildOrganComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.Parent is not { } parentUid)
return;
var parentComp = _parent.Comp(parentUid);
parentComp.Children.Remove(ent);
Dirty(parentUid, parentComp);
}
private void OnParentShutdown(Entity<ParentOrganComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.Children.Count == 0)
return;
foreach (var childUid in ent.Comp.Children)
{
var childComp = _child.Comp(childUid);
childComp.Parent = null;
Dirty(childUid, childComp);
}
}
/// <summary>
/// Associates a parent organ and a child organ.
/// </summary>
[PublicAPI]
public void Relate(Entity<ParentOrganComponent?> parent, Entity<ChildOrganComponent?> child)
{
if (!_parent.Resolve(parent, ref parent.Comp) || !_child.Resolve(child, ref child.Comp))
return;
DebugTools.Assert(child.Comp.Parent == null);
parent.Comp.Children.Add(child);
Dirty(parent, parent.Comp);
child.Comp.Parent = parent;
Dirty(child, child.Comp);
}
/// <summary>
/// Breaks the relationship between a child orphan and its parent.
/// </summary>
[PublicAPI]
public void Orphan(Entity<ChildOrganComponent?> child)
{
if (!_child.Resolve(child, ref child.Comp))
return;
if (child.Comp.Parent is not { } parentUid)
return;
child.Comp.Parent = null;
Dirty(child, child.Comp);
var parentComp = _parent.Comp(parentUid);
parentComp.Children.Remove(child);
Dirty(parentUid, parentComp);
}
/// <summary>
/// Enumerates all parents of a child organ recursively.
/// </summary>
[PublicAPI]
public IEnumerable<Entity<ParentOrganComponent>> AllParents(Entity<ChildOrganComponent?> child)
{
if (!_child.Resolve(child, ref child.Comp))
yield break;
while (child.Comp?.Parent is { } parent)
{
yield return (parent, _parent.Comp(parent));
if (!_child.TryGetComponent(parent, out var parentChild))
yield break;
child = (parent, parentChild);
}
}
/// <summary>
/// Enumerates all children of a parent organ recursively.
/// </summary>
[PublicAPI]
public IEnumerable<Entity<ChildOrganComponent>> AllChildren(Entity<ParentOrganComponent?> parent)
{
if (!_parent.Resolve(parent, ref parent.Comp, false))
yield break;
foreach (var child in parent.Comp.Children)
{
yield return (child, _child.Comp(child));
foreach (var childChild in AllChildren(child))
{
yield return childChild;
}
}
}
}

View file

@ -0,0 +1,17 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Body;
/// <summary>
/// An organ that can be a parent to other organs.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(OrganRelationSystem))]
public sealed partial class ParentOrganComponent : Component
{
/// <summary>
/// The current set of child organs parented to this one.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<EntityUid> Children = new();
}

View file

@ -0,0 +1,23 @@
command-description-body-insert =
Inserts the given organ into the body.
command-description-body-organs =
Returns all organs contained within the body.
command-description-organ-parent =
Returns the parent of the organ.
command-description-organ-children =
Returns the children of the organ.
command-description-organ-detach =
(DEBUG ONLY) Detaches an organ from its current body into a detached body.
command-description-organ-attach =
Attaches an organ to another organ.
command-description-organ-is =
Returns if an organ is the given type.
command-description-organ-of_type =
Filters to organs of the given type.

View file

@ -43,7 +43,10 @@
- Meat
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseParent
- OrganBaseChildExternal
id: OrganBaseTorso
name: torso
abstract: true
@ -68,7 +71,10 @@
- Special
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseParent
- OrganBaseChildExternal
id: OrganBaseHead
name: head
abstract: true
@ -95,7 +101,10 @@
- HeadTop
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseParent
- OrganBaseChildExternal
id: OrganBaseArmLeft
name: left arm
abstract: true
@ -114,7 +123,10 @@
- LArm
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseParent
- OrganBaseChildExternal
id: OrganBaseArmRight
name: right arm
abstract: true
@ -133,7 +145,9 @@
- RArm
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildExternal
id: OrganBaseHandLeft
name: left hand
abstract: true
@ -156,7 +170,9 @@
- LHand
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildExternal
id: OrganBaseHandRight
name: right hand
abstract: true
@ -179,7 +195,10 @@
- RHand
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseParent
- OrganBaseChildExternal
id: OrganBaseLegLeft
name: left leg
abstract: true
@ -198,7 +217,10 @@
- LLeg
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseParent
- OrganBaseChildExternal
id: OrganBaseLegRight
name: right leg
abstract: true
@ -217,7 +239,9 @@
- RLeg
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildExternal
id: OrganBaseFootLeft
name: left foot
abstract: true
@ -236,7 +260,9 @@
- LFoot
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildExternal
id: OrganBaseFootRight
name: right foot
abstract: true
@ -255,7 +281,9 @@
- RFoot
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildInternal
id: OrganBaseBrain
name: brain
abstract: true
@ -288,7 +316,9 @@
Taco: Brain
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildInternal
id: OrganBaseEyes
name: eyes
abstract: true
@ -311,7 +341,9 @@
- Eyes
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildInternal
id: OrganBaseTongue
name: tongue
abstract: true
@ -324,7 +356,9 @@
- state: tongue
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildInternal
id: OrganBaseAppendix
name: appendix
abstract: true
@ -337,7 +371,9 @@
- state: appendix
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildInternal
id: OrganBaseEars
name: ears
abstract: true
@ -350,7 +386,9 @@
- state: ears
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildInternal
id: OrganBaseLungs
name: lungs
abstract: true
@ -383,7 +421,9 @@
canReact: false
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildInternal
id: OrganBaseHeart
name: heart
abstract: true
@ -399,7 +439,9 @@
stages: [ Bloodstream ]
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildInternal
id: OrganBaseStomach
name: stomach
abstract: true
@ -431,7 +473,9 @@
maxVol: 150 # 1.25 liters which is average for a human
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildInternal
id: OrganBaseLiver
name: liver
abstract: true
@ -449,7 +493,9 @@
stages: [ Metabolites ]
- type: entity
parent: OrganBase
parent:
- OrganBase
- OrganBaseChildInternal
id: OrganBaseKidneys
name: kidneys
abstract: true
@ -498,3 +544,26 @@
sexStateOverrides:
Male: torso_m
Female: torso_f
- type: entity
abstract: true
id: OrganBaseParent
components:
- type: ParentOrgan
- type: DetachableOrgan
detachedBody: DetachedBody
- type: entity
abstract: true
id: OrganBaseChildExternal
components:
- type: ChildOrgan
- type: DetachableOrgan
detachedBody: DetachedBody
- type: entity
abstract: true
id: OrganBaseChildInternal
components:
- type: ChildOrgan
- type: InternalChildOrgan

View file

@ -0,0 +1,15 @@
- type: entity
parent:
- BaseSpeciesLayers
id: DetachedBody
save: false
components:
- type: Clickable
- type: Body
- type: VisualBody
- type: ContainerContainer
- type: Appearance
- type: HideableHumanoidLayers
- type: Item
- type: InteractionOutline
- type: InputMover

View file

@ -87,3 +87,11 @@
type: HumanoidMarkingModifierBoundUserInterface
enum.StrippingUiKey.Key:
type: StrippableBoundUserInterface
- type: InitialBody
relationships:
Torso: [Head, ArmLeft, ArmRight, LegLeft, LegRight, Appendix, Lungs, Heart, Stomach, Liver, Kidneys]
Head: [Brain, Eyes, Tongue, Ears]
ArmLeft: [HandLeft]
ArmRight: [HandRight]
LegLeft: [FootLeft]
LegRight: [FootRight]