Repository: DeltaV-Station/Delta-v
Pull request #3418: Allow players to print IPC/FBP chasses
Author: login: sowelipililimute
Labels: name: Changes: YML
description: Changes any yml files
name: S: Needs Review
description: Awaiting review from a Maintainer
name: Changes: C#
description: Changes any cs files
name: size/M
description: 64-255 lines
name: S: Content Approved
description: This PR has been approved by a member of Content Review.
Created: 2025-04-09T20:58:22Z | Merged: 2025-04-13T10:27:42Z

<!-- If you are new to the Delta-V repository, please read the [Contributing Guidelines](https://github.com/DeltaV-Station/Delta-v/blob/master/CONTRIBUTING.md) -->

## About the PR
Players can now print IPC chasses, replacing the dummy torso recipe with an actual torso-only IPC that can have a posibrain or an MMI inserted into it.

IPCs also gain the ability to have an MMI inserted into them as their brain, allowing them to serve as a full-body prosthetic.

IPCs also gain the Anesthesia component on surgery, because robots don't feel pain and there is no logical way to make anesthesia for a species that does not breathe or have metabolism. 

## Why / Balance
This allows the reconstruction of a replacement body for IPCs who have had their

## Technical details
- HumanoidVisualLayersExtension changed to allow getting at the data w/o a component instance
- BodySystem now sets appearance layers correctly at mapinit time
- new prototypes for IPC body w/ only torso and IPC w/ that body prototype
- make the lathe print that instead of the dummy torso

## Media
https://github.com/user-attachments/assets/d8eea7f9-5561-4be2-b3a5-8598e25802f5

## Requirements
<!-- Confirm the following by placing an X in the brackets [X]: -->
- [x] I have tested all added content and changes.
- [x] I have added media to this PR or it does not require an ingame showcase.
<!-- You should understand that not following the above may get your PR closed at maintainer’s discretion -->

## Breaking changes
<!-- List any breaking changes, including namespaces, public class/method/field changes, prototype renames; and provide instructions for fixing them. -->

**Changelog**
<!-- Add a Changelog entry to make players aware of new features or changes that could affect gameplay.
Make sure to read the guidelines and take this Changelog template out of the comment block in order for it to show up.
Changelogs must have a :cl: symbol, so the bot recognizes the changes and adds them to the game's changelog. -->
:cl:
- add: Robotics can now print IPC chasses that can receive a brain and limbs
- add: IPCs do not feel pain in surgery anymore
- add: MMIs can now be inserted into IPC chasses
<!--
:cl:
- add: Added fun!
- remove: Removed fun!
- tweak: Changed fun!
- fix: Fixed fun!
-->



Diff:
diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs
index e745708924c4..819248f1b30a 100644
--- a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs
+++ b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs
@@ -126,7 +126,7 @@ private void OnBodyMapInit(Entity<BodyComponent> ent, ref MapInitEvent args)
         MapInitBody(ent, prototype);
     }
 
-    private void MapInitBody(EntityUid bodyEntity, BodyPrototype prototype)
+    private void MapInitBody(Entity<BodyComponent> bodyEntity, BodyPrototype prototype) // DeltaV - don't drop the component, we need it for below
     {
         var protoRoot = prototype.Slots[prototype.Root];
         if (protoRoot.Part is null)
@@ -141,6 +141,7 @@ private void MapInitBody(EntityUid bodyEntity, BodyPrototype prototype)
         // Setup the rest of the body entities.
         SetupOrgans((rootPartUid, rootPart), protoRoot.Organs);
         MapInitParts(rootPartUid, rootPart, prototype); // Shitmed Change
+        MapInitAppearance(bodyEntity); // DeltaV - some bodies don't spawn with all their parts
     }
 
     private void OnBodyCanDrag(Entity<BodyComponent> ent, ref CanDragEvent args)
diff --git a/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs b/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
index 384f3cc7ae54..c0828d016561 100644
--- a/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
+++ b/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
@@ -82,9 +82,15 @@ public static IEnumerable<HumanoidVisualLayers> Sublayers(HumanoidVisualLayers l
             }
         }
 
+        // Begin DeltaV - We want this data w/o needing a BodyPartComponnet
         public static HumanoidVisualLayers? ToHumanoidLayers(this BodyPartComponent part)
         {
-            switch (part.PartType)
+            return part.PartType.ToHumanoidLayers(part.Symmetry);
+        }
+
+        public static HumanoidVisualLayers? ToHumanoidLayers(this BodyPartType partType, BodyPartSymmetry partSymmetry)
+        {
+            switch (partType)
             {
                 case BodyPartType.Other:
                     break;
@@ -97,7 +103,7 @@ public static IEnumerable<HumanoidVisualLayers> Sublayers(HumanoidVisualLayers l
                     // if that's what you're looking for
                     return HumanoidVisualLayers.Head;
                 case BodyPartType.Arm:
-                    switch (part.Symmetry)
+                    switch (partSymmetry)
                     {
                         case BodyPartSymmetry.None:
                             break;
@@ -109,7 +115,7 @@ public static IEnumerable<HumanoidVisualLayers> Sublayers(HumanoidVisualLayers l
 
                     break;
                 case BodyPartType.Hand:
-                    switch (part.Symmetry)
+                    switch (partSymmetry)
                     {
                         case BodyPartSymmetry.None:
                             break;
@@ -121,7 +127,7 @@ public static IEnumerable<HumanoidVisualLayers> Sublayers(HumanoidVisualLayers l
 
                     break;
                 case BodyPartType.Leg:
-                    switch (part.Symmetry)
+                    switch (partSymmetry)
                     {
                         case BodyPartSymmetry.None:
                             break;
@@ -133,7 +139,7 @@ public static IEnumerable<HumanoidVisualLayers> Sublayers(HumanoidVisualLayers l
 
                     break;
                 case BodyPartType.Foot:
-                    switch (part.Symmetry)
+                    switch (partSymmetry)
                     {
                         case BodyPartSymmetry.None:
                             break;
@@ -148,5 +154,6 @@ public static IEnumerable<HumanoidVisualLayers> Sublayers(HumanoidVisualLayers l
 
             return null;
         }
+        // End DeltaV - We want this data w/o needing a BodyPartComponnet
     }
 }
diff --git a/Content.Shared/_DV/Body/Systems/SharedBodySystem.MapInit.cs b/Content.Shared/_DV/Body/Systems/SharedBodySystem.MapInit.cs
new file mode 100644
index 000000000000..96ced240e5bd
--- /dev/null
+++ b/Content.Shared/_DV/Body/Systems/SharedBodySystem.MapInit.cs
@@ -0,0 +1,57 @@
+using System.Linq;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Part;
+using Content.Shared.Humanoid;
+
+namespace Content.Shared.Body.Systems;
+
+public partial class SharedBodySystem
+{
+    private static readonly BodyPartType[] _asymmetric =
+    [
+        BodyPartType.Other,
+        BodyPartType.Torso,
+        BodyPartType.Tail,
+        BodyPartType.Head,
+    ];
+
+    private static readonly BodyPartType[] _symmetric =
+    [
+        BodyPartType.Arm,
+        BodyPartType.Hand,
+        BodyPartType.Leg,
+        BodyPartType.Foot,
+    ];
+
+    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!;
+
+    private void MapInitAppearance(Entity<BodyComponent> ent)
+    {
+        var symmetries = Enum.GetValues<BodyPartSymmetry>();
+
+        foreach (var part in _asymmetric)
+        {
+            if (part.ToHumanoidLayers(BodyPartSymmetry.None) is not {} layer)
+                continue;
+
+            var layers = HumanoidVisualLayersExtension.Sublayers(layer);
+            var visible = GetBodyChildrenOfType(ent, part).Any();
+
+            _humanoidAppearance.SetLayersVisibility(ent.Owner, layers, visible: visible);
+        }
+
+        foreach (var part in _symmetric)
+        {
+            foreach (var side in symmetries)
+            {
+                if (part.ToHumanoidLayers(side) is not {} layer)
+                    continue;
+
+                var layers = HumanoidVisualLayersExtension.Sublayers(layer);
+                var visible = GetBodyChildrenOfType(ent, part, symmetry: side).Any();
+
+                _humanoidAppearance.SetLayersVisibility(ent.Owner, layers, visible: visible);
+            }
+        }
+    }
+}
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml
index 37a8d9e80710..4b3416dee781 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml
@@ -15,6 +15,8 @@
   - type: Input
     context: human
   - type: MMI
+  - type: Organ # DeltaV - mmis also count as positronic brains for IPC chasses
+    slotId: posbrain
   - type: BorgBrain
   - type: BlockMovement
   - type: Examiner
diff --git a/Resources/Prototypes/_DV/Body/Prototypes/ipc.yml b/Resources/Prototypes/_DV/Body/Prototypes/ipc.yml
new file mode 100644
index 000000000000..e12eb20ac56a
--- /dev/null
+++ b/Resources/Prototypes/_DV/Body/Prototypes/ipc.yml
@@ -0,0 +1,7 @@
+- type: body
+  id: IPCTorsoOnly
+  name: "ipc"
+  root: torso
+  slots:
+    torso:
+      part: TorsoIPC
diff --git a/Resources/Prototypes/_DV/Entities/Mobs/Player/ipc.yml b/Resources/Prototypes/_DV/Entities/Mobs/Player/ipc.yml
new file mode 100644
index 000000000000..1e2ac9ebaf6d
--- /dev/null
+++ b/Resources/Prototypes/_DV/Entities/Mobs/Player/ipc.yml
@@ -0,0 +1,16 @@
+- type: entity
+  parent: MobIPC
+  id: MobIPCTorsoOnly
+  name: ipc chassis
+  suffix: Torso Only
+  components:
+  - type: Body
+    prototype: IPCTorsoOnly
+  - type: IsDeadIC
+  - type: RandomHumanoidAppearance
+  - type: ItemSlots
+    slots:
+      cell_slot:
+        locked: true
+        name: power-cell-slot-component-slot-name-default
+        startingItem: null
diff --git a/Resources/Prototypes/_EE/Entities/Mobs/Player/silicon_base.yml b/Resources/Prototypes/_EE/Entities/Mobs/Player/silicon_base.yml
index a6006a6f6526..24b3e75ef2bd 100644
--- a/Resources/Prototypes/_EE/Entities/Mobs/Player/silicon_base.yml
+++ b/Resources/Prototypes/_EE/Entities/Mobs/Player/silicon_base.yml
@@ -317,3 +317,4 @@
     - type: LayingDown
     - type: Fiber # DeltaV - IPCs leave fibers instead of prints
       fiberMaterial: fibers-steel
+    - type: Anesthesia # DeltaV - IPCs can be operated on without anesthesia
diff --git a/Resources/Prototypes/_Shitmed/Recipes/Lathes/robotics.yml b/Resources/Prototypes/_Shitmed/Recipes/Lathes/robotics.yml
index e6eceeb4de4b..e5de5f1a65ac 100644
--- a/Resources/Prototypes/_Shitmed/Recipes/Lathes/robotics.yml
+++ b/Resources/Prototypes/_Shitmed/Recipes/Lathes/robotics.yml
@@ -108,8 +108,11 @@
 - type: latheRecipe
   parent: BaseRoboticsRecipe
   id: TorsoIPC
-  result: TorsoIPC
-  completetime: 3
+  result: MobIPCTorsoOnly # DeltaV - this prints an actual body that can have stuff inserted into
+  completetime: 15 # DeltaV - Don't let players spam beeping IPC chasses
+  icon: # DeltaV - don't false marketing what the lathe will spit out
+    sprite: _Shitmed/Mobs/Species/IPC/parts.rsi
+    state: "torso_m"
   materials:
     Steel: 3000
     Gold: 500


Reviews:
user: login: deltanedas
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:48:14Z
user: login: sowelipililimute
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T03:15:30Z
user: login: deltanedas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T07:24:28Z
user: login: ThataKat
body: Good to go, IPCs rejoice
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T15:56:03Z
user: login: deltanedas
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T02:27:19Z
user: login: deltanedas
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T10:21:56Z

Review comments:
diff_hunk: @@ -65,6 +65,58 @@ private void OnApplyMetabolicMultiplier(
         }
     }
 
+    // Begin DeltaV - Have body appearances be based on what parts they actually have on spawn
+    protected override void MapInitAppearance(Entity<BodyComponent> ent)
+    {
+        base.MapInitAppearance(ent);
+
+        var asymmetric = new List<BodyPartType>()
+        {
+            BodyPartType.Other,
+            BodyPartType.Torso,
+            BodyPartType.Tail,
+            BodyPartType.Head,
+        };
+
+        var symmetric = new List<BodyPartType>()
+        {
+            BodyPartType.Arm,
+            BodyPartType.Hand,
+            BodyPartType.Leg,
+            BodyPartType.Foot,
+        };
+
+        var symmetries = Enum.GetValues<BodyPartSymmetry>();
+
+        foreach (var part in asymmetric)
+        {
+            var layer = HumanoidVisualLayersExtension.ToHumanoidLayers(part, BodyPartSymmetry.None);
+            if (layer is not {} layerValue)
+                continue;
+
+            var layers = HumanoidVisualLayersExtension.Sublayers(layerValue);
+            var visible = GetBodyChildrenOfType(ent, part).Any();
+
+            _humanoidSystem.SetLayersVisibility(ent.Owner, layers, visible: visible);
+        }
+
+        foreach (var part in symmetric)
+        {
+            foreach (var side in symmetries)
+            {
+                var layer = HumanoidVisualLayersExtension.ToHumanoidLayers(part, side);
+                if (layer is not {} layerValue)
+                    continue;
path: Content.Server/Body/Systems/BodySystem.cs
user: login: deltanedas
body: `if (... is not {} layer)` and use layer instead of layerValue
created_at: 2025-04-09T23:39:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,6 +65,58 @@ private void OnApplyMetabolicMultiplier(
         }
     }
 
+    // Begin DeltaV - Have body appearances be based on what parts they actually have on spawn
+    protected override void MapInitAppearance(Entity<BodyComponent> ent)
path: Content.Server/Body/Systems/BodySystem.cs
user: login: deltanedas
body: no reason this cant be in shared + put it in a _DV partial system instead of directly in SharedBodySystem
created_at: 2025-04-09T23:40:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,8 +141,15 @@ private void MapInitBody(EntityUid bodyEntity, BodyPrototype prototype)
         // Setup the rest of the body entities.
         SetupOrgans((rootPartUid, rootPart), protoRoot.Organs);
         MapInitParts(rootPartUid, rootPart, prototype); // Shitmed Change
+        MapInitAppearance(bodyEntity); // DeltaV - some bodies don't spawn with all their parts
     }
 
+    // Begin DeltaV - Overridable method for the server BodySystem to set up appearance based on what parts the body spawned with
+    protected virtual void MapInitAppearance(Entity<BodyComponent> ent)
+    {
+    }
+    // End DeltaV - Overridable method for the server BodySystem to set up appearance based on what parts the body spawned with
+
path: Content.Shared/Body/Systems/SharedBodySystem.Body.cs
user: login: deltanedas
body: remove
created_at: 2025-04-09T23:40:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -82,9 +82,15 @@ public static IEnumerable<HumanoidVisualLayers> Sublayers(HumanoidVisualLayers l
             }
         }
 
+        // Begin DeltaV - We want this data w/o needing a BodyPartComponnet
         public static HumanoidVisualLayers? ToHumanoidLayers(this BodyPartComponent part)
         {
-            switch (part.PartType)
+            return HumanoidVisualLayersExtension.ToHumanoidLayers(part.PartType, part.Symmetry);
path: Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
user: login: deltanedas
body: `return this.PartType.ToHumanoidLayers(part.Symmetry);`
created_at: 2025-04-09T23:41:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -82,9 +82,15 @@ public static IEnumerable<HumanoidVisualLayers> Sublayers(HumanoidVisualLayers l
             }
         }
 
+        // Begin DeltaV - We want this data w/o needing a BodyPartComponnet
         public static HumanoidVisualLayers? ToHumanoidLayers(this BodyPartComponent part)
         {
-            switch (part.PartType)
+            return HumanoidVisualLayersExtension.ToHumanoidLayers(part.PartType, part.Symmetry);
+        }
+
+        public static HumanoidVisualLayers? ToHumanoidLayers(BodyPartType partType, BodyPartSymmetry partSymmetry)
path: Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
user: login: deltanedas
body: put this before BodyPartType so its actually an extension method
update the use of it accordingly
created_at: 2025-04-09T23:41:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -317,3 +317,4 @@
     - type: LayingDown
     - type: Fiber # DeltaV - IPCs leave fibers instead of prints
       fiberMaterial: fibers-steel
+    - type: Anesthesia # DeltaV - IPCs can be operated on without anesthesia
path: Resources/Prototypes/_EE/Entities/Mobs/Player/silicon_base.yml
user: login: deltanedas
body: add a trailing newline
created_at: 2025-04-09T23:44:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,6 +65,58 @@ private void OnApplyMetabolicMultiplier(
         }
     }
 
+    // Begin DeltaV - Have body appearances be based on what parts they actually have on spawn
+    protected override void MapInitAppearance(Entity<BodyComponent> ent)
+    {
+        base.MapInitAppearance(ent);
+
+        var asymmetric = new List<BodyPartType>()
+        {
+            BodyPartType.Other,
+            BodyPartType.Torso,
+            BodyPartType.Tail,
+            BodyPartType.Head,
+        };
+
+        var symmetric = new List<BodyPartType>()
+        {
+            BodyPartType.Arm,
+            BodyPartType.Hand,
+            BodyPartType.Leg,
+            BodyPartType.Foot,
+        };
path: Content.Server/Body/Systems/BodySystem.cs
user: login: deltanedas
body: these should be const arrays not allocating a list every time a mob spawns
created_at: 2025-04-09T23:48:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,6 +65,58 @@ private void OnApplyMetabolicMultiplier(
         }
     }
 
+    // Begin DeltaV - Have body appearances be based on what parts they actually have on spawn
+    protected override void MapInitAppearance(Entity<BodyComponent> ent)
path: Content.Server/Body/Systems/BodySystem.cs
user: login: sowelipililimute
body: subscribed to what event exactly? (BodyComponent, MapInitEvent) are already being used by this server system
created_at: 2025-04-10T03:15:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,6 +65,58 @@ private void OnApplyMetabolicMultiplier(
         }
     }
 
+    // Begin DeltaV - Have body appearances be based on what parts they actually have on spawn
+    protected override void MapInitAppearance(Entity<BodyComponent> ent)
path: Content.Server/Body/Systems/BodySystem.cs
user: login: deltanedas
body: not subscribed just have this method in the partial system and call it
created_at: 2025-04-10T07:24:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,57 @@
+using System.Linq;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Part;
+using Content.Shared.Humanoid;
+
+namespace Content.Shared.Body.Systems;
+
+public partial class SharedBodySystem
+{
+    private static readonly BodyPartType[] _asymmetric =
+    [
+        BodyPartType.Other,
+        BodyPartType.Torso,
+        BodyPartType.Tail,
+        BodyPartType.Head,
+    ];
+
+    private static readonly BodyPartType[] _symmetric =
+    [
+        BodyPartType.Arm,
+        BodyPartType.Hand,
+        BodyPartType.Leg,
+        BodyPartType.Foot,
+    ];
+
+    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!;
+
+    private void MapInitAppearance(Entity<BodyComponent> ent)
+    {
+        var symmetries = Enum.GetValues<BodyPartSymmetry>();
+
+        foreach (var part in _asymmetric)
+        {
+            if (HumanoidVisualLayersExtension.ToHumanoidLayers(part, BodyPartSymmetry.None) is not {} layer)
path: Content.Shared/_DV/Body/Systems/SharedBodySystem.MapInit.cs
user: login: deltanedas
body: part.ToHumanoidLayers for these
created_at: 2025-04-12T02:26:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: Avalon-Proto
created_at: 2025-04-09T23:32:08Z
body: Neat, another way for players to re-enter the round when gibbed! Any way to set the names?

author_association: CONTRIBUTOR
user: login: Avalon-Proto
created_at: 2025-04-11T16:24:28Z
body: Well not just IPCs :3 you can get gibbed. And slapped into an IPC body instead 
author_association: CONTRIBUTOR