diff --git a/Content.Client/Atmos/GasTileOverlay.cs b/Content.Client/Atmos/GasTileOverlay.cs
index 8f852d0ed1..16a55249e4 100644
--- a/Content.Client/Atmos/GasTileOverlay.cs
+++ b/Content.Client/Atmos/GasTileOverlay.cs
@@ -40,7 +40,12 @@ namespace Content.Client.Atmos
foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
{
- foreach (var tile in mapGrid.GetTilesIntersecting(worldBounds))
+ if (!_gasTileOverlaySystem.HasData(mapGrid.Index))
+ continue;
+
+ var gridBounds = new Box2(mapGrid.WorldToLocal(worldBounds.BottomLeft), mapGrid.WorldToLocal(worldBounds.TopRight));
+
+ foreach (var tile in mapGrid.GetTilesIntersecting(gridBounds))
{
foreach (var (texture, color) in _gasTileOverlaySystem.GetOverlays(mapGrid.Index, tile.GridIndices))
{
diff --git a/Content.Client/Chat/ChatBox.cs b/Content.Client/Chat/ChatBox.cs
index eb37c86703..dbe24f1d76 100644
--- a/Content.Client/Chat/ChatBox.cs
+++ b/Content.Client/Chat/ChatBox.cs
@@ -35,6 +35,8 @@ namespace Content.Client.Chat
public bool ReleaseFocusOnEnter { get; set; } = true;
+ public bool ClearOnEnter { get; set; } = true;
+
public ChatBox()
{
/*MarginLeft = -475.0f;
@@ -166,12 +168,18 @@ namespace Content.Client.Chat
private void Input_OnTextEntered(LineEdit.LineEditEventArgs args)
{
+ // We set it there to true so it's set to false by TextSubmitted.Invoke if necessary
+ ClearOnEnter = true;
+
if (!string.IsNullOrWhiteSpace(args.Text))
{
TextSubmitted?.Invoke(this, args.Text);
}
- Input.Clear();
+ if (ClearOnEnter)
+ {
+ Input.Clear();
+ }
if (ReleaseFocusOnEnter)
{
diff --git a/Content.Client/Chat/ChatManager.cs b/Content.Client/Chat/ChatManager.cs
index 21be7769ae..61dbdc28f1 100644
--- a/Content.Client/Chat/ChatManager.cs
+++ b/Content.Client/Chat/ChatManager.cs
@@ -1,17 +1,21 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Content.Client.Interfaces.Chat;
using Content.Shared.Chat;
using Robust.Client.Console;
using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Client.Interfaces.UserInterface;
+using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
+using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
+using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -45,6 +49,11 @@ namespace Content.Client.Chat
///
private const int SpeechBubbleCap = 4;
+ ///
+ /// The max amount of characters an entity can send in one message
+ ///
+ private int _maxMessageLength = 1000;
+
private const char ConCmdSlash = '/';
private const char OOCAlias = '[';
private const char MeAlias = '@';
@@ -89,11 +98,15 @@ namespace Content.Client.Chat
public void Initialize()
{
_netManager.RegisterNetMessage(MsgChatMessage.NAME, _onChatMessage);
+ _netManager.RegisterNetMessage(ChatMaxMsgLengthMessage.NAME, _onMaxLengthReceived);
_speechBubbleRoot = new LayoutContainer();
LayoutContainer.SetAnchorPreset(_speechBubbleRoot, LayoutContainer.LayoutPreset.Wide);
_userInterfaceManager.StateRoot.AddChild(_speechBubbleRoot);
_speechBubbleRoot.SetPositionFirst();
+
+ // When connexion is achieved, request the max chat message length
+ _netManager.Connected += new EventHandler(RequestMaxLength);
}
public void FrameUpdate(FrameEventArgs delta)
@@ -213,6 +226,15 @@ namespace Content.Client.Chat
if (string.IsNullOrWhiteSpace(text))
return;
+ // Check if message is longer than the character limit
+ if (text.Length > _maxMessageLength)
+ {
+ string locWarning = Loc.GetString("Your message exceeds {0} character limit", _maxMessageLength);
+ _currentChatBox?.AddLine(locWarning, ChatChannel.Server, Color.Orange);
+ _currentChatBox.ClearOnEnter = false; // The text shouldn't be cleared if it hasn't been sent
+ return;
+ }
+
switch (text[0])
{
case ConCmdSlash:
@@ -345,6 +367,17 @@ namespace Content.Client.Chat
}
}
+ private void _onMaxLengthReceived(ChatMaxMsgLengthMessage msg)
+ {
+ _maxMessageLength = msg.MaxMessageLength;
+ }
+
+ private void RequestMaxLength(object sender, NetChannelArgs args)
+ {
+ ChatMaxMsgLengthMessage msg = _netManager.CreateNetMessage();
+ _netManager.ClientSendMessage(msg);
+ }
+
private void AddSpeechBubble(MsgChatMessage msg, SpeechBubble.SpeechType speechType)
{
if (!_entityManager.TryGetEntity(msg.SenderEntity, out var entity))
diff --git a/Content.Client/GameObjects/Components/Atmos/CanSeeGasesComponent.cs b/Content.Client/GameObjects/Components/Atmos/CanSeeGasesComponent.cs
deleted file mode 100644
index d95748f72b..0000000000
--- a/Content.Client/GameObjects/Components/Atmos/CanSeeGasesComponent.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Content.Client.Atmos;
-using Robust.Client.GameObjects;
-using Robust.Client.Interfaces.Graphics.Overlays;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Interfaces.GameObjects;
-using Robust.Shared.IoC;
-
-namespace Content.Client.GameObjects.Components.Atmos
-{
- [RegisterComponent]
- public class CanSeeGasesComponent : Component
- {
- [Dependency] private readonly IOverlayManager _overlayManager = default!;
-
- public override string Name => "CanSeeGases";
-
- public override void HandleMessage(ComponentMessage message, IComponent component)
- {
- base.HandleMessage(message, component);
-
- switch (message)
- {
- case PlayerAttachedMsg _:
- if(!_overlayManager.HasOverlay(nameof(GasTileOverlay)))
- _overlayManager.AddOverlay(new GasTileOverlay());
- break;
-
- case PlayerDetachedMsg _:
- if(!_overlayManager.HasOverlay(nameof(GasTileOverlay)))
- _overlayManager.RemoveOverlay(nameof(GasTileOverlay));
- break;
- }
- }
- }
-}
diff --git a/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs b/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs
new file mode 100644
index 0000000000..2948c9191e
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs
@@ -0,0 +1,71 @@
+#nullable enable
+using Content.Client.GameObjects.Components.Disposal;
+using Content.Client.Interfaces.GameObjects.Components.Interaction;
+using Content.Shared.GameObjects.Components.Body;
+using Content.Shared.GameObjects.Components.Damage;
+using Robust.Client.Interfaces.GameObjects.Components;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Network;
+using Robust.Shared.IoC;
+using Robust.Shared.Players;
+
+namespace Content.Client.GameObjects.Components.Body
+{
+ [RegisterComponent]
+ [ComponentReference(typeof(IDamageableComponent))]
+ [ComponentReference(typeof(IBodyManagerComponent))]
+ public class BodyManagerComponent : SharedBodyManagerComponent, IClientDraggable
+ {
+#pragma warning disable 649
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+#pragma warning restore 649
+
+ public bool ClientCanDropOn(CanDropEventArgs eventArgs)
+ {
+ return eventArgs.Target.HasComponent();
+ }
+
+ public bool ClientCanDrag(CanDragEventArgs eventArgs)
+ {
+ return true;
+ }
+
+ public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null)
+ {
+ if (!Owner.TryGetComponent(out ISpriteComponent sprite))
+ {
+ return;
+ }
+
+ switch (message)
+ {
+ case BodyPartAddedMessage partAdded:
+ sprite.LayerSetVisible(partAdded.RSIMap, true);
+ sprite.LayerSetRSI(partAdded.RSIMap, partAdded.RSIPath);
+ sprite.LayerSetState(partAdded.RSIMap, partAdded.RSIState);
+ break;
+ case BodyPartRemovedMessage partRemoved:
+ sprite.LayerSetVisible(partRemoved.RSIMap, false);
+
+ if (!partRemoved.Dropped.HasValue ||
+ !_entityManager.TryGetEntity(partRemoved.Dropped.Value, out var entity) ||
+ !entity.TryGetComponent(out ISpriteComponent droppedSprite))
+ {
+ break;
+ }
+
+ var color = sprite[partRemoved.RSIMap].Color;
+
+ droppedSprite.LayerSetColor(0, color);
+ break;
+ case MechanismSpriteAddedMessage mechanismAdded:
+ sprite.LayerSetVisible(mechanismAdded.RSIMap, true);
+ break;
+ case MechanismSpriteRemovedMessage mechanismRemoved:
+ sprite.LayerSetVisible(mechanismRemoved.RSIMap, false);
+ break;
+ }
+ }
+ }
+}
diff --git a/Content.Client/Health/BodySystem/BodyScanner/BodyScannerBoundUserInterface.cs b/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerBoundUserInterface.cs
similarity index 76%
rename from Content.Client/Health/BodySystem/BodyScanner/BodyScannerBoundUserInterface.cs
rename to Content.Client/GameObjects/Components/Body/Scanner/BodyScannerBoundUserInterface.cs
index 5bf9bec18b..5b2f5b7abc 100644
--- a/Content.Client/Health/BodySystem/BodyScanner/BodyScannerBoundUserInterface.cs
+++ b/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerBoundUserInterface.cs
@@ -1,11 +1,13 @@
using System.Collections.Generic;
-using Content.Shared.Health.BodySystem.BodyScanner;
+using Content.Shared.Body.Scanner;
+using JetBrains.Annotations;
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects.Components.UserInterface;
using Robust.Shared.ViewVariables;
-namespace Content.Client.Health.BodySystem.BodyScanner
+namespace Content.Client.GameObjects.Components.Body.Scanner
{
+ [UsedImplicitly]
public class BodyScannerBoundUserInterface : BoundUserInterface
{
[ViewVariables]
@@ -17,9 +19,7 @@ namespace Content.Client.Health.BodySystem.BodyScanner
[ViewVariables]
private Dictionary _parts;
- public BodyScannerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
- {
- }
+ public BodyScannerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) { }
protected override void Open()
{
@@ -34,7 +34,9 @@ namespace Content.Client.Health.BodySystem.BodyScanner
base.UpdateState(state);
if (!(state is BodyScannerInterfaceState scannerState))
+ {
return;
+ }
_template = scannerState.Template;
_parts = scannerState.Parts;
@@ -45,7 +47,13 @@ namespace Content.Client.Health.BodySystem.BodyScanner
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
- }
+ if (disposing)
+ {
+ _display.Dispose();
+ _template = null;
+ _parts.Clear();
+ }
+ }
}
}
diff --git a/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerDisplay.cs b/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerDisplay.cs
new file mode 100644
index 0000000000..c5dfe18f44
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerDisplay.cs
@@ -0,0 +1,164 @@
+using System.Collections.Generic;
+using System.Globalization;
+using Content.Shared.Body.Scanner;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
+using static Robust.Client.UserInterface.Controls.ItemList;
+
+namespace Content.Client.GameObjects.Components.Body.Scanner
+{
+ public sealed class BodyScannerDisplay : SS14Window
+ {
+ private BodyScannerTemplateData _template;
+
+ private Dictionary _parts;
+
+ private List _slots;
+
+ private BodyScannerBodyPartData _currentBodyPart;
+
+ public BodyScannerDisplay(BodyScannerBoundUserInterface owner)
+ {
+ IoCManager.InjectDependencies(this);
+ Owner = owner;
+ Title = Loc.GetString("Body Scanner");
+
+ var hSplit = new HBoxContainer
+ {
+ Children =
+ {
+ // Left half
+ new ScrollContainer
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ Children =
+ {
+ (BodyPartList = new ItemList())
+ }
+ },
+ // Right half
+ new VBoxContainer
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ Children =
+ {
+ // Top half of the right half
+ new VBoxContainer
+ {
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ Children =
+ {
+ (BodyPartLabel = new Label()),
+ new HBoxContainer
+ {
+ Children =
+ {
+ new Label
+ {
+ Text = "Health: "
+ },
+ (BodyPartHealth = new Label())
+ }
+ },
+ new ScrollContainer
+ {
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ Children =
+ {
+ (MechanismList = new ItemList())
+ }
+ }
+ }
+ },
+ // Bottom half of the right half
+ (MechanismInfoLabel = new RichTextLabel
+ {
+ SizeFlagsVertical = SizeFlags.FillExpand
+ })
+ }
+ }
+ }
+ };
+
+ Contents.AddChild(hSplit);
+
+ BodyPartList.OnItemSelected += BodyPartOnItemSelected;
+ MechanismList.OnItemSelected += MechanismOnItemSelected;
+ }
+
+ public BodyScannerBoundUserInterface Owner { get; }
+
+ protected override Vector2? CustomSize => (800, 600);
+
+ private ItemList BodyPartList { get; }
+
+ private Label BodyPartLabel { get; }
+
+ private Label BodyPartHealth { get; }
+
+ private ItemList MechanismList { get; }
+
+ private RichTextLabel MechanismInfoLabel { get; }
+
+ public void UpdateDisplay(BodyScannerTemplateData template, Dictionary parts)
+ {
+ _template = template;
+ _parts = parts;
+ _slots = new List();
+ BodyPartList.Clear();
+
+ foreach (var slotName in _parts.Keys)
+ {
+ // We have to do this since ItemLists only return the index of what item is
+ // selected and dictionaries don't allow you to explicitly grab things by index.
+ // So we put the contents of the dictionary into a list so
+ // that we can grab the list by index. I don't know either.
+ _slots.Add(slotName);
+
+ BodyPartList.AddItem(Loc.GetString(slotName));
+ }
+ }
+
+ public void BodyPartOnItemSelected(ItemListSelectedEventArgs args)
+ {
+ if (_parts.TryGetValue(_slots[args.ItemIndex], out _currentBodyPart)) {
+ UpdateBodyPartBox(_currentBodyPart, _slots[args.ItemIndex]);
+ }
+ }
+
+ private void UpdateBodyPartBox(BodyScannerBodyPartData part, string slotName)
+ {
+ BodyPartLabel.Text = $"{Loc.GetString(slotName)}: {Loc.GetString(part.Name)}";
+ BodyPartHealth.Text = $"{part.CurrentDurability}/{part.MaxDurability}";
+
+ MechanismList.Clear();
+ foreach (var mechanism in part.Mechanisms) {
+ MechanismList.AddItem(mechanism.Name);
+ }
+ }
+
+ public void MechanismOnItemSelected(ItemListSelectedEventArgs args)
+ {
+ UpdateMechanismBox(_currentBodyPart.Mechanisms[args.ItemIndex]);
+ }
+
+ private void UpdateMechanismBox(BodyScannerMechanismData mechanism)
+ {
+ // TODO: Improve UI
+ if (mechanism == null)
+ {
+ MechanismInfoLabel.SetMessage("");
+ return;
+ }
+
+ var message =
+ Loc.GetString(
+ $"{mechanism.Name}\nHealth: {mechanism.CurrentDurability}/{mechanism.MaxDurability}\n{mechanism.Description}");
+
+ MechanismInfoLabel.SetMessage(message);
+ }
+ }
+}
diff --git a/Content.Client/Health/BodySystem/Surgery/GenericSurgeryBoundUserInterface.cs b/Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryBoundUserInterface.cs
similarity index 72%
rename from Content.Client/Health/BodySystem/Surgery/GenericSurgeryBoundUserInterface.cs
rename to Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryBoundUserInterface.cs
index 04c0246e83..e44f368221 100644
--- a/Content.Client/Health/BodySystem/Surgery/GenericSurgeryBoundUserInterface.cs
+++ b/Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryBoundUserInterface.cs
@@ -1,29 +1,30 @@
-using Content.Shared.Health.BodySystem.Surgery;
+#nullable enable
+using Content.Shared.Body.Surgery;
+using JetBrains.Annotations;
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects.Components.UserInterface;
-namespace Content.Client.Health.BodySystem.Surgery
+namespace Content.Client.GameObjects.Components.Body.Surgery
{
-
- //TODO : Make window close if target or surgery tool gets too far away from user.
+ // TODO : Make window close if target or surgery tool gets too far away from user.
///
- /// Generic client-side UI list popup that allows users to choose from an option of limbs or organs to operate on.
+ /// Generic client-side UI list popup that allows users to choose from an option
+ /// of limbs or organs to operate on.
///
+ [UsedImplicitly]
public class GenericSurgeryBoundUserInterface : BoundUserInterface
{
+ private GenericSurgeryWindow? _window;
- private GenericSurgeryWindow _window;
-
- public GenericSurgeryBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
- {
-
- }
+ public GenericSurgeryBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) { }
protected override void Open()
{
_window = new GenericSurgeryWindow();
+
_window.OpenCentered();
+ _window.OnClose += Close;
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
@@ -44,40 +45,42 @@ namespace Content.Client.Health.BodySystem.Surgery
private void HandleBodyPartRequest(RequestBodyPartSurgeryUIMessage msg)
{
- _window.BuildDisplay(msg.Targets, BodyPartSelectedCallback);
+ _window?.BuildDisplay(msg.Targets, BodyPartSelectedCallback);
}
+
private void HandleMechanismRequest(RequestMechanismSurgeryUIMessage msg)
{
- _window.BuildDisplay(msg.Targets, MechanismSelectedCallback);
+ _window?.BuildDisplay(msg.Targets, MechanismSelectedCallback);
}
+
private void HandleBodyPartSlotRequest(RequestBodyPartSlotSurgeryUIMessage msg)
{
- _window.BuildDisplay(msg.Targets, BodyPartSlotSelectedCallback);
+ _window?.BuildDisplay(msg.Targets, BodyPartSlotSelectedCallback);
}
-
-
private void BodyPartSelectedCallback(int selectedOptionData)
{
SendMessage(new ReceiveBodyPartSurgeryUIMessage(selectedOptionData));
}
+
private void MechanismSelectedCallback(int selectedOptionData)
{
SendMessage(new ReceiveMechanismSurgeryUIMessage(selectedOptionData));
}
+
private void BodyPartSlotSelectedCallback(int selectedOptionData)
{
SendMessage(new ReceiveBodyPartSlotSurgeryUIMessage(selectedOptionData));
}
-
-
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
- if (!disposing)
- return;
- _window.Dispose();
+
+ if (disposing)
+ {
+ _window?.Dispose();
+ }
}
}
}
diff --git a/Content.Client/Health/BodySystem/Surgery/GenericSurgeryWindow.cs b/Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryWindow.cs
similarity index 57%
rename from Content.Client/Health/BodySystem/Surgery/GenericSurgeryWindow.cs
rename to Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryWindow.cs
index baae1b145c..d432c3342a 100644
--- a/Content.Client/Health/BodySystem/Surgery/GenericSurgeryWindow.cs
+++ b/Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryWindow.cs
@@ -1,58 +1,62 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
-namespace Content.Client.Health.BodySystem.Surgery
+namespace Content.Client.GameObjects.Components.Body.Surgery
{
public class GenericSurgeryWindow : SS14Window
{
- public delegate void CloseCallback();
public delegate void OptionSelectedCallback(int selectedOptionData);
- private Control _vSplitContainer;
- private VBoxContainer _optionsBox;
+ private readonly VBoxContainer _optionsBox;
private OptionSelectedCallback _optionSelectedCallback;
-
protected override Vector2? CustomSize => (300, 400);
public GenericSurgeryWindow()
{
Title = Loc.GetString("Select surgery target...");
RectClipContent = true;
- _vSplitContainer = new VBoxContainer();
- var listScrollContainer = new ScrollContainer
- {
- SizeFlagsVertical = SizeFlags.FillExpand,
- SizeFlagsHorizontal = SizeFlags.FillExpand,
- HScrollEnabled = true,
- VScrollEnabled = true
- };
- _optionsBox = new VBoxContainer
- {
- SizeFlagsHorizontal = SizeFlags.FillExpand
- };
- listScrollContainer.AddChild(_optionsBox);
- _vSplitContainer.AddChild(listScrollContainer);
- Contents.AddChild(_vSplitContainer);
+ var vSplitContainer = new VBoxContainer
+ {
+ Children =
+ {
+ new ScrollContainer
+ {
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ HScrollEnabled = true,
+ VScrollEnabled = true,
+ Children =
+ {
+ (_optionsBox = new VBoxContainer
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand
+ })
+ }
+ }
+ }
+ };
+
+ Contents.AddChild(vSplitContainer);
}
public void BuildDisplay(Dictionary data, OptionSelectedCallback callback)
{
_optionsBox.DisposeAllChildren();
_optionSelectedCallback = callback;
+
foreach (var (displayText, callbackData) in data)
{
var button = new SurgeryButton(callbackData);
- button.SetOnToggleBehavior(OnButtonPressed);
- button.SetDisplayText(CultureInfo.CurrentCulture.TextInfo.ToTitleCase(displayText));
+ button.SetOnToggleBehavior(OnButtonPressed);
+ button.SetDisplayText(Loc.GetString(displayText));
_optionsBox.AddChild(button);
}
@@ -60,17 +64,23 @@ namespace Content.Client.Health.BodySystem.Surgery
private void OnButtonPressed(BaseButton.ButtonEventArgs args)
{
- var pressedButton = (SurgeryButton)args.Button.Parent;
- _optionSelectedCallback(pressedButton.CallbackData);
+ if (args.Button.Parent is SurgeryButton surgery)
+ {
+ _optionSelectedCallback(surgery.CallbackData);
+ }
}
}
class SurgeryButton : PanelContainer
{
public Button Button { get; }
+
private SpriteView SpriteView { get; }
+
private Control EntityControl { get; }
+
private Label DisplayText { get; }
+
public int CallbackData { get; }
public SurgeryButton(int callbackData)
@@ -84,25 +94,28 @@ namespace Content.Client.Health.BodySystem.Surgery
ToggleMode = true,
MouseFilter = MouseFilterMode.Stop
};
+
AddChild(Button);
- var hBoxContainer = new HBoxContainer();
- SpriteView = new SpriteView
+
+ AddChild(new HBoxContainer
{
- CustomMinimumSize = new Vector2(32.0f, 32.0f)
- };
- DisplayText = new Label
- {
- SizeFlagsVertical = SizeFlags.ShrinkCenter,
- Text = "N/A",
- };
- hBoxContainer.AddChild(SpriteView);
- hBoxContainer.AddChild(DisplayText);
- EntityControl = new Control
- {
- SizeFlagsHorizontal = SizeFlags.FillExpand
- };
- hBoxContainer.AddChild(EntityControl);
- AddChild(hBoxContainer);
+ Children =
+ {
+ (SpriteView = new SpriteView
+ {
+ CustomMinimumSize = new Vector2(32.0f, 32.0f)
+ }),
+ (DisplayText = new Label
+ {
+ SizeFlagsVertical = SizeFlags.ShrinkCenter,
+ Text = "N/A",
+ }),
+ (new Control
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand
+ })
+ }
+ });
}
public void SetDisplayText(string text)
diff --git a/Content.Client/GameObjects/Components/DamageableComponent.cs b/Content.Client/GameObjects/Components/DamageableComponent.cs
deleted file mode 100644
index 2e42501894..0000000000
--- a/Content.Client/GameObjects/Components/DamageableComponent.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System.Collections.Generic;
-using Content.Shared.GameObjects.Components.Damage;
-using Robust.Shared.GameObjects;
-
-namespace Content.Client.GameObjects.Components
-{
- ///
- /// Fuck I really hate doing this
- /// TODO: make sure the client only gets damageable component on the clientside entity for its player mob
- ///
- [RegisterComponent]
- public class DamageableComponent : SharedDamageableComponent
- {
- ///
- public override string Name => "Damageable";
-
- public Dictionary CurrentDamage = new Dictionary();
-
- public override void HandleComponentState(ComponentState curState, ComponentState nextState)
- {
- base.HandleComponentState(curState, nextState);
-
- if(curState is DamageComponentState damagestate)
- {
- CurrentDamage = damagestate.CurrentDamage;
- }
- }
- }
-}
diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerBoundUserInterface.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerBoundUserInterface.cs
index 135a55f992..4b2fa2a1fc 100644
--- a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerBoundUserInterface.cs
+++ b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerBoundUserInterface.cs
@@ -1,9 +1,11 @@
+using JetBrains.Annotations;
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects.Components.UserInterface;
using static Content.Shared.GameObjects.Components.Medical.SharedMedicalScannerComponent;
namespace Content.Client.GameObjects.Components.MedicalScanner
{
+ [UsedImplicitly]
public class MedicalScannerBoundUserInterface : BoundUserInterface
{
public MedicalScannerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs
index 4f6eefb5d9..61adc113f2 100644
--- a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs
+++ b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs
@@ -1,6 +1,10 @@
using System.Text;
+using Content.Shared.Damage;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
using Robust.Shared.Maths;
using static Content.Shared.GameObjects.Components.Medical.SharedMedicalScannerComponent;
@@ -14,22 +18,36 @@ namespace Content.Client.GameObjects.Components.MedicalScanner
{
Contents.RemoveAllChildren();
var text = new StringBuilder();
- if (state.MaxHealth == 0)
- {
- text.Append("No patient data.");
- } else
- {
- text.Append($"Patient's health: {state.CurrentHealth}/{state.MaxHealth}\n");
- if (state.DamageDictionary != null)
+ if (!state.Entity.HasValue ||
+ !state.HasDamage() ||
+ !IoCManager.Resolve().TryGetEntity(state.Entity.Value, out var entity))
+ {
+ text.Append(Loc.GetString("No patient data."));
+ }
+ else
+ {
+ text.Append($"{entity.Name}{Loc.GetString("'s health:")}\n");
+
+ foreach (var (@class, classAmount) in state.DamageClasses)
{
- foreach (var (dmgType, amount) in state.DamageDictionary)
+ text.Append($"\n{Loc.GetString("{0}: {1}", @class, classAmount)}");
+
+ foreach (var type in @class.ToTypes())
{
- text.Append($"\n{dmgType}: {amount}");
+ if (!state.DamageTypes.TryGetValue(type, out var typeAmount))
+ {
+ continue;
+ }
+
+ text.Append($"\n- {Loc.GetString("{0}: {1}", type, typeAmount)}");
}
+
+ text.Append("\n");
}
}
- Contents.AddChild(new Label(){Text = text.ToString()});
+
+ Contents.AddChild(new Label() {Text = text.ToString()});
}
}
}
diff --git a/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs b/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs
index 44b4b7e4ce..0ddf3f175a 100644
--- a/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs
+++ b/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs
@@ -44,8 +44,8 @@ namespace Content.Client.GameObjects.Components.Mobs
sprite.LayerSetColor(HumanoidVisualLayers.Hair, Appearance.HairColor);
sprite.LayerSetColor(HumanoidVisualLayers.FacialHair, Appearance.FacialHairColor);
- sprite.LayerSetState(HumanoidVisualLayers.Chest, Sex == Sex.Male ? "human_chest_m" : "human_chest_f");
- sprite.LayerSetState(HumanoidVisualLayers.Head, Sex == Sex.Male ? "human_head_m" : "human_head_f");
+ sprite.LayerSetState(HumanoidVisualLayers.Chest, Sex == Sex.Male ? "torso_m" : "torso_f");
+ sprite.LayerSetState(HumanoidVisualLayers.Head, Sex == Sex.Male ? "head_m" : "head_f");
sprite.LayerSetVisible(HumanoidVisualLayers.StencilMask, Sex == Sex.Female);
diff --git a/Content.Client/GameObjects/Components/Mobs/SpeciesComponent.cs b/Content.Client/GameObjects/Components/Mobs/SpeciesComponent.cs
deleted file mode 100644
index 6500134159..0000000000
--- a/Content.Client/GameObjects/Components/Mobs/SpeciesComponent.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Content.Client.GameObjects.Components.Disposal;
-using Content.Client.Interfaces.GameObjects.Components.Interaction;
-using Content.Shared.GameObjects.Components.Mobs;
-using Robust.Shared.GameObjects;
-
-namespace Content.Client.GameObjects.Components.Mobs
-{
- [RegisterComponent]
- [ComponentReference(typeof(SharedSpeciesComponent))]
- public class SpeciesComponent : SharedSpeciesComponent, IClientDraggable
- {
- bool IClientDraggable.ClientCanDropOn(CanDropEventArgs eventArgs)
- {
- return eventArgs.Target.HasComponent();
- }
-
- bool IClientDraggable.ClientCanDrag(CanDragEventArgs eventArgs)
- {
- return true;
- }
- }
-}
diff --git a/Content.Client/GameObjects/Components/Mobs/SpeciesVisualizer.cs b/Content.Client/GameObjects/Components/Rotation/RotationVisualizer.cs
similarity index 81%
rename from Content.Client/GameObjects/Components/Mobs/SpeciesVisualizer.cs
rename to Content.Client/GameObjects/Components/Rotation/RotationVisualizer.cs
index cc2e60b92e..d78cf08a33 100644
--- a/Content.Client/GameObjects/Components/Mobs/SpeciesVisualizer.cs
+++ b/Content.Client/GameObjects/Components/Rotation/RotationVisualizer.cs
@@ -1,5 +1,6 @@
using System;
-using Content.Shared.GameObjects.Components.Mobs;
+using Content.Shared.GameObjects.Components.Rotation;
+using JetBrains.Annotations;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.GameObjects.Components.Animations;
@@ -7,22 +8,23 @@ using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.Animations;
using Robust.Shared.Maths;
-namespace Content.Client.GameObjects.Components.Mobs
+namespace Content.Client.GameObjects.Components.Rotation
{
- public class SpeciesVisualizer : AppearanceVisualizer
+ [UsedImplicitly]
+ public class RotationVisualizer : AppearanceVisualizer
{
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
- if (component.TryGetData(SharedSpeciesComponent.MobVisuals.RotationState, out var state))
+ if (component.TryGetData(RotationVisuals.RotationState, out var state))
{
switch (state)
{
- case SharedSpeciesComponent.MobState.Standing:
+ case RotationState.Vertical:
SetRotation(component, 0);
break;
- case SharedSpeciesComponent.MobState.Down:
+ case RotationState.Horizontal:
SetRotation(component, Angle.FromDegrees(90));
break;
}
@@ -40,7 +42,9 @@ namespace Content.Client.GameObjects.Components.Mobs
}
if (animation.HasRunningAnimation("rotate"))
+ {
animation.Stop("rotate");
+ }
animation.Play(new Animation
{
diff --git a/Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs
index a3eeadd0d3..253563f527 100644
--- a/Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs
+++ b/Content.Client/GameObjects/EntitySystems/GasTileOverlaySystem.cs
@@ -1,13 +1,19 @@
-using System;
+#nullable enable
+using System;
using System.Collections.Generic;
+using Content.Client.Atmos;
+using Content.Client.GameObjects.Components.Atmos;
using Content.Shared.Atmos;
-using Content.Shared.GameObjects.EntitySystems;
+using Content.Shared.GameObjects.EntitySystems.Atmos;
using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+using Robust.Client.GameObjects.EntitySystems;
using Robust.Client.Graphics;
+using Robust.Client.Interfaces.Graphics.Overlays;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
-using Robust.Shared.GameObjects;
+using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -16,8 +22,9 @@ using Robust.Shared.Utility;
namespace Content.Client.GameObjects.EntitySystems
{
[UsedImplicitly]
- public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
+ internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
{
+ [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
private readonly Dictionary _fireCache = new Dictionary();
@@ -36,19 +43,20 @@ namespace Content.Client.GameObjects.EntitySystems
private readonly float[][] _fireFrameDelays = new float[FireStates][];
private readonly int[] _fireFrameCounter = new int[FireStates];
private readonly Texture[][] _fireFrames = new Texture[FireStates][];
-
- private Dictionary> _overlay = new Dictionary>();
+
+ private Dictionary> _tileData =
+ new Dictionary>();
public override void Initialize()
{
base.Initialize();
-
- SubscribeNetworkEvent(new EntityEventHandler(OnTileOverlayMessage));
+ SubscribeNetworkEvent(HandleGasOverlayMessage);
+ _mapManager.OnGridRemoved += OnGridRemoved;
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
- var gas = Atmospherics.GetGas(i);
- switch (gas.GasOverlay)
+ var overlay = Atmospherics.GetOverlay(i);
+ switch (overlay)
{
case SpriteSpecifier.Rsi animated:
var rsi = _resourceCache.GetResource(animated.RsiPath).RSI;
@@ -82,13 +90,77 @@ namespace Content.Client.GameObjects.EntitySystems
_fireFrameDelays[i] = state.GetDelays();
_fireFrameCounter[i] = 0;
}
+
+ var overlayManager = IoCManager.Resolve();
+ if(!overlayManager.HasOverlay(nameof(GasTileOverlay)))
+ overlayManager.AddOverlay(new GasTileOverlay());
+ }
+
+ private void HandleGasOverlayMessage(GasOverlayMessage message)
+ {
+ foreach (var (indices, data) in message.OverlayData)
+ {
+ var chunk = GetOrCreateChunk(message.GridId, indices);
+ chunk.Update(data, indices);
+ }
+ }
+
+ // Slightly different to the server-side system version
+ private GasOverlayChunk GetOrCreateChunk(GridId gridId, MapIndices indices)
+ {
+ if (!_tileData.TryGetValue(gridId, out var chunks))
+ {
+ chunks = new Dictionary();
+ _tileData[gridId] = chunks;
+ }
+
+ var chunkIndices = GetGasChunkIndices(indices);
+
+ if (!chunks.TryGetValue(chunkIndices, out var chunk))
+ {
+ chunk = new GasOverlayChunk(gridId, chunkIndices);
+ chunks[chunkIndices] = chunk;
+ }
+
+ return chunk;
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _mapManager.OnGridRemoved -= OnGridRemoved;
+ var overlayManager = IoCManager.Resolve();
+ if(!overlayManager.HasOverlay(nameof(GasTileOverlay)))
+ overlayManager.RemoveOverlay(nameof(GasTileOverlay));
+ }
+
+ private void OnGridRemoved(GridId gridId)
+ {
+ if (_tileData.ContainsKey(gridId))
+ {
+ _tileData.Remove(gridId);
+ }
+ }
+
+ public bool HasData(GridId gridId)
+ {
+ return _tileData.ContainsKey(gridId);
}
public (Texture, Color color)[] GetOverlays(GridId gridIndex, MapIndices indices)
{
- if (!_overlay.TryGetValue(gridIndex, out var tiles) || !tiles.TryGetValue(indices, out var overlays))
+ if (!_tileData.TryGetValue(gridIndex, out var chunks))
return Array.Empty<(Texture, Color)>();
+
+ var chunkIndex = GetGasChunkIndices(indices);
+ if (!chunks.TryGetValue(chunkIndex, out var chunk))
+ return Array.Empty<(Texture, Color)>();
+
+ var overlays = chunk.GetData(indices);
+ if (overlays.Gas == null)
+ return Array.Empty<(Texture, Color)>();
+
var fire = overlays.FireState != 0;
var length = overlays.Gas.Length + (fire ? 1 : 0);
@@ -112,23 +184,6 @@ namespace Content.Client.GameObjects.EntitySystems
return list;
}
- private void OnTileOverlayMessage(GasTileOverlayMessage ev)
- {
- if(ev.ClearAllOtherOverlays)
- _overlay.Clear();
-
- foreach (var data in ev.OverlayData)
- {
- if (!_overlay.TryGetValue(data.GridIndex, out var gridOverlays))
- {
- gridOverlays = new Dictionary();
- _overlay.Add(data.GridIndex, gridOverlays);
- }
-
- gridOverlays[data.GridIndices] = data.Data;
- }
- }
-
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
diff --git a/Content.Client/Health/BodySystem/BodyScanner/BodyScannerDisplay.cs b/Content.Client/Health/BodySystem/BodyScanner/BodyScannerDisplay.cs
deleted file mode 100644
index d997c73216..0000000000
--- a/Content.Client/Health/BodySystem/BodyScanner/BodyScannerDisplay.cs
+++ /dev/null
@@ -1,155 +0,0 @@
-using System.Collections.Generic;
-using System.Globalization;
-using Content.Shared.Health.BodySystem.BodyScanner;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Shared.IoC;
-using Robust.Shared.Localization;
-using Robust.Shared.Maths;
-using static Robust.Client.UserInterface.Controls.ItemList;
-
-namespace Content.Client.Health.BodySystem.BodyScanner
-{
- public sealed class BodyScannerDisplay : SS14Window
- {
- #pragma warning disable 649
- [Dependency] private readonly ILocalizationManager _loc;
- #pragma warning restore 649
-
- public BodyScannerBoundUserInterface Owner { get; private set; }
- protected override Vector2? CustomSize => (800, 600);
- private ItemList BodyPartList { get; }
- private Label BodyPartLabel { get; }
- private Label BodyPartHealth { get; }
- private ItemList MechanismList { get; }
- private RichTextLabel MechanismInfoLabel { get; }
-
-
- private BodyScannerTemplateData _template;
- private Dictionary _parts;
- private List _slots;
- private BodyScannerBodyPartData _currentBodyPart;
-
-
- public BodyScannerDisplay(BodyScannerBoundUserInterface owner)
- {
- IoCManager.InjectDependencies(this);
- Owner = owner;
- Title = _loc.GetString("Body Scanner");
-
- var hSplit = new HBoxContainer();
- Contents.AddChild(hSplit);
-
- //Left half
- var scrollBox = new ScrollContainer
- {
- SizeFlagsHorizontal = SizeFlags.FillExpand,
- };
- hSplit.AddChild(scrollBox);
- BodyPartList = new ItemList { };
- scrollBox.AddChild(BodyPartList);
- BodyPartList.OnItemSelected += BodyPartOnItemSelected;
-
- //Right half
- var vSplit = new VBoxContainer
- {
- SizeFlagsHorizontal = SizeFlags.FillExpand,
- };
- hSplit.AddChild(vSplit);
-
- //Top half of the right half
- var limbBox = new VBoxContainer
- {
- SizeFlagsVertical = SizeFlags.FillExpand
- };
- vSplit.AddChild(limbBox);
- BodyPartLabel = new Label();
- limbBox.AddChild(BodyPartLabel);
- var limbHealthHBox = new HBoxContainer();
- limbBox.AddChild(limbHealthHBox);
- var healthLabel = new Label
- {
- Text = "Health: "
- };
- limbHealthHBox.AddChild(healthLabel);
- BodyPartHealth = new Label();
- limbHealthHBox.AddChild(BodyPartHealth);
- var limbScroll = new ScrollContainer
- {
- SizeFlagsVertical = SizeFlags.FillExpand
- };
- limbBox.AddChild(limbScroll);
- MechanismList = new ItemList();
- limbScroll.AddChild(MechanismList);
- MechanismList.OnItemSelected += MechanismOnItemSelected;
-
- //Bottom half of the right half
- MechanismInfoLabel = new RichTextLabel
- {
- SizeFlagsVertical = SizeFlags.FillExpand
- };
- vSplit.AddChild(MechanismInfoLabel);
- }
-
-
- public void UpdateDisplay(BodyScannerTemplateData template, Dictionary parts)
- {
- _template = template;
- _parts = parts;
- _slots = new List();
- BodyPartList.Clear();
- foreach (var (key, value) in _parts)
- {
- _slots.Add(key); //We have to do this since ItemLists only return the index of what item is selected and dictionaries don't allow you to explicitly grab things by index.
- //So we put the contents of the dictionary into a list so that we can grab the list by index. I don't know either.
- BodyPartList.AddItem(CultureInfo.CurrentCulture.TextInfo.ToTitleCase(key));
- }
- }
-
-
- public void BodyPartOnItemSelected(ItemListSelectedEventArgs args)
- {
- if(_parts.TryGetValue(_slots[args.ItemIndex], out _currentBodyPart)) {
- UpdateBodyPartBox(_currentBodyPart, _slots[args.ItemIndex]);
- }
- }
- private void UpdateBodyPartBox(BodyScannerBodyPartData part, string slotName)
- {
- BodyPartLabel.Text = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(slotName) + ": " + CultureInfo.CurrentCulture.TextInfo.ToTitleCase(part.Name);
- BodyPartHealth.Text = part.CurrentDurability + "/" + part.MaxDurability;
-
- MechanismList.Clear();
- foreach (var mechanism in part.Mechanisms) {
- MechanismList.AddItem(mechanism.Name);
- }
- }
-
-
- public void MechanismOnItemSelected(ItemListSelectedEventArgs args)
- {
- UpdateMechanismBox(_currentBodyPart.Mechanisms[args.ItemIndex]);
- }
- private void UpdateMechanismBox(BodyScannerMechanismData mechanism)
- {
- //TODO: Make UI look less shit and clean up whatever the fuck this is lmao
- if (mechanism != null)
- {
- string message = "";
- message += mechanism.Name;
- message += "\nHealth: ";
- message += mechanism.CurrentDurability;
- message += "/";
- message += mechanism.MaxDurability;
- message += "\n";
- message += mechanism.Description;
- MechanismInfoLabel.SetMessage(message);
- }
- else
- {
- MechanismInfoLabel.SetMessage("");
- }
- }
-
-
- }
-}
diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs
index 5169e4a189..a032972f02 100644
--- a/Content.Client/IgnoredComponents.cs
+++ b/Content.Client/IgnoredComponents.cs
@@ -96,7 +96,6 @@
"BarSign",
"DroppedBodyPart",
"DroppedMechanism",
- "BodyManager",
"SolarPanel",
"BodyScanner",
"Stunbaton",
@@ -157,6 +156,7 @@
"Vapor",
"DamageOnHighSpeedImpact",
"Barotrauma",
+ "MobStateManager",
};
}
}
diff --git a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs
index 005a30a9bc..6c15106012 100644
--- a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs
+++ b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using Content.Server.AI.Operators;
@@ -6,10 +6,9 @@ using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.BehaviorSets;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Utility;
-using Content.Server.GameObjects.Components.Damage;
-using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems.AI.LoadBalancer;
using Content.Server.GameObjects.EntitySystems.JobQueues;
+using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.AI;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
@@ -114,39 +113,27 @@ namespace Content.Server.AI.Utility.AiLogic
_planCooldownRemaining = PlanCooldown;
_blackboard = new Blackboard(SelfEntity);
_planner = IoCManager.Resolve().GetEntitySystem();
- if (SelfEntity.TryGetComponent(out DamageableComponent damageableComponent))
+ if (SelfEntity.TryGetComponent(out IDamageableComponent damageableComponent))
{
- damageableComponent.DamageThresholdPassed += DamageThresholdHandle;
+ damageableComponent.HealthChangedEvent += DeathHandle;
}
}
public override void Shutdown()
{
// TODO: If DamageableComponent removed still need to unsubscribe?
- if (SelfEntity.TryGetComponent(out DamageableComponent damageableComponent))
+ if (SelfEntity.TryGetComponent(out IDamageableComponent damageableComponent))
{
- damageableComponent.DamageThresholdPassed -= DamageThresholdHandle;
+ damageableComponent.HealthChangedEvent -= DeathHandle;
}
var currentOp = CurrentAction?.ActionOperators.Peek();
currentOp?.Shutdown(Outcome.Failed);
}
- private void DamageThresholdHandle(object sender, DamageThresholdPassedEventArgs eventArgs)
+ private void DeathHandle(HealthChangedEventArgs eventArgs)
{
- if (!SelfEntity.TryGetComponent(out SpeciesComponent speciesComponent))
- {
- return;
- }
-
- if (speciesComponent.CurrentDamageState is DeadState)
- {
- _isDead = true;
- }
- else
- {
- _isDead = false;
- }
+ _isDead = eventArgs.Damageable.CurrentDamageState == DamageState.Dead || eventArgs.Damageable.CurrentDamageState == DamageState.Critical;
}
private void ReceivedAction()
diff --git a/Content.Server/AI/Utility/Considerations/Combat/TargetHealthCon.cs b/Content.Server/AI/Utility/Considerations/Combat/TargetHealthCon.cs
index 8e70bb9200..64fce8bf56 100644
--- a/Content.Server/AI/Utility/Considerations/Combat/TargetHealthCon.cs
+++ b/Content.Server/AI/Utility/Considerations/Combat/TargetHealthCon.cs
@@ -1,4 +1,4 @@
-using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Damage;
@@ -11,13 +11,12 @@ namespace Content.Server.AI.Utility.Considerations.Combat
{
var target = context.GetState().GetValue();
- if (target == null || !target.TryGetComponent(out DamageableComponent damageableComponent))
+ if (target == null || !target.TryGetComponent(out IDamageableComponent damageableComponent))
{
return 0.0f;
}
- // Just went with max health
- return damageableComponent.CurrentDamage[DamageType.Total] / 300.0f;
+ return damageableComponent.TotalDamage / 300.0f;
}
}
}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs b/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs
index 97ed516326..856a53958d 100644
--- a/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs
+++ b/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs
@@ -1,6 +1,6 @@
-using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
-using Content.Server.GameObjects.Components.Mobs;
+using Content.Shared.GameObjects.Components.Damage;
namespace Content.Server.AI.Utility.Considerations.Combat
{
@@ -10,12 +10,12 @@ namespace Content.Server.AI.Utility.Considerations.Combat
{
var target = context.GetState().GetValue();
- if (target == null || !target.TryGetComponent(out SpeciesComponent speciesComponent))
+ if (target == null || !target.TryGetComponent(out IDamageableComponent damageableComponent))
{
return 0.0f;
}
- if (speciesComponent.CurrentDamageState is CriticalState)
+ if (damageableComponent.CurrentDamageState == DamageState.Critical)
{
return 1.0f;
}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs b/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs
index e973538e79..d4d582e6b1 100644
--- a/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs
+++ b/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs
@@ -1,6 +1,6 @@
-using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
-using Content.Server.GameObjects.Components.Mobs;
+using Content.Shared.GameObjects.Components.Damage;
namespace Content.Server.AI.Utility.Considerations.Combat
{
@@ -10,12 +10,12 @@ namespace Content.Server.AI.Utility.Considerations.Combat
{
var target = context.GetState().GetValue();
- if (target == null || !target.TryGetComponent(out SpeciesComponent speciesComponent))
+ if (target == null || !target.TryGetComponent(out IDamageableComponent damageableComponent))
{
return 0.0f;
}
- if (speciesComponent.CurrentDamageState is DeadState)
+ if (damageableComponent.CurrentDamageState == DamageState.Dead)
{
return 1.0f;
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs
index b54aed6274..cde979be3e 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.Actions.Combat.Melee;
@@ -7,8 +7,8 @@ using Content.Server.AI.Utility.Considerations.Combat.Melee;
using Content.Server.AI.Utils;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
-using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Movement;
+using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.GameObjects;
using Robust.Shared.IoC;
@@ -37,7 +37,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
throw new InvalidOperationException();
}
- foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(SpeciesComponent),
+ foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(IDamageableComponent),
controller.VisionRadius))
{
if (entity.HasComponent() && entity != owner)
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs
index 29937b0270..bd78ab74ec 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs
@@ -15,7 +15,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
{
var owner = context.GetState().GetValue();
- foreach (var entity in context.GetState().GetValue())
+ foreach (var entity in context.GetState().GetValue())
{
yield return new MeleeWeaponAttackEntity(owner, entity, Bonus);
}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyPlayerExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyPlayerExp.cs
index 74696e13e5..46f1f79479 100644
--- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyPlayerExp.cs
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyPlayerExp.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.Actions.Combat.Melee;
@@ -7,8 +7,8 @@ using Content.Server.AI.Utility.Considerations.Combat.Melee;
using Content.Server.AI.Utils;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
-using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Movement;
+using Content.Shared.GameObjects.Components.Body;
using Robust.Server.GameObjects;
using Robust.Shared.IoC;
@@ -37,7 +37,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
throw new InvalidOperationException();
}
- foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(SpeciesComponent),
+ foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(IBodyManagerComponent),
controller.VisionRadius))
{
if (entity.HasComponent() && entity != owner)
diff --git a/Content.Server/AI/WorldState/States/Mobs/NearbySpeciesState.cs b/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs
similarity index 70%
rename from Content.Server/AI/WorldState/States/Mobs/NearbySpeciesState.cs
rename to Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs
index a72786d326..2554c4cf53 100644
--- a/Content.Server/AI/WorldState/States/Mobs/NearbySpeciesState.cs
+++ b/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs
@@ -1,16 +1,16 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using Content.Server.AI.Utils;
-using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Movement;
+using Content.Shared.GameObjects.Components.Body;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.AI.WorldState.States.Mobs
{
[UsedImplicitly]
- public sealed class NearbySpeciesState : CachedStateData>
+ public sealed class NearbyBodiesState : CachedStateData>
{
- public override string Name => "NearbySpecies";
+ public override string Name => "NearbyBodies";
protected override List GetTrueValue()
{
@@ -21,7 +21,7 @@ namespace Content.Server.AI.WorldState.States.Mobs
return result;
}
- foreach (var entity in Visibility.GetEntitiesInRange(Owner.Transform.GridPosition, typeof(SpeciesComponent), controller.VisionRadius))
+ foreach (var entity in Visibility.GetEntitiesInRange(Owner.Transform.GridPosition, typeof(IBodyManagerComponent), controller.VisionRadius))
{
if (entity == Owner) continue;
result.Add(entity);
diff --git a/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs b/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs
index f1c7bbae1a..1ba739e3aa 100644
--- a/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs
+++ b/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs
@@ -1,6 +1,6 @@
-using System.Collections.Generic;
-using Content.Server.GameObjects.Components.Mobs;
+using System.Collections.Generic;
using Content.Server.GameObjects.Components.Movement;
+using Content.Shared.GameObjects.Components.Damage;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects;
@@ -27,7 +27,7 @@ namespace Content.Server.AI.WorldState.States.Mobs
foreach (var player in nearbyPlayers)
{
- if (player.AttachedEntity != Owner && player.AttachedEntity.HasComponent())
+ if (player.AttachedEntity != Owner && player.AttachedEntity.HasComponent())
{
result.Add(player.AttachedEntity);
}
diff --git a/Content.Server/Atmos/Hotspot.cs b/Content.Server/Atmos/Hotspot.cs
index 2178a6bea8..4597813c6f 100644
--- a/Content.Server/Atmos/Hotspot.cs
+++ b/Content.Server/Atmos/Hotspot.cs
@@ -23,7 +23,7 @@ namespace Content.Server.Atmos
/// State for the fire sprite.
///
[ViewVariables]
- public int State;
+ public byte State;
public void Start()
{
diff --git a/Content.Server/Atmos/TileAtmosphere.cs b/Content.Server/Atmos/TileAtmosphere.cs
index cddfc3ee96..b1ad2df3cf 100644
--- a/Content.Server/Atmos/TileAtmosphere.cs
+++ b/Content.Server/Atmos/TileAtmosphere.cs
@@ -5,6 +5,7 @@ using System.Runtime.CompilerServices;
using Content.Server.Atmos.Reactions;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.EntitySystems;
+using Content.Server.GameObjects.EntitySystems.Atmos;
using Content.Server.Interfaces;
using Content.Shared.Atmos;
using Content.Shared.Audio;
@@ -745,7 +746,7 @@ namespace Content.Server.Atmos
}
else
{
- Hotspot.State = Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1;
+ Hotspot.State = (byte) (Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1);
}
if (Hotspot.Temperature > MaxFireTemperatureSustained)
diff --git a/Content.Server/Body/BodyCommands.cs b/Content.Server/Body/BodyCommands.cs
new file mode 100644
index 0000000000..89d6355203
--- /dev/null
+++ b/Content.Server/Body/BodyCommands.cs
@@ -0,0 +1,147 @@
+#nullable enable
+using System.Linq;
+using Content.Server.GameObjects.Components.Body;
+using Content.Shared.Body.Part;
+using Content.Shared.GameObjects.Components.Body;
+using Robust.Server.Interfaces.Console;
+using Robust.Server.Interfaces.Player;
+using Robust.Shared.Interfaces.Random;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Body
+{
+ class AddHandCommand : IClientCommand
+ {
+ public string Command => "addhand";
+ public string Description => "Adds a hand to your entity.";
+ public string Help => $"Usage: {Command}";
+
+ public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
+ {
+ if (player == null)
+ {
+ shell.SendText(player, "Only a player can run this command.");
+ return;
+ }
+
+ if (player.AttachedEntity == null)
+ {
+ shell.SendText(player, "You have no entity.");
+ return;
+ }
+
+ if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent body))
+ {
+ var random = IoCManager.Resolve();
+ var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
+
+ shell.SendText(player, text);
+ return;
+ }
+
+ var prototypeManager = IoCManager.Resolve();
+ prototypeManager.TryIndex("bodyPart.Hand.BasicHuman", out BodyPartPrototype prototype);
+
+ var part = new BodyPart(prototype);
+ var slot = part.GetHashCode().ToString();
+
+ body.Template.Slots.Add(slot, BodyPartType.Hand);
+ body.InstallBodyPart(part, slot);
+ }
+ }
+
+ class RemoveHandCommand : IClientCommand
+ {
+ public string Command => "removehand";
+ public string Description => "Removes a hand from your entity.";
+ public string Help => $"Usage: {Command}";
+
+ public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
+ {
+ if (player == null)
+ {
+ shell.SendText(player, "Only a player can run this command.");
+ return;
+ }
+
+ if (player.AttachedEntity == null)
+ {
+ shell.SendText(player, "You have no entity.");
+ return;
+ }
+
+ if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent body))
+ {
+ var random = IoCManager.Resolve();
+ var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
+
+ shell.SendText(player, text);
+ return;
+ }
+
+ var hand = body.Parts.FirstOrDefault(x => x.Value.PartType == BodyPartType.Hand);
+ if (hand.Value == null)
+ {
+ shell.SendText(player, "You have no hands.");
+ }
+ else
+ {
+ body.DisconnectBodyPart(hand.Value, true);
+ }
+ }
+ }
+
+ class DestroyMechanismCommand : IClientCommand
+ {
+ public string Command => "destroymechanism";
+ public string Description => "Destroys a mechanism from your entity";
+ public string Help => $"Usage: {Command} ";
+
+ public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
+ {
+ if (player == null)
+ {
+ shell.SendText(player, "Only a player can run this command.");
+ return;
+ }
+
+ if (args.Length == 0)
+ {
+ shell.SendText(player, Help);
+ return;
+ }
+
+ if (player.AttachedEntity == null)
+ {
+ shell.SendText(player, "You have no entity.");
+ return;
+ }
+
+ if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent body))
+ {
+ var random = IoCManager.Resolve();
+ var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
+
+ shell.SendText(player, text);
+ return;
+ }
+
+ var mechanismName = string.Join(" ", args).ToLowerInvariant();
+
+ foreach (var part in body.Parts.Values)
+ foreach (var mechanism in part.Mechanisms)
+ {
+ if (mechanism.Name.ToLowerInvariant() == mechanismName)
+ {
+ part.DestroyMechanism(mechanism);
+ shell.SendText(player, $"Mechanism with name {mechanismName} has been destroyed.");
+ return;
+ }
+ }
+
+ shell.SendText(player, $"No mechanism was found with name {mechanismName}.");
+ }
+ }
+}
diff --git a/Content.Server/Body/BodyPart.cs b/Content.Server/Body/BodyPart.cs
new file mode 100644
index 0000000000..32121f056a
--- /dev/null
+++ b/Content.Server/Body/BodyPart.cs
@@ -0,0 +1,602 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Server.Body.Mechanisms;
+using Content.Server.Body.Surgery;
+using Content.Server.GameObjects.Components.Body;
+using Content.Server.GameObjects.Components.Metabolism;
+using Content.Shared.Body.Mechanism;
+using Content.Shared.Body.Part;
+using Content.Shared.Body.Part.Properties;
+using Content.Shared.Damage.DamageContainer;
+using Content.Shared.Damage.ResistanceSet;
+using Content.Shared.GameObjects.Components.Body;
+using Content.Shared.GameObjects.Components.Damage;
+using Robust.Server.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Reflection;
+using Robust.Shared.Interfaces.Serialization;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Body
+{
+ ///
+ /// Data class representing a singular limb such as an arm or a leg.
+ /// Typically held within either a ,
+ /// which coordinates functions between BodyParts, or a
+ /// .
+ ///
+ public class BodyPart
+ {
+ ///
+ /// The body that this body part is in, if any.
+ ///
+ private BodyManagerComponent? _body;
+
+ ///
+ /// Set of all currently inside this
+ /// .
+ /// To add and remove from this list see and
+ ///
+ ///
+ private readonly HashSet _mechanisms = new HashSet();
+
+ public BodyPart(BodyPartPrototype data)
+ {
+ SurgeryData = null!;
+ Properties = new HashSet();
+ Name = null!;
+ Plural = null!;
+ RSIPath = null!;
+ RSIState = null!;
+ RSIMap = null!;
+ Damage = null!;
+ Resistances = null!;
+
+ LoadFromPrototype(data);
+ }
+
+ ///
+ /// The body that this body part is in, if any.
+ ///
+ [ViewVariables]
+ public BodyManagerComponent? Body
+ {
+ get => _body;
+ set
+ {
+ var old = _body;
+ _body = value;
+
+ if (value == null && old != null)
+ {
+ foreach (var mechanism in Mechanisms)
+ {
+ mechanism.RemovedFromBody(old);
+ }
+ }
+ else
+ {
+ foreach (var mechanism in Mechanisms)
+ {
+ mechanism.InstalledIntoBody();
+ }
+ }
+ }
+ }
+
+ ///
+ /// The class currently representing this BodyPart's
+ /// surgery status.
+ ///
+ [ViewVariables] private SurgeryData SurgeryData { get; set; }
+
+ ///
+ /// How much space is currently taken up by Mechanisms in this BodyPart.
+ ///
+ [ViewVariables] private int SizeUsed { get; set; }
+
+ ///
+ /// List of properties, allowing for additional
+ /// data classes to be attached to a limb, such as a "length" class to an arm.
+ ///
+ [ViewVariables]
+ private HashSet Properties { get; }
+
+ ///
+ /// The name of this , often displayed to the user.
+ /// For example, it could be named "advanced robotic arm".
+ ///
+ [ViewVariables]
+ public string Name { get; private set; }
+
+ ///
+ /// Plural version of this name.
+ ///
+ [ViewVariables]
+ public string Plural { get; private set; }
+
+ ///
+ /// Path to the RSI that represents this .
+ ///
+ [ViewVariables]
+ public string RSIPath { get; private set; }
+
+ ///
+ /// RSI state that represents this .
+ ///
+ [ViewVariables]
+ public string RSIState { get; private set; }
+
+ ///
+ /// RSI map keys that this body part changes on the sprite.
+ ///
+ [ViewVariables]
+ public Enum? RSIMap { get; set; }
+
+ ///
+ /// RSI color of this body part.
+ ///
+ // TODO: SpriteComponent rework
+ public Color? RSIColor { get; set; }
+
+ ///
+ /// that this is considered
+ /// to be.
+ /// For example, .
+ ///
+ [ViewVariables]
+ public BodyPartType PartType { get; private set; }
+
+ ///
+ /// Determines many things: how many mechanisms can be fit inside this
+ /// , whether a body can fit through tiny crevices, etc.
+ ///
+ [ViewVariables]
+ private int Size { get; set; }
+
+ ///
+ /// Max HP of this .
+ ///
+ [ViewVariables]
+ public int MaxDurability { get; private set; }
+
+ ///
+ /// Current HP of this based on sum of all damage types.
+ ///
+ [ViewVariables]
+ public int CurrentDurability => MaxDurability - Damage.TotalDamage;
+
+ // TODO: Individual body part damage
+ ///
+ /// Current damage dealt to this .
+ ///
+ [ViewVariables]
+ public DamageContainer Damage { get; private set; }
+
+ ///
+ /// Armor of this against damages.
+ ///
+ [ViewVariables]
+ public ResistanceSet Resistances { get; private set; }
+
+ ///
+ /// At what HP this destroyed.
+ ///
+ [ViewVariables]
+ public int DestroyThreshold { get; private set; }
+
+ ///
+ /// What types of BodyParts this can easily attach to.
+ /// For the most part, most limbs aren't universal and require extra work to
+ /// attach between types.
+ ///
+ [ViewVariables]
+ public BodyPartCompatibility Compatibility { get; private set; }
+
+ ///
+ /// Set of all currently inside this
+ /// .
+ ///
+ [ViewVariables]
+ public IReadOnlyCollection Mechanisms => _mechanisms;
+
+ ///
+ /// This method is called by
+ /// before is called.
+ ///
+ public void PreMetabolism(float frameTime)
+ {
+ foreach (var mechanism in Mechanisms)
+ {
+ mechanism.PreMetabolism(frameTime);
+ }
+ }
+
+ ///
+ /// This method is called by
+ /// after is called.
+ ///
+ public void PostMetabolism(float frameTime)
+ {
+ foreach (var mechanism in Mechanisms)
+ {
+ mechanism.PreMetabolism(frameTime);
+ }
+ }
+
+ ///
+ /// Attempts to add the given .
+ ///
+ ///
+ /// True if a of that type doesn't exist,
+ /// false otherwise.
+ ///
+ public bool TryAddProperty(BodyPartProperty property)
+ {
+ if (HasProperty(property.GetType()))
+ {
+ return false;
+ }
+
+ Properties.Add(property);
+ return true;
+ }
+
+ ///
+ /// Attempts to retrieve the given type.
+ /// The resulting will be null if unsuccessful.
+ ///
+ /// The property if found, null otherwise.
+ /// The type of the property to find.
+ /// True if successful, false otherwise.
+ public bool TryGetProperty(out T property)
+ {
+ property = (T) Properties.First(x => x.GetType() == typeof(T));
+ return property != null;
+ }
+
+ ///
+ /// Attempts to retrieve the given type.
+ /// The resulting will be null if unsuccessful.
+ ///
+ /// True if successful, false otherwise.
+ public bool TryGetProperty(Type propertyType, out BodyPartProperty property)
+ {
+ property = (BodyPartProperty) Properties.First(x => x.GetType() == propertyType);
+ return property != null;
+ }
+
+ ///
+ /// Checks if the given type is on this .
+ ///
+ ///
+ /// The subtype of to look for.
+ ///
+ ///
+ /// True if this has a property of type
+ /// , false otherwise.
+ ///
+ public bool HasProperty() where T : BodyPartProperty
+ {
+ return Properties.Count(x => x.GetType() == typeof(T)) > 0;
+ }
+
+ ///
+ /// Checks if a subtype of is on this
+ /// .
+ ///
+ ///
+ /// The subtype of to look for.
+ ///
+ ///
+ /// True if this has a property of type
+ /// , false otherwise.
+ ///
+ public bool HasProperty(Type propertyType)
+ {
+ return Properties.Count(x => x.GetType() == propertyType) > 0;
+ }
+
+ ///
+ /// Checks if another can be connected to this one.
+ ///
+ /// The part to connect.
+ /// True if it can be connected, false otherwise.
+ public bool CanAttachBodyPart(BodyPart toBeConnected)
+ {
+ return SurgeryData.CanAttachBodyPart(toBeConnected);
+ }
+
+ ///
+ /// Checks if a can be installed on this
+ /// .
+ ///
+ /// True if it can be installed, false otherwise.
+ public bool CanInstallMechanism(Mechanism mechanism)
+ {
+ return SizeUsed + mechanism.Size <= Size &&
+ SurgeryData.CanInstallMechanism(mechanism);
+ }
+
+ ///
+ /// Tries to install a mechanism onto this body part.
+ /// Call instead if you want to
+ /// easily install an with a
+ /// .
+ ///
+ /// The mechanism to try to install.
+ ///
+ /// True if successful, false if there was an error
+ /// (e.g. not enough room in ).
+ ///
+ private bool TryInstallMechanism(Mechanism mechanism)
+ {
+ if (!CanInstallMechanism(mechanism))
+ {
+ return false;
+ }
+
+ AddMechanism(mechanism);
+
+ return true;
+ }
+
+ ///
+ /// Tries to install a into this
+ /// , potentially deleting the dropped
+ /// .
+ ///
+ /// The mechanism to install.
+ ///
+ /// True if successful, false if there was an error
+ /// (e.g. not enough room in ).
+ ///
+ public bool TryInstallDroppedMechanism(DroppedMechanismComponent droppedMechanism)
+ {
+ if (!TryInstallMechanism(droppedMechanism.ContainedMechanism))
+ {
+ return false; //Installing the mechanism failed for some reason.
+ }
+
+ droppedMechanism.Owner.Delete();
+ return true;
+ }
+
+ ///
+ /// Tries to remove the given reference from
+ /// this .
+ ///
+ ///
+ /// The newly spawned , or null
+ /// if there was an error in spawning the entity or removing the mechanism.
+ ///
+ public bool TryDropMechanism(IEntity dropLocation, Mechanism mechanismTarget,
+ [NotNullWhen(true)] out DroppedMechanismComponent dropped)
+ {
+ dropped = null!;
+
+ if (!_mechanisms.Remove(mechanismTarget))
+ {
+ return false;
+ }
+
+ SizeUsed -= mechanismTarget.Size;
+
+ var entityManager = IoCManager.Resolve();
+ var position = dropLocation.Transform.GridPosition;
+ var mechanismEntity = entityManager.SpawnEntity("BaseDroppedMechanism", position);
+
+ dropped = mechanismEntity.GetComponent();
+ dropped.InitializeDroppedMechanism(mechanismTarget);
+
+ return true;
+ }
+
+ ///
+ /// Tries to destroy the given in this
+ /// . Does NOT spawn a dropped entity.
+ ///
+ ///
+ /// Tries to destroy the given in this
+ /// .
+ ///
+ /// The mechanism to destroy.
+ /// True if successful, false otherwise.
+ public bool DestroyMechanism(Mechanism mechanismTarget)
+ {
+ if (!RemoveMechanism(mechanismTarget))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Checks if the given can be used on
+ /// the current state of this .
+ ///
+ /// True if it can be used, false otherwise.
+ public bool SurgeryCheck(SurgeryType toolType)
+ {
+ return SurgeryData.CheckSurgery(toolType);
+ }
+
+ ///
+ /// Attempts to perform surgery on this with the given
+ /// tool.
+ ///
+ /// True if successful, false if there was an error.
+ public bool AttemptSurgery(SurgeryType toolType, IBodyPartContainer target, ISurgeon surgeon, IEntity performer)
+ {
+ return SurgeryData.PerformSurgery(toolType, target, surgeon, performer);
+ }
+
+ private void AddMechanism(Mechanism mechanism)
+ {
+ DebugTools.AssertNotNull(mechanism);
+
+ _mechanisms.Add(mechanism);
+ SizeUsed += mechanism.Size;
+ mechanism.Part = this;
+
+ mechanism.EnsureInitialize();
+
+ if (Body == null)
+ {
+ return;
+ }
+
+ if (!Body.Template.MechanismLayers.TryGetValue(mechanism.Id, out var mapString))
+ {
+ return;
+ }
+
+ if (!IoCManager.Resolve().TryParseEnumReference(mapString, out var @enum))
+ {
+ Logger.Warning($"Template {Body.Template.Name} has an invalid RSI map key {mapString} for mechanism {mechanism.Id}.");
+ return;
+ }
+
+ var message = new MechanismSpriteAddedMessage(@enum);
+
+ Body.Owner.SendNetworkMessage(Body, message);
+ }
+
+ ///
+ /// Tries to remove the given from this
+ /// .
+ ///
+ /// The mechanism to remove.
+ /// True if it was removed, false otherwise.
+ private bool RemoveMechanism(Mechanism mechanism)
+ {
+ DebugTools.AssertNotNull(mechanism);
+
+ if (!_mechanisms.Remove(mechanism))
+ {
+ return false;
+ }
+
+ SizeUsed -= mechanism.Size;
+ mechanism.Part = null;
+
+ if (Body == null)
+ {
+ return true;
+ }
+
+ if (!Body.Template.MechanismLayers.TryGetValue(mechanism.Id, out var mapString))
+ {
+ return true;
+ }
+
+ if (!IoCManager.Resolve().TryParseEnumReference(mapString, out var @enum))
+ {
+ Logger.Warning($"Template {Body.Template.Name} has an invalid RSI map key {mapString} for mechanism {mechanism.Id}.");
+ return true;
+ }
+
+ var message = new MechanismSpriteRemovedMessage(@enum);
+
+ Body.Owner.SendNetworkMessage(Body, message);
+
+ return true;
+ }
+
+ ///
+ /// Loads the given .
+ /// Current data on this will be overwritten!
+ ///
+ protected virtual void LoadFromPrototype(BodyPartPrototype data)
+ {
+ var prototypeManager = IoCManager.Resolve();
+
+ Name = data.Name;
+ Plural = data.Plural;
+ PartType = data.PartType;
+ RSIPath = data.RSIPath;
+ RSIState = data.RSIState;
+ MaxDurability = data.Durability;
+
+ if (!prototypeManager.TryIndex(data.DamageContainerPresetId,
+ out DamageContainerPrototype damageContainerData))
+ {
+ throw new InvalidOperationException(
+ $"No {nameof(DamageContainerPrototype)} found with id {data.DamageContainerPresetId}");
+ }
+
+ Damage = new DamageContainer(OnHealthChanged, damageContainerData);
+
+ if (!prototypeManager.TryIndex(data.ResistanceSetId, out ResistanceSetPrototype resistancesData))
+ {
+ throw new InvalidOperationException(
+ $"No {nameof(ResistanceSetPrototype)} found with id {data.ResistanceSetId}");
+ }
+
+ Resistances = new ResistanceSet(resistancesData);
+ Size = data.Size;
+ Compatibility = data.Compatibility;
+
+ Properties.Clear();
+ Properties.UnionWith(data.Properties);
+
+ var surgeryDataType = Type.GetType(data.SurgeryDataName);
+
+ if (surgeryDataType == null)
+ {
+ throw new InvalidOperationException($"No {nameof(Surgery.SurgeryData)} found with name {data.SurgeryDataName}");
+ }
+
+ if (!surgeryDataType.IsSubclassOf(typeof(SurgeryData)))
+ {
+ throw new InvalidOperationException(
+ $"Class {data.SurgeryDataName} is not a subtype of {nameof(Surgery.SurgeryData)} with id {data.ID}");
+ }
+
+ SurgeryData = IoCManager.Resolve().CreateInstance(surgeryDataType, new object[] {this});
+
+ foreach (var id in data.Mechanisms)
+ {
+ if (!prototypeManager.TryIndex(id, out MechanismPrototype mechanismData))
+ {
+ throw new InvalidOperationException($"No {nameof(MechanismPrototype)} found with id {id}");
+ }
+
+ var mechanism = new Mechanism(mechanismData);
+
+ AddMechanism(mechanism);
+ }
+ }
+
+ private void OnHealthChanged(List changes)
+ {
+ // TODO
+ }
+
+ public bool SpawnDropped([NotNullWhen(true)] out IEntity dropped)
+ {
+ dropped = default!;
+
+ if (Body == null)
+ {
+ return false;
+ }
+
+ dropped = IoCManager.Resolve().SpawnEntity("BaseDroppedBodyPart", Body.Owner.Transform.GridPosition);
+
+ dropped.GetComponent().TransferBodyPartData(this);
+
+ return true;
+ }
+ }
+}
diff --git a/Content.Server/Body/BodyPreset.cs b/Content.Server/Body/BodyPreset.cs
new file mode 100644
index 0000000000..12e67c53e9
--- /dev/null
+++ b/Content.Server/Body/BodyPreset.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using Content.Shared.Body.Part;
+using Content.Shared.Body.Preset;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Body
+{
+ ///
+ /// Stores data on what should
+ /// fill a BodyTemplate.
+ /// Used for loading complete body presets, like a "basic human" with all
+ /// human limbs.
+ ///
+ public class BodyPreset
+ {
+ public BodyPreset(BodyPresetPrototype data)
+ {
+ LoadFromPrototype(data);
+ }
+
+ [ViewVariables] public string Name { get; private set; }
+
+ ///
+ /// Maps a template slot to the ID of the that should
+ /// fill it. E.g. "right arm" : "BodyPart.arm.basic_human".
+ ///
+ [ViewVariables]
+ public Dictionary PartIDs { get; private set; }
+
+ protected virtual void LoadFromPrototype(BodyPresetPrototype data)
+ {
+ Name = data.Name;
+ PartIDs = data.PartIDs;
+ }
+ }
+}
diff --git a/Content.Server/Body/BodyTemplate.cs b/Content.Server/Body/BodyTemplate.cs
new file mode 100644
index 0000000000..ae32c02688
--- /dev/null
+++ b/Content.Server/Body/BodyTemplate.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.GameObjects.Components.Body;
+using Content.Shared.Body.Template;
+using Content.Shared.GameObjects.Components.Body;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Body
+{
+ ///
+ /// This class is a data capsule representing the standard format of a
+ /// .
+ /// For instance, the "humanoid" BodyTemplate defines two arms, each connected to
+ /// a torso and so on.
+ /// Capable of loading data from a .
+ ///
+ public class BodyTemplate
+ {
+ public BodyTemplate()
+ {
+ Name = "empty";
+ CenterSlot = "";
+ Slots = new Dictionary();
+ Connections = new Dictionary>();
+ Layers = new Dictionary();
+ MechanismLayers = new Dictionary();
+ }
+
+ public BodyTemplate(BodyTemplatePrototype data)
+ {
+ LoadFromPrototype(data);
+ }
+
+ [ViewVariables] public string Name { get; private set; }
+
+ ///
+ /// The name of the center BodyPart. For humans, this is set to "torso".
+ /// Used in many calculations.
+ ///
+ [ViewVariables]
+ public string CenterSlot { get; set; }
+
+ ///
+ /// Maps all parts on this template to its BodyPartType.
+ /// For instance, "right arm" is mapped to "BodyPartType.arm" on the humanoid
+ /// template.
+ ///
+ [ViewVariables]
+ public Dictionary Slots { get; private set; }
+
+ ///
+ /// Maps limb name to the list of their connections to other limbs.
+ /// For instance, on the humanoid template "torso" is mapped to a list
+ /// containing "right arm", "left arm", "left leg", and "right leg".
+ /// This is mapped both ways during runtime, but in the prototype only one
+ /// way has to be defined, i.e., "torso" to "left arm" will automatically
+ /// map "left arm" to "torso".
+ ///
+ [ViewVariables]
+ public Dictionary> Connections { get; private set; }
+
+ [ViewVariables]
+ public Dictionary Layers { get; private set; }
+
+ [ViewVariables]
+ public Dictionary MechanismLayers { get; private set; }
+
+ public bool Equals(BodyTemplate other)
+ {
+ return GetHashCode() == other.GetHashCode();
+ }
+
+ ///
+ /// Checks if the given slot exists in this .
+ ///
+ /// True if it does, false otherwise.
+ public bool SlotExists(string slotName)
+ {
+ return Slots.Keys.Any(slot => slot == slotName);
+ }
+
+ ///
+ /// Calculates the hash code for this instance of .
+ /// It does not matter in which order the Connections or Slots are defined.
+ ///
+ ///
+ /// An integer unique to this 's layout.
+ ///
+ public override int GetHashCode()
+ {
+ var slotsHash = 0;
+ var connectionsHash = 0;
+
+ foreach (var (key, value) in Slots)
+ {
+ var slot = key.GetHashCode();
+ slot = HashCode.Combine(slot, value.GetHashCode());
+ slotsHash ^= slot;
+ }
+
+ var connections = new List();
+ foreach (var (key, value) in Connections)
+ {
+ foreach (var targetBodyPart in value)
+ {
+ var connection = key.GetHashCode() ^ targetBodyPart.GetHashCode();
+ if (!connections.Contains(connection))
+ {
+ connections.Add(connection);
+ }
+ }
+ }
+
+ foreach (var connection in connections)
+ {
+ connectionsHash ^= connection;
+ }
+
+ // One of the unit tests considers 0 to be an error, but it will be 0 if
+ // the BodyTemplate is empty, so let's shift that up to 1.
+ var hash = HashCode.Combine(
+ CenterSlot.GetHashCode(),
+ slotsHash,
+ connectionsHash);
+
+ if (hash == 0)
+ {
+ hash++;
+ }
+
+ return hash;
+ }
+
+ protected virtual void LoadFromPrototype(BodyTemplatePrototype data)
+ {
+ Name = data.Name;
+ CenterSlot = data.CenterSlot;
+ Slots = data.Slots;
+ Connections = data.Connections;
+ Layers = data.Layers;
+ MechanismLayers = data.MechanismLayers;
+ }
+ }
+}
diff --git a/Content.Server/Body/IBodyPartContainer.cs b/Content.Server/Body/IBodyPartContainer.cs
new file mode 100644
index 0000000000..fdc4c402e9
--- /dev/null
+++ b/Content.Server/Body/IBodyPartContainer.cs
@@ -0,0 +1,19 @@
+using Content.Server.Body.Surgery;
+using Content.Server.GameObjects.Components.Body;
+
+namespace Content.Server.Body
+{
+ ///
+ /// Making a class inherit from this interface allows you to do many things with
+ /// it in the class.
+ /// This includes passing it as an argument to a
+ /// delegate, as to later typecast it back
+ /// to the original class type.
+ /// Every BodyPart also needs an to be its parent
+ /// (i.e. the holds many ,
+ /// each of which have an upward reference to it).
+ ///
+ public interface IBodyPartContainer
+ {
+ }
+}
diff --git a/Content.Server/Body/Mechanisms/Behaviors/BrainBehavior.cs b/Content.Server/Body/Mechanisms/Behaviors/BrainBehavior.cs
new file mode 100644
index 0000000000..869534803e
--- /dev/null
+++ b/Content.Server/Body/Mechanisms/Behaviors/BrainBehavior.cs
@@ -0,0 +1,9 @@
+namespace Content.Server.Body.Mechanisms.Behaviors
+{
+ ///
+ /// The behaviors of a brain, inhabitable by a player.
+ ///
+ public class BrainBehavior : MechanismBehavior
+ {
+ }
+}
diff --git a/Content.Server/Body/Mechanisms/Behaviors/HeartBehavior.cs b/Content.Server/Body/Mechanisms/Behaviors/HeartBehavior.cs
new file mode 100644
index 0000000000..51366583bd
--- /dev/null
+++ b/Content.Server/Body/Mechanisms/Behaviors/HeartBehavior.cs
@@ -0,0 +1,38 @@
+#nullable enable
+using System;
+using Content.Server.Body.Network;
+using Content.Server.GameObjects.Components.Body.Circulatory;
+using JetBrains.Annotations;
+
+namespace Content.Server.Body.Mechanisms.Behaviors
+{
+ [UsedImplicitly]
+ public class HeartBehavior : MechanismBehavior
+ {
+ private float _accumulatedFrameTime;
+
+ protected override Type? Network => typeof(CirculatoryNetwork);
+
+ public override void PreMetabolism(float frameTime)
+ {
+ // TODO do between pre and metabolism
+ base.PreMetabolism(frameTime);
+
+ if (Mechanism.Body == null ||
+ !Mechanism.Body.Owner.TryGetComponent(out BloodstreamComponent bloodstream))
+ {
+ return;
+ }
+
+ // Update at most once per second
+ _accumulatedFrameTime += frameTime;
+
+ // TODO: Move/accept/process bloodstream reagents only when the heart is pumping
+ if (_accumulatedFrameTime >= 1)
+ {
+ // bloodstream.Update(_accumulatedFrameTime);
+ _accumulatedFrameTime -= 1;
+ }
+ }
+ }
+}
diff --git a/Content.Server/Body/Mechanisms/Behaviors/LungBehavior.cs b/Content.Server/Body/Mechanisms/Behaviors/LungBehavior.cs
new file mode 100644
index 0000000000..675cc37e76
--- /dev/null
+++ b/Content.Server/Body/Mechanisms/Behaviors/LungBehavior.cs
@@ -0,0 +1,27 @@
+#nullable enable
+using System;
+using Content.Server.Body.Network;
+using Content.Server.GameObjects.Components.Body.Respiratory;
+using JetBrains.Annotations;
+
+namespace Content.Server.Body.Mechanisms.Behaviors
+{
+ [UsedImplicitly]
+ public class LungBehavior : MechanismBehavior
+ {
+ protected override Type? Network => typeof(RespiratoryNetwork);
+
+ public override void PreMetabolism(float frameTime)
+ {
+ base.PreMetabolism(frameTime);
+
+ if (Mechanism.Body == null ||
+ !Mechanism.Body.Owner.TryGetComponent(out LungComponent lung))
+ {
+ return;
+ }
+
+ lung.Update(frameTime);
+ }
+ }
+}
diff --git a/Content.Server/Body/Mechanisms/Behaviors/MechanismBehavior.cs b/Content.Server/Body/Mechanisms/Behaviors/MechanismBehavior.cs
new file mode 100644
index 0000000000..77c3d5e982
--- /dev/null
+++ b/Content.Server/Body/Mechanisms/Behaviors/MechanismBehavior.cs
@@ -0,0 +1,185 @@
+#nullable enable
+using System;
+using Content.Server.GameObjects.Components.Body;
+using Content.Server.GameObjects.Components.Metabolism;
+
+namespace Content.Server.Body.Mechanisms.Behaviors
+{
+ ///
+ /// The behaviors a mechanism performs.
+ ///
+ public abstract class MechanismBehavior
+ {
+ private bool Initialized { get; set; }
+
+ private bool Removed { get; set; }
+
+ ///
+ /// The network, if any, that this behavior forms when its mechanism is
+ /// added and destroys when its mechanism is removed.
+ ///
+ protected virtual Type? Network { get; } = null;
+
+ ///
+ /// Upward reference to the parent that this
+ /// behavior is attached to.
+ ///
+ protected Mechanism Mechanism { get; private set; } = null!;
+
+ ///
+ /// Called by a to initialize this behavior.
+ ///
+ /// The mechanism that owns this behavior.
+ ///
+ /// If the mechanism has already been initialized.
+ ///
+ public void Initialize(Mechanism mechanism)
+ {
+ if (Initialized)
+ {
+ throw new InvalidOperationException("This mechanism has already been initialized.");
+ }
+
+ Mechanism = mechanism;
+
+ Initialize();
+
+ if (Mechanism.Body != null)
+ {
+ OnInstalledIntoBody();
+ }
+
+ if (Mechanism.Part != null)
+ {
+ OnInstalledIntoPart();
+ }
+
+ Initialized = true;
+ }
+
+ ///
+ /// Called when a behavior is removed from a .
+ ///
+ public void Remove()
+ {
+ OnRemove();
+ TryRemoveNetwork(Mechanism.Body);
+
+ Mechanism = null!;
+ Removed = true;
+ }
+
+ ///
+ /// Called when the containing is attached to a
+ /// .
+ /// For instance, attaching a head to a body will call this on the brain inside.
+ ///
+ public void InstalledIntoBody()
+ {
+ TryAddNetwork();
+ OnInstalledIntoBody();
+ }
+
+ ///
+ /// Called when the parent is
+ /// installed into a .
+ /// For instance, putting a brain into an empty head.
+ ///
+ public void InstalledIntoPart()
+ {
+ TryAddNetwork();
+ OnInstalledIntoPart();
+ }
+
+ ///
+ /// Called when the containing is removed from a
+ /// .
+ /// For instance, cutting off ones head will call this on the brain inside.
+ ///
+ public void RemovedFromBody(BodyManagerComponent old)
+ {
+ OnRemovedFromBody(old);
+ TryRemoveNetwork(old);
+ }
+
+ ///
+ /// Called when the parent is removed from a
+ /// .
+ /// For instance, taking a brain out of ones head.
+ ///
+ public void RemovedFromPart(BodyPart old)
+ {
+ OnRemovedFromPart(old);
+ TryRemoveNetwork(old.Body);
+ }
+
+ private void TryAddNetwork()
+ {
+ if (Network != null)
+ {
+ Mechanism.Body?.EnsureNetwork(Network);
+ }
+ }
+
+ private void TryRemoveNetwork(BodyManagerComponent? body)
+ {
+ if (Network != null)
+ {
+ body?.RemoveNetwork(Network);
+ }
+ }
+
+ ///
+ /// Called by when this behavior is first initialized.
+ ///
+ protected virtual void Initialize() { }
+
+ protected virtual void OnRemove() { }
+
+ ///
+ /// Called when the containing is attached to a
+ /// .
+ /// For instance, attaching a head to a body will call this on the brain inside.
+ ///
+ protected virtual void OnInstalledIntoBody() { }
+
+ ///
+ /// Called when the parent is
+ /// installed into a .
+ /// For instance, putting a brain into an empty head.
+ ///
+ protected virtual void OnInstalledIntoPart() { }
+
+ ///
+ /// Called when the containing is removed from a
+ /// .
+ /// For instance, cutting off ones head will call this on the brain inside.
+ ///
+ protected virtual void OnRemovedFromBody(BodyManagerComponent old) { }
+
+ ///
+ /// Called when the parent is removed from a
+ /// .
+ /// For instance, taking a brain out of ones head.
+ ///
+ protected virtual void OnRemovedFromPart(BodyPart old) { }
+
+ ///
+ /// Called every update when this behavior is connected to a
+ /// , but not while in a
+ /// or
+ /// ,
+ /// before is called.
+ ///
+ public virtual void PreMetabolism(float frameTime) { }
+
+ ///
+ /// Called every update when this behavior is connected to a
+ /// , but not while in a
+ /// or
+ /// ,
+ /// after is called.
+ ///
+ public virtual void PostMetabolism(float frameTime) { }
+ }
+}
diff --git a/Content.Server/Body/Mechanisms/Behaviors/StomachBehavior.cs b/Content.Server/Body/Mechanisms/Behaviors/StomachBehavior.cs
new file mode 100644
index 0000000000..ae1e17b49b
--- /dev/null
+++ b/Content.Server/Body/Mechanisms/Behaviors/StomachBehavior.cs
@@ -0,0 +1,36 @@
+#nullable enable
+using System;
+using Content.Server.Body.Network;
+using Content.Server.GameObjects.Components.Body.Digestive;
+using JetBrains.Annotations;
+
+namespace Content.Server.Body.Mechanisms.Behaviors
+{
+ [UsedImplicitly]
+ public class StomachBehavior : MechanismBehavior
+ {
+ private float _accumulatedFrameTime;
+
+ protected override Type? Network => typeof(DigestiveNetwork);
+
+ public override void PreMetabolism(float frameTime)
+ {
+ base.PreMetabolism(frameTime);
+
+ if (Mechanism.Body == null ||
+ !Mechanism.Body.Owner.TryGetComponent(out StomachComponent stomach))
+ {
+ return;
+ }
+
+ // Update at most once per second
+ _accumulatedFrameTime += frameTime;
+
+ if (_accumulatedFrameTime >= 1)
+ {
+ stomach.Update(_accumulatedFrameTime);
+ _accumulatedFrameTime -= 1;
+ }
+ }
+ }
+}
diff --git a/Content.Server/Body/Mechanisms/Mechanism.cs b/Content.Server/Body/Mechanisms/Mechanism.cs
new file mode 100644
index 0000000000..9c88b0425b
--- /dev/null
+++ b/Content.Server/Body/Mechanisms/Mechanism.cs
@@ -0,0 +1,249 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using Content.Server.Body.Mechanisms.Behaviors;
+using Content.Server.GameObjects.Components.Body;
+using Content.Server.GameObjects.Components.Metabolism;
+using Content.Shared.Body.Mechanism;
+using Content.Shared.GameObjects.Components.Body;
+using Robust.Shared.IoC;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Body.Mechanisms
+{
+ ///
+ /// Data class representing a persistent item inside a .
+ /// This includes livers, eyes, cameras, brains, explosive implants,
+ /// binary communicators, and other things.
+ ///
+ public class Mechanism
+ {
+ private BodyPart? _part;
+
+ public Mechanism(MechanismPrototype data)
+ {
+ Data = data;
+ Id = null!;
+ Name = null!;
+ Description = null!;
+ ExamineMessage = null!;
+ RSIPath = null!;
+ RSIState = null!;
+ Behaviors = new List();
+ }
+
+ [ViewVariables] private bool Initialized { get; set; }
+
+ [ViewVariables] private MechanismPrototype Data { get; set; }
+
+ [ViewVariables] public string Id { get; private set; }
+
+ [ViewVariables] public string Name { get; set; }
+
+ ///
+ /// Professional description of the .
+ ///
+ [ViewVariables]
+ public string Description { get; set; }
+
+ ///
+ /// The message to display upon examining a mob with this Mechanism installed.
+ /// If the string is empty (""), no message will be displayed.
+ ///
+ [ViewVariables]
+ public string ExamineMessage { get; set; }
+
+ ///
+ /// Path to the RSI that represents this .
+ ///
+ [ViewVariables]
+ public string RSIPath { get; set; }
+
+ ///
+ /// RSI state that represents this .
+ ///
+ [ViewVariables]
+ public string RSIState { get; set; }
+
+ ///
+ /// Max HP of this .
+ ///
+ [ViewVariables]
+ public int MaxDurability { get; set; }
+
+ ///
+ /// Current HP of this .
+ ///
+ [ViewVariables]
+ public int CurrentDurability { get; set; }
+
+ ///
+ /// At what HP this is completely destroyed.
+ ///
+ [ViewVariables]
+ public int DestroyThreshold { get; set; }
+
+ ///
+ /// Armor of this against attacks.
+ ///
+ [ViewVariables]
+ public int Resistance { get; set; }
+
+ ///
+ /// Determines a handful of things - mostly whether this
+ /// can fit into a .
+ ///
+ [ViewVariables]
+ public int Size { get; set; }
+
+ ///
+ /// What kind of this can be
+ /// easily installed into.
+ ///
+ [ViewVariables]
+ public BodyPartCompatibility Compatibility { get; set; }
+
+ ///
+ /// The behaviors that this performs.
+ ///
+ [ViewVariables]
+ private List Behaviors { get; }
+
+ public BodyManagerComponent? Body => Part?.Body;
+
+ public BodyPart? Part
+ {
+ get => _part;
+ set
+ {
+ var old = _part;
+ _part = value;
+
+ if (value == null && old != null)
+ {
+ foreach (var behavior in Behaviors)
+ {
+ behavior.RemovedFromPart(old);
+ }
+ }
+ else
+ {
+ foreach (var behavior in Behaviors)
+ {
+ behavior.InstalledIntoPart();
+ }
+ }
+ }
+ }
+
+ public void EnsureInitialize()
+ {
+ if (Initialized)
+ {
+ return;
+ }
+
+ LoadFromPrototype(Data);
+ Initialized = true;
+ }
+
+ ///
+ /// Loads the given .
+ /// Current data on this will be overwritten!
+ ///
+ private void LoadFromPrototype(MechanismPrototype data)
+ {
+ Data = data;
+ Id = data.ID;
+ Name = data.Name;
+ Description = data.Description;
+ ExamineMessage = data.ExamineMessage;
+ RSIPath = data.RSIPath;
+ RSIState = data.RSIState;
+ MaxDurability = data.Durability;
+ CurrentDurability = MaxDurability;
+ DestroyThreshold = data.DestroyThreshold;
+ Resistance = data.Resistance;
+ Size = data.Size;
+ Compatibility = data.Compatibility;
+
+ foreach (var behavior in Behaviors.ToArray())
+ {
+ RemoveBehavior(behavior);
+ }
+
+ foreach (var mechanismBehaviorName in data.BehaviorClasses)
+ {
+ var mechanismBehaviorType = Type.GetType(mechanismBehaviorName);
+
+ if (mechanismBehaviorType == null)
+ {
+ throw new InvalidOperationException(
+ $"No {nameof(MechanismBehavior)} found with name {mechanismBehaviorName}");
+ }
+
+ if (!mechanismBehaviorType.IsSubclassOf(typeof(MechanismBehavior)))
+ {
+ throw new InvalidOperationException(
+ $"Class {mechanismBehaviorName} is not a subtype of {nameof(MechanismBehavior)} for mechanism prototype {data.ID}");
+ }
+
+ var newBehavior = IoCManager.Resolve().CreateInstance(mechanismBehaviorType);
+
+ AddBehavior(newBehavior);
+ }
+ }
+
+ public void InstalledIntoBody()
+ {
+ foreach (var behavior in Behaviors)
+ {
+ behavior.InstalledIntoBody();
+ }
+ }
+
+ public void RemovedFromBody(BodyManagerComponent old)
+ {
+ foreach (var behavior in Behaviors)
+ {
+ behavior.RemovedFromBody(old);
+ }
+ }
+
+ ///
+ /// This method is called by before
+ /// is called.
+ ///
+ public void PreMetabolism(float frameTime)
+ {
+ foreach (var behavior in Behaviors)
+ {
+ behavior.PreMetabolism(frameTime);
+ }
+ }
+
+ ///
+ /// This method is called by after
+ /// is called.
+ ///
+ public void PostMetabolism(float frameTime)
+ {
+ foreach (var behavior in Behaviors)
+ {
+ behavior.PostMetabolism(frameTime);
+ }
+ }
+
+ private void AddBehavior(MechanismBehavior behavior)
+ {
+ Behaviors.Add(behavior);
+ behavior.Initialize(this);
+ }
+
+ private bool RemoveBehavior(MechanismBehavior behavior)
+ {
+ behavior.Remove();
+ return Behaviors.Remove(behavior);
+ }
+ }
+}
diff --git a/Content.Server/Body/Network/BodyNetwork.cs b/Content.Server/Body/Network/BodyNetwork.cs
new file mode 100644
index 0000000000..911bfbde75
--- /dev/null
+++ b/Content.Server/Body/Network/BodyNetwork.cs
@@ -0,0 +1,76 @@
+using System;
+using Content.Server.GameObjects.Components.Body;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Serialization;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Body.Network
+{
+ ///
+ /// Represents a "network" such as a bloodstream or electrical power that
+ /// is coordinated throughout an entire .
+ ///
+ public abstract class BodyNetwork : IExposeData
+ {
+ [ViewVariables]
+ public abstract string Name { get; }
+
+ protected IEntity Owner { get; private set; }
+
+ public virtual void ExposeData(ObjectSerializer serializer) { }
+
+ public void OnAdd(IEntity entity)
+ {
+ Owner = entity;
+ OnAdd();
+ }
+
+ protected virtual void OnAdd() { }
+
+ public virtual void OnRemove() { }
+
+ ///
+ /// Called every update by .
+ ///
+ public virtual void Update(float frameTime) { }
+ }
+
+ public static class BodyNetworkExtensions
+ {
+ public static void TryAddNetwork(this IEntity entity, Type type)
+ {
+ if (!entity.TryGetComponent(out BodyManagerComponent body))
+ {
+ return;
+ }
+
+ body.EnsureNetwork(type);
+ }
+
+ public static void TryAddNetwork(this IEntity entity) where T : BodyNetwork
+ {
+ if (!entity.TryGetComponent(out BodyManagerComponent body))
+ {
+ return;
+ }
+
+ body.EnsureNetwork();
+ }
+
+ public static bool TryGetBodyNetwork(this IEntity entity, Type type, out BodyNetwork network)
+ {
+ network = null;
+
+ return entity.TryGetComponent(out BodyManagerComponent body) &&
+ body.TryGetNetwork(type, out network);
+ }
+
+ public static bool TryGetBodyNetwork(this IEntity entity, out T network) where T : BodyNetwork
+ {
+ entity.TryGetBodyNetwork(typeof(T), out var unCastNetwork);
+ network = (T) unCastNetwork;
+ return network != null;
+ }
+ }
+}
diff --git a/Content.Server/Body/Network/BodyNetworkFactory.cs b/Content.Server/Body/Network/BodyNetworkFactory.cs
new file mode 100644
index 0000000000..8b5ec0cc7f
--- /dev/null
+++ b/Content.Server/Body/Network/BodyNetworkFactory.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using Robust.Shared.Interfaces.Reflection;
+using Robust.Shared.IoC;
+
+namespace Content.Server.Body.Network
+{
+ public class BodyNetworkFactory : IBodyNetworkFactory
+ {
+ [Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
+ [Dependency] private readonly IReflectionManager _reflectionManager = default!;
+
+ ///
+ /// Mapping of body network names to their types.
+ ///
+ private readonly Dictionary _names = new Dictionary();
+
+ private void Register(Type type)
+ {
+ if (_names.ContainsValue(type))
+ {
+ throw new InvalidOperationException($"Type is already registered: {type}");
+ }
+
+ if (!type.IsSubclassOf(typeof(BodyNetwork)))
+ {
+ throw new InvalidOperationException($"{type} is not a subclass of {nameof(BodyNetwork)}");
+ }
+
+ var dummy = _typeFactory.CreateInstance(type);
+
+ if (dummy == null)
+ {
+ throw new NullReferenceException();
+ }
+
+ var name = dummy.Name;
+
+ if (name == null)
+ {
+ throw new NullReferenceException($"{type}'s name cannot be null.");
+ }
+
+ if (_names.ContainsKey(name))
+ {
+ throw new InvalidOperationException($"{name} is already registered.");
+ }
+
+ _names.Add(name, type);
+ }
+
+ public void DoAutoRegistrations()
+ {
+ var bodyNetwork = typeof(BodyNetwork);
+
+ foreach (var child in _reflectionManager.GetAllChildren(bodyNetwork))
+ {
+ Register(child);
+ }
+ }
+
+ public BodyNetwork GetNetwork(string name)
+ {
+ Type type;
+
+ try
+ {
+ type = _names[name];
+ }
+ catch (KeyNotFoundException)
+ {
+ throw new ArgumentException($"No {nameof(BodyNetwork)} exists with name {name}");
+ }
+
+ return _typeFactory.CreateInstance(type);
+ }
+
+ public BodyNetwork GetNetwork(Type type)
+ {
+ if (!_names.ContainsValue(type))
+ {
+ throw new ArgumentException($"{type} is not registered.");
+ }
+
+ return _typeFactory.CreateInstance(type);
+ }
+ }
+}
diff --git a/Content.Server/Body/Network/CirculatoryNetwork.cs b/Content.Server/Body/Network/CirculatoryNetwork.cs
new file mode 100644
index 0000000000..c67bc6e201
--- /dev/null
+++ b/Content.Server/Body/Network/CirculatoryNetwork.cs
@@ -0,0 +1,25 @@
+using Content.Server.GameObjects.Components.Body.Circulatory;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.Body.Network
+{
+ [UsedImplicitly]
+ public class CirculatoryNetwork : BodyNetwork
+ {
+ public override string Name => "Circulatory";
+
+ protected override void OnAdd()
+ {
+ Owner.EnsureComponent();
+ }
+
+ public override void OnRemove()
+ {
+ if (Owner.HasComponent())
+ {
+ Owner.RemoveComponent();
+ }
+ }
+ }
+}
diff --git a/Content.Server/Body/Network/DigestiveNetwork.cs b/Content.Server/Body/Network/DigestiveNetwork.cs
new file mode 100644
index 0000000000..6362b55072
--- /dev/null
+++ b/Content.Server/Body/Network/DigestiveNetwork.cs
@@ -0,0 +1,28 @@
+using Content.Server.GameObjects.Components.Body.Digestive;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.Body.Network
+{
+ ///
+ /// Represents the system that processes food, liquids, and the reagents inside them.
+ ///
+ [UsedImplicitly]
+ public class DigestiveNetwork : BodyNetwork
+ {
+ public override string Name => "Digestive";
+
+ protected override void OnAdd()
+ {
+ Owner.EnsureComponent();
+ }
+
+ public override void OnRemove()
+ {
+ if (Owner.HasComponent())
+ {
+ Owner.RemoveComponent();
+ }
+ }
+ }
+}
diff --git a/Content.Server/Body/Network/IBodyNetworkFactory.cs b/Content.Server/Body/Network/IBodyNetworkFactory.cs
new file mode 100644
index 0000000000..29d883f21b
--- /dev/null
+++ b/Content.Server/Body/Network/IBodyNetworkFactory.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Content.Server.Body.Network
+{
+ public interface IBodyNetworkFactory
+ {
+ void DoAutoRegistrations();
+
+ BodyNetwork GetNetwork(string name);
+
+ BodyNetwork GetNetwork(Type type);
+ }
+}
diff --git a/Content.Server/Body/Network/RespiratoryNetwork.cs b/Content.Server/Body/Network/RespiratoryNetwork.cs
new file mode 100644
index 0000000000..92c974f2e3
--- /dev/null
+++ b/Content.Server/Body/Network/RespiratoryNetwork.cs
@@ -0,0 +1,25 @@
+using Content.Server.GameObjects.Components.Body.Respiratory;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.Body.Network
+{
+ [UsedImplicitly]
+ public class RespiratoryNetwork : BodyNetwork
+ {
+ public override string Name => "Respiratory";
+
+ protected override void OnAdd()
+ {
+ Owner.EnsureComponent();
+ }
+
+ public override void OnRemove()
+ {
+ if (Owner.HasComponent())
+ {
+ Owner.RemoveComponent();
+ }
+ }
+ }
+}
diff --git a/Content.Server/Body/Surgery/BiologicalSurgeryData.cs b/Content.Server/Body/Surgery/BiologicalSurgeryData.cs
new file mode 100644
index 0000000000..37fbb57b29
--- /dev/null
+++ b/Content.Server/Body/Surgery/BiologicalSurgeryData.cs
@@ -0,0 +1,250 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.Body.Mechanisms;
+using Content.Server.GameObjects.Components.Body;
+using Content.Shared.GameObjects.Components.Body;
+using Content.Shared.Interfaces;
+using JetBrains.Annotations;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Localization;
+
+namespace Content.Server.Body.Surgery
+{
+ ///
+ /// Data class representing the surgery state of a biological entity.
+ ///
+ [UsedImplicitly]
+ public class BiologicalSurgeryData : SurgeryData
+ {
+ private readonly List _disconnectedOrgans = new List();
+
+ private bool _skinOpened;
+ private bool _skinRetracted;
+ private bool _vesselsClamped;
+
+ public BiologicalSurgeryData(BodyPart parent) : base(parent) { }
+
+ protected override SurgeryAction? GetSurgeryStep(SurgeryType toolType)
+ {
+ if (toolType == SurgeryType.Amputation)
+ {
+ return RemoveBodyPartSurgery;
+ }
+
+ if (!_skinOpened)
+ {
+ // Case: skin is normal.
+ if (toolType == SurgeryType.Incision)
+ {
+ return OpenSkinSurgery;
+ }
+ }
+ else if (!_vesselsClamped)
+ {
+ // Case: skin is opened, but not clamped.
+ switch (toolType)
+ {
+ case SurgeryType.VesselCompression:
+ return ClampVesselsSurgery;
+ case SurgeryType.Cauterization:
+ return CauterizeIncisionSurgery;
+ }
+ }
+ else if (!_skinRetracted)
+ {
+ // Case: skin is opened and clamped, but not retracted.
+ switch (toolType)
+ {
+ case SurgeryType.Retraction:
+ return RetractSkinSurgery;
+ case SurgeryType.Cauterization:
+ return CauterizeIncisionSurgery;
+ }
+ }
+ else
+ {
+ // Case: skin is fully open.
+ if (Parent.Mechanisms.Count > 0 &&
+ toolType == SurgeryType.VesselCompression)
+ {
+ if (_disconnectedOrgans.Except(Parent.Mechanisms).Count() != 0 ||
+ Parent.Mechanisms.Except(_disconnectedOrgans).Count() != 0)
+ {
+ return LoosenOrganSurgery;
+ }
+ }
+
+ if (_disconnectedOrgans.Count > 0 && toolType == SurgeryType.Incision)
+ {
+ return RemoveOrganSurgery;
+ }
+
+ if (toolType == SurgeryType.Cauterization)
+ {
+ return CauterizeIncisionSurgery;
+ }
+ }
+
+ return null;
+ }
+
+ public override string GetDescription(IEntity target)
+ {
+ var toReturn = "";
+
+ if (_skinOpened && !_vesselsClamped)
+ {
+ // Case: skin is opened, but not clamped.
+ toReturn += Loc.GetString("The skin on {0:their} {1} has an incision, but it is prone to bleeding.\n",
+ target, Parent.Name);
+ }
+ else if (_skinOpened && _vesselsClamped && !_skinRetracted)
+ {
+ // Case: skin is opened and clamped, but not retracted.
+ toReturn += Loc.GetString("The skin on {0:their} {1} has an incision, but it is not retracted.\n",
+ target, Parent.Name);
+ }
+ else if (_skinOpened && _vesselsClamped && _skinRetracted)
+ {
+ // Case: skin is fully open.
+ toReturn += Loc.GetString("There is an incision on {0:their} {1}.\n", target, Parent.Name);
+ foreach (var mechanism in _disconnectedOrgans)
+ {
+ toReturn += Loc.GetString("{0:their} {1} is loose.\n", target, mechanism.Name);
+ }
+ }
+
+ return toReturn;
+ }
+
+ public override bool CanInstallMechanism(Mechanism mechanism)
+ {
+ return _skinOpened && _vesselsClamped && _skinRetracted;
+ }
+
+ public override bool CanAttachBodyPart(BodyPart part)
+ {
+ return true;
+ // TODO: if a bodypart is disconnected, you should have to do some surgery to allow another bodypart to be attached.
+ }
+
+ private void OpenSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
+ {
+ performer.PopupMessage(performer, Loc.GetString("Cut open the skin..."));
+
+ // TODO do_after: Delay
+ _skinOpened = true;
+ }
+
+ private void ClampVesselsSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
+ {
+ performer.PopupMessage(performer, Loc.GetString("Clamp the vessels..."));
+
+ // TODO do_after: Delay
+ _vesselsClamped = true;
+ }
+
+ private void RetractSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
+ {
+ performer.PopupMessage(performer, Loc.GetString("Retract the skin..."));
+
+ // TODO do_after: Delay
+ _skinRetracted = true;
+ }
+
+ private void CauterizeIncisionSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
+ {
+ performer.PopupMessage(performer, Loc.GetString("Cauterize the incision..."));
+
+ // TODO do_after: Delay
+ _skinOpened = false;
+ _vesselsClamped = false;
+ _skinRetracted = false;
+ }
+
+ private void LoosenOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
+ {
+ if (Parent.Mechanisms.Count <= 0)
+ {
+ return;
+ }
+
+ var toSend = new List();
+ foreach (var mechanism in Parent.Mechanisms)
+ {
+ if (!_disconnectedOrgans.Contains(mechanism))
+ {
+ toSend.Add(mechanism);
+ }
+ }
+
+ if (toSend.Count > 0)
+ {
+ surgeon.RequestMechanism(toSend, LoosenOrganSurgeryCallback);
+ }
+ }
+
+ private void LoosenOrganSurgeryCallback(Mechanism target, IBodyPartContainer container, ISurgeon surgeon,
+ IEntity performer)
+ {
+ if (target == null || !Parent.Mechanisms.Contains(target))
+ {
+ return;
+ }
+
+ performer.PopupMessage(performer, Loc.GetString("Loosen the organ..."));
+
+ // TODO do_after: Delay
+ _disconnectedOrgans.Add(target);
+ }
+
+ private void RemoveOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
+ {
+ if (_disconnectedOrgans.Count <= 0)
+ {
+ return;
+ }
+
+ if (_disconnectedOrgans.Count == 1)
+ {
+ RemoveOrganSurgeryCallback(_disconnectedOrgans[0], container, surgeon, performer);
+ }
+ else
+ {
+ surgeon.RequestMechanism(_disconnectedOrgans, RemoveOrganSurgeryCallback);
+ }
+ }
+
+ private void RemoveOrganSurgeryCallback(Mechanism target, IBodyPartContainer container,
+ ISurgeon surgeon,
+ IEntity performer)
+ {
+ if (target == null || !Parent.Mechanisms.Contains(target))
+ {
+ return;
+ }
+
+ performer.PopupMessage(performer, Loc.GetString("Remove the organ..."));
+
+ // TODO do_after: Delay
+ Parent.TryDropMechanism(performer, target, out _);
+ _disconnectedOrgans.Remove(target);
+ }
+
+ private void RemoveBodyPartSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
+ {
+ // This surgery requires a DroppedBodyPartComponent.
+ if (!(container is BodyManagerComponent))
+ {
+ return;
+ }
+
+ var bmTarget = (BodyManagerComponent) container;
+ performer.PopupMessage(performer, Loc.GetString("Saw off the limb!"));
+
+ // TODO do_after: Delay
+ bmTarget.DisconnectBodyPart(Parent, true);
+ }
+ }
+}
diff --git a/Content.Server/Body/Surgery/ISurgeon.cs b/Content.Server/Body/Surgery/ISurgeon.cs
new file mode 100644
index 0000000000..be2ed1135c
--- /dev/null
+++ b/Content.Server/Body/Surgery/ISurgeon.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using Content.Server.Body.Mechanisms;
+using Content.Server.GameObjects.Components.Body;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.Body.Surgery
+{
+ ///
+ /// Interface representing an entity capable of performing surgery (performing operations on an
+ /// class).
+ /// For an example see , which inherits from this class.
+ ///
+ public interface ISurgeon
+ {
+ public delegate void MechanismRequestCallback(
+ Mechanism target,
+ IBodyPartContainer container,
+ ISurgeon surgeon,
+ IEntity performer);
+
+ ///
+ /// How long it takes to perform a single surgery step (in seconds).
+ ///
+ public float BaseOperationTime { get; set; }
+
+ ///
+ /// When performing a surgery, the may sometimes require selecting from a set of Mechanisms
+ /// to operate on.
+ /// This function is called in that scenario, and it is expected that you call the callback with one mechanism from the
+ /// provided list.
+ ///
+ public void RequestMechanism(IEnumerable options, MechanismRequestCallback callback);
+ }
+}
diff --git a/Content.Server/Body/Surgery/SurgeryData.cs b/Content.Server/Body/Surgery/SurgeryData.cs
new file mode 100644
index 0000000000..a4274d0042
--- /dev/null
+++ b/Content.Server/Body/Surgery/SurgeryData.cs
@@ -0,0 +1,91 @@
+#nullable enable
+using Content.Server.Body.Mechanisms;
+using Content.Shared.GameObjects.Components.Body;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.Body.Surgery
+{
+ ///
+ /// This data class represents the state of a in regards to everything surgery related -
+ /// whether there's an incision on it, whether the bone is broken, etc.
+ ///
+ public abstract class SurgeryData
+ {
+ protected delegate void SurgeryAction(IBodyPartContainer container, ISurgeon surgeon, IEntity performer);
+
+ ///
+ /// The this surgeryData is attached to.
+ /// The class should not exist without a
+ /// that it represents, and will throw errors if it
+ /// is null.
+ ///
+ protected readonly BodyPart Parent;
+
+ protected SurgeryData(BodyPart parent)
+ {
+ Parent = parent;
+ }
+
+ ///
+ /// The of the parent .
+ ///
+ protected BodyPartType ParentType => Parent.PartType;
+
+ ///
+ /// Returns the description of this current to be shown
+ /// upon observing the given entity.
+ ///
+ public abstract string GetDescription(IEntity target);
+
+ ///
+ /// Returns whether a can be installed into the
+ /// this represents.
+ ///
+ public abstract bool CanInstallMechanism(Mechanism mechanism);
+
+ ///
+ /// Returns whether the given can be connected to the
+ /// this represents.
+ ///
+ public abstract bool CanAttachBodyPart(BodyPart part);
+
+ ///
+ /// Gets the delegate corresponding to the surgery step using the given
+ /// .
+ ///
+ ///
+ /// The corresponding surgery action or null if no step can be performed.
+ ///
+ protected abstract SurgeryAction? GetSurgeryStep(SurgeryType toolType);
+
+ ///
+ /// Returns whether the given can be used to perform a surgery on the BodyPart this
+ /// represents.
+ ///
+ public bool CheckSurgery(SurgeryType toolType)
+ {
+ return GetSurgeryStep(toolType) != null;
+ }
+
+ ///
+ /// Attempts to perform surgery of the given . Returns whether the operation was successful.
+ ///
+ /// The used for this surgery.
+ /// The container where the surgery is being done.
+ /// The entity being used to perform the surgery.
+ /// The entity performing the surgery.
+ public bool PerformSurgery(SurgeryType surgeryType, IBodyPartContainer container, ISurgeon surgeon,
+ IEntity performer)
+ {
+ var step = GetSurgeryStep(surgeryType);
+
+ if (step == null)
+ {
+ return false;
+ }
+
+ step(container, surgeon, performer);
+ return true;
+ }
+ }
+}
diff --git a/Content.Server/Chat/ChatCommands.cs b/Content.Server/Chat/ChatCommands.cs
index a6c5404f59..777bba1fe9 100644
--- a/Content.Server/Chat/ChatCommands.cs
+++ b/Content.Server/Chat/ChatCommands.cs
@@ -1,6 +1,5 @@
-using System;
+using System;
using System.Linq;
-using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Observer;
@@ -15,6 +14,7 @@ using Robust.Shared.Enums;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
+using Content.Shared.Damage;
namespace Content.Server.Chat
{
@@ -107,24 +107,24 @@ namespace Content.Server.Chat
"If that fails, it will attempt to use an object in the environment.\n" +
"Finally, if neither of the above worked, you will die by biting your tongue.";
- private void DealDamage(ISuicideAct suicide, IChatManager chat, DamageableComponent damageableComponent, IEntity source, IEntity target)
+ private void DealDamage(ISuicideAct suicide, IChatManager chat, IDamageableComponent damageableComponent, IEntity source, IEntity target)
{
SuicideKind kind = suicide.Suicide(target, chat);
if (kind != SuicideKind.Special)
{
- damageableComponent.TakeDamage(kind switch
- {
- SuicideKind.Brute => DamageType.Brute,
- SuicideKind.Heat => DamageType.Heat,
- SuicideKind.Cold => DamageType.Cold,
- SuicideKind.Acid => DamageType.Acid,
- SuicideKind.Toxic => DamageType.Toxic,
- SuicideKind.Electric => DamageType.Electric,
- _ => DamageType.Brute
- },
- 500, //TODO: needs to be a max damage of some sorts
- source,
- target);
+ damageableComponent.ChangeDamage(kind switch
+ {
+ SuicideKind.Blunt => DamageType.Blunt,
+ SuicideKind.Piercing => DamageType.Piercing,
+ SuicideKind.Heat => DamageType.Heat,
+ SuicideKind.Disintegration => DamageType.Disintegration,
+ SuicideKind.Cellular => DamageType.Cellular,
+ SuicideKind.DNA => DamageType.DNA,
+ SuicideKind.Asphyxiation => DamageType.Asphyxiation,
+ _ => DamageType.Blunt
+ },
+ 500,
+ true, source);
}
}
@@ -135,7 +135,7 @@ namespace Content.Server.Chat
var chat = IoCManager.Resolve();
var owner = player.ContentData().Mind.OwnedMob.Owner;
- var dmgComponent = owner.GetComponent();
+ var dmgComponent = owner.GetComponent();
//TODO: needs to check if the mob is actually alive
//TODO: maybe set a suicided flag to prevent ressurection?
@@ -169,7 +169,7 @@ namespace Content.Server.Chat
}
// Default suicide, bite your tongue
chat.EntityMe(owner, Loc.GetString("is attempting to bite {0:their} own tongue, looks like {0:theyre} trying to commit suicide!", owner)); //TODO: theyre macro
- dmgComponent.TakeDamage(DamageType.Brute, 500, owner, owner); //TODO: dmg value needs to be a max damage of some sorts
+ dmgComponent.ChangeDamage(DamageType.Piercing, 500, true, owner);
// Prevent the player from returning to the body. Yes, this is an ugly hack.
var ghost = new Ghost(){CanReturn = false};
diff --git a/Content.Server/Chat/ChatManager.cs b/Content.Server/Chat/ChatManager.cs
index 1a035fbb5c..a6e0296b04 100644
--- a/Content.Server/Chat/ChatManager.cs
+++ b/Content.Server/Chat/ChatManager.cs
@@ -5,7 +5,9 @@ using Content.Server.Interfaces;
using Content.Server.Interfaces.Chat;
using Content.Shared.Chat;
using Content.Shared.GameObjects.EntitySystems;
+using NFluidsynth;
using Robust.Server.Console;
+using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
@@ -19,8 +21,18 @@ namespace Content.Server.Chat
///
internal sealed class ChatManager : IChatManager
{
+ ///
+ /// The maximum length a player-sent message can be sent
+ ///
+ public int MaxMessageLength = 1000;
+
private const int VoiceRange = 7; // how far voice goes in world units
+ ///
+ /// The message displayed to the player when it exceeds the chat character limit
+ ///
+ private const string MaxLengthExceededMessage = "Your message exceeded {0} character limit";
+
#pragma warning disable 649
[Dependency] private readonly IEntitySystemManager _entitySystemManager;
[Dependency] private readonly IServerNetManager _netManager;
@@ -33,6 +45,12 @@ namespace Content.Server.Chat
public void Initialize()
{
_netManager.RegisterNetMessage(MsgChatMessage.NAME);
+ _netManager.RegisterNetMessage(ChatMaxMsgLengthMessage.NAME, _onMaxLengthRequest);
+
+ // Tell all the connected players the chat's character limit
+ var msg = _netManager.CreateNetMessage();
+ msg.MaxMessageLength = MaxMessageLength;
+ _netManager.ServerSendToAll(msg);
}
public void DispatchServerAnnouncement(string message)
@@ -69,6 +87,17 @@ namespace Content.Server.Chat
return;
}
+ // Get entity's PlayerSession
+ IPlayerSession playerSession = source.GetComponent().playerSession;
+
+ // Check if message exceeds the character limit if the sender is a player
+ if (playerSession != null)
+ if (message.Length > MaxMessageLength)
+ {
+ DispatchServerMessage(playerSession, Loc.GetString(MaxLengthExceededMessage, MaxMessageLength));
+ return;
+ }
+
var pos = source.Transform.GridPosition;
var clients = _playerManager.GetPlayersInRange(pos, VoiceRange).Select(p => p.ConnectedClient);
@@ -90,6 +119,17 @@ namespace Content.Server.Chat
return;
}
+ // Check if entity is a player
+ IPlayerSession playerSession = source.GetComponent().playerSession;
+
+ // Check if message exceeds the character limit
+ if (playerSession != null)
+ if (action.Length > MaxMessageLength)
+ {
+ DispatchServerMessage(playerSession, Loc.GetString(MaxLengthExceededMessage, MaxMessageLength));
+ return;
+ }
+
var pos = source.Transform.GridPosition;
var clients = _playerManager.GetPlayersInRange(pos, VoiceRange).Select(p => p.ConnectedClient);
@@ -103,6 +143,13 @@ namespace Content.Server.Chat
public void SendOOC(IPlayerSession player, string message)
{
+ // Check if message exceeds the character limi
+ if (message.Length > MaxMessageLength)
+ {
+ DispatchServerMessage(player, Loc.GetString(MaxLengthExceededMessage, MaxMessageLength));
+ return;
+ }
+
var msg = _netManager.CreateNetMessage();
msg.Channel = ChatChannel.OOC;
msg.Message = message;
@@ -114,6 +161,13 @@ namespace Content.Server.Chat
public void SendDeadChat(IPlayerSession player, string message)
{
+ // Check if message exceeds the character limit
+ if (message.Length > MaxMessageLength)
+ {
+ DispatchServerMessage(player, Loc.GetString(MaxLengthExceededMessage, MaxMessageLength));
+ return;
+ }
+
var clients = _playerManager.GetPlayersBy(x => x.AttachedEntity != null && x.AttachedEntity.HasComponent()).Select(p => p.ConnectedClient);;
var msg = _netManager.CreateNetMessage();
@@ -126,7 +180,14 @@ namespace Content.Server.Chat
public void SendAdminChat(IPlayerSession player, string message)
{
- if(!_conGroupController.CanCommand(player, "asay"))
+ // Check if message exceeds the character limit
+ if (message.Length > MaxMessageLength)
+ {
+ DispatchServerMessage(player, Loc.GetString(MaxLengthExceededMessage, MaxMessageLength));
+ return;
+ }
+
+ if (!_conGroupController.CanCommand(player, "asay"))
{
SendOOC(player, message);
return;
@@ -149,5 +210,12 @@ namespace Content.Server.Chat
msg.MessageWrap = $"OOC: (D){sender}: {{0}}";
_netManager.ServerSendToAll(msg);
}
+
+ private void _onMaxLengthRequest(ChatMaxMsgLengthMessage msg)
+ {
+ var response = _netManager.CreateNetMessage();
+ response.MaxMessageLength = MaxMessageLength;
+ _netManager.ServerSendMessage(response, msg.MsgChannel);
+ }
}
}
diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs
index e83771204d..120b364352 100644
--- a/Content.Server/EntryPoint.cs
+++ b/Content.Server/EntryPoint.cs
@@ -1,8 +1,9 @@
-using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.WorldState;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using Content.Server.Interfaces;
using Content.Server.Interfaces.Chat;
+using Content.Server.Body.Network;
using Content.Server.Interfaces.GameTicking;
using Content.Server.Interfaces.PDA;
using Content.Server.Sandbox;
@@ -46,6 +47,8 @@ namespace Content.Server
IoCManager.BuildGraph();
+ IoCManager.Resolve().DoAutoRegistrations();
+
_gameTicker = IoCManager.Resolve();
IoCManager.Resolve().Initialize();
diff --git a/Content.Server/Explosions/ExplosionHelper.cs b/Content.Server/Explosions/ExplosionHelper.cs
index 277713bf28..1b61f037f3 100644
--- a/Content.Server/Explosions/ExplosionHelper.cs
+++ b/Content.Server/Explosions/ExplosionHelper.cs
@@ -1,7 +1,7 @@
using System;
using System.Linq;
using Content.Server.GameObjects.Components.Mobs;
-using Content.Server.GameObjects.EntitySystems;
+using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Maps;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
diff --git a/Content.Server/GameObjects/Components/AnchorableComponent.cs b/Content.Server/GameObjects/Components/AnchorableComponent.cs
index 6ad088bdbe..b1d07fbc60 100644
--- a/Content.Server/GameObjects/Components/AnchorableComponent.cs
+++ b/Content.Server/GameObjects/Components/AnchorableComponent.cs
@@ -14,6 +14,8 @@ namespace Content.Server.GameObjects.Components
{
public override string Name => "Anchorable";
+ int IInteractUsing.Priority => 1;
+
///
/// Checks if a tool can change the anchored status.
///
diff --git a/Content.Server/GameObjects/Components/Atmos/BarotraumaComponent.cs b/Content.Server/GameObjects/Components/Atmos/BarotraumaComponent.cs
index 4beaa374b1..cabaf72d5a 100644
--- a/Content.Server/GameObjects/Components/Atmos/BarotraumaComponent.cs
+++ b/Content.Server/GameObjects/Components/Atmos/BarotraumaComponent.cs
@@ -1,11 +1,11 @@
using System;
using System.Runtime.CompilerServices;
-using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.GameObjects;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.Components.Damage;
+using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
@@ -23,7 +23,7 @@ namespace Content.Server.GameObjects.Components.Atmos
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update(float frameTime)
{
- if (!Owner.TryGetComponent(out DamageableComponent damageable)) return;
+ if (!Owner.TryGetComponent(out IDamageableComponent damageable)) return;
Owner.TryGetComponent(out ServerStatusEffectsComponent status);
var coordinates = Owner.Transform.GridPosition;
@@ -52,7 +52,7 @@ namespace Content.Server.GameObjects.Components.Atmos
if(pressure > Atmospherics.WarningLowPressure)
goto default;
- damageable.TakeDamage(DamageType.Brute, Atmospherics.LowPressureDamage, Owner);
+ damageable.ChangeDamage(DamageType.Blunt, Atmospherics.LowPressureDamage, false, Owner);
if (status == null) break;
@@ -74,7 +74,7 @@ namespace Content.Server.GameObjects.Components.Atmos
var damage = (int) MathF.Min((pressure / Atmospherics.HazardHighPressure) * Atmospherics.PressureDamageCoefficient, Atmospherics.MaxHighPressureDamage);
- damageable.TakeDamage(DamageType.Brute, damage, Owner);
+ damageable.ChangeDamage(DamageType.Blunt, damage, false, Owner);
if (status == null) break;
diff --git a/Content.Server/GameObjects/Components/Atmos/GasMixtureComponent.cs b/Content.Server/GameObjects/Components/Atmos/GasMixtureComponent.cs
index 68e59e0869..c60ad08efa 100644
--- a/Content.Server/GameObjects/Components/Atmos/GasMixtureComponent.cs
+++ b/Content.Server/GameObjects/Components/Atmos/GasMixtureComponent.cs
@@ -1,6 +1,7 @@
using Content.Server.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
@@ -8,7 +9,8 @@ namespace Content.Server.GameObjects.Components.Atmos
public class GasMixtureComponent : Component
{
public override string Name => "GasMixture";
- public GasMixture GasMixture { get; set; } = new GasMixture();
+
+ [ViewVariables] public GasMixture GasMixture { get; set; } = new GasMixture();
public override void ExposeData(ObjectSerializer serializer)
{
diff --git a/Content.Server/GameObjects/Components/Body/BodyManagerComponent.cs b/Content.Server/GameObjects/Components/Body/BodyManagerComponent.cs
new file mode 100644
index 0000000000..ac2f427299
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Body/BodyManagerComponent.cs
@@ -0,0 +1,1015 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Server.Body;
+using Content.Server.Body.Network;
+using Content.Server.GameObjects.Components.Metabolism;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Server.Interfaces.GameObjects.Components.Interaction;
+using Content.Server.Mobs;
+using Content.Server.Observer;
+using Content.Shared.Body.Part;
+using Content.Shared.Body.Part.Properties.Movement;
+using Content.Shared.Body.Part.Properties.Other;
+using Content.Shared.Body.Preset;
+using Content.Shared.Body.Template;
+using Content.Shared.GameObjects.Components.Body;
+using Content.Shared.GameObjects.Components.Damage;
+using Content.Shared.GameObjects.Components.Movement;
+using Robust.Server.GameObjects;
+using Robust.Server.Interfaces.Player;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Reflection;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Maths;
+using Robust.Shared.Players;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.GameObjects.Components.Body
+{
+ ///
+ /// Component representing a collection of
+ /// attached to each other.
+ ///
+ [RegisterComponent]
+ [ComponentReference(typeof(IDamageableComponent))]
+ [ComponentReference(typeof(IBodyManagerComponent))]
+ public class BodyManagerComponent : SharedBodyManagerComponent, IBodyPartContainer, IRelayMoveInput
+ {
+#pragma warning disable CS0649
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IBodyNetworkFactory _bodyNetworkFactory = default!;
+ [Dependency] private readonly IReflectionManager _reflectionManager = default!;
+#pragma warning restore
+
+ [ViewVariables] private string _presetName = default!;
+
+ private readonly Dictionary _parts = new Dictionary();
+
+ [ViewVariables] private readonly Dictionary _networks = new Dictionary();
+
+ ///
+ /// All with
+ /// that are currently affecting move speed, mapped to how big that leg
+ /// they're on is.
+ ///
+ [ViewVariables]
+ private readonly Dictionary _activeLegs = new Dictionary();
+
+ ///
+ /// The that this
+ /// is adhering to.
+ ///
+ [ViewVariables]
+ public BodyTemplate Template { get; private set; } = default!;
+
+ ///
+ /// The that this
+ /// is adhering to.
+ ///
+ [ViewVariables]
+ public BodyPreset Preset { get; private set; } = default!;
+
+ ///
+ /// Maps slot name to the
+ /// object filling it (if there is one).
+ ///
+ [ViewVariables]
+ public IReadOnlyDictionary Parts => _parts;
+
+ ///
+ /// List of all slots in this body, taken from the keys of
+ /// slots.
+ ///
+ public IEnumerable AllSlots => Template.Slots.Keys;
+
+ ///
+ /// List of all occupied slots in this body, taken from the values of
+ /// .
+ ///
+ public IEnumerable OccupiedSlots => Parts.Keys;
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataReadWriteFunction(
+ "baseTemplate",
+ "bodyTemplate.Humanoid",
+ template =>
+ {
+ if (!_prototypeManager.TryIndex(template, out BodyTemplatePrototype templateData))
+ {
+ // Invalid prototype
+ throw new InvalidOperationException(
+ $"No {nameof(BodyTemplatePrototype)} found with name {template}");
+ }
+
+ Template = new BodyTemplate(templateData);
+ },
+ () => Template.Name);
+
+ serializer.DataReadWriteFunction(
+ "basePreset",
+ "bodyPreset.BasicHuman",
+ preset =>
+ {
+ if (!_prototypeManager.TryIndex(preset, out BodyPresetPrototype presetData))
+ {
+ // Invalid prototype
+ throw new InvalidOperationException(
+ $"No {nameof(BodyPresetPrototype)} found with name {preset}");
+ }
+
+ Preset = new BodyPreset(presetData);
+ },
+ () => _presetName);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ LoadBodyPreset(Preset);
+
+ foreach (var behavior in Owner.GetAllComponents())
+ {
+ HealthChangedEvent += behavior.OnHealthChanged;
+ }
+ }
+
+ protected override void Startup()
+ {
+ base.Startup();
+
+ // Just in case something activates at default health.
+ ForceHealthChangedEvent();
+ }
+
+ private void LoadBodyPreset(BodyPreset preset)
+ {
+ _presetName = preset.Name;
+
+ foreach (var slotName in Template.Slots.Keys)
+ {
+ // For each slot in our BodyManagerComponent's template,
+ // try and grab what the ID of what the preset says should be inside it.
+ if (!preset.PartIDs.TryGetValue(slotName, out var partId))
+ {
+ // If the preset doesn't define anything for it, continue.
+ continue;
+ }
+
+ // Get the BodyPartPrototype corresponding to the BodyPart ID we grabbed.
+ if (!_prototypeManager.TryIndex(partId, out BodyPartPrototype newPartData))
+ {
+ throw new InvalidOperationException($"No {nameof(BodyPartPrototype)} prototype found with ID {partId}");
+ }
+
+ // Try and remove an existing limb if that exists.
+ RemoveBodyPart(slotName, false);
+
+ // Add a new BodyPart with the BodyPartPrototype as a baseline to our
+ // BodyComponent.
+ var addedPart = new BodyPart(newPartData);
+ AddBodyPart(addedPart, slotName);
+ }
+
+ OnBodyChanged(); // TODO: Duplicate code
+ }
+
+ ///
+ /// Changes the current to the given
+ /// .
+ /// Attempts to keep previous if there is a slot for
+ /// them in both .
+ ///
+ public void ChangeBodyTemplate(BodyTemplatePrototype newTemplate)
+ {
+ foreach (var part in Parts)
+ {
+ // TODO: Make this work.
+ }
+
+ OnBodyChanged();
+ }
+
+ ///
+ /// This method is called by before
+ /// is called.
+ ///
+ public void PreMetabolism(float frameTime)
+ {
+ if (CurrentDamageState == DamageState.Dead)
+ {
+ return;
+ }
+
+ foreach (var part in Parts.Values)
+ {
+ part.PreMetabolism(frameTime);
+ }
+
+ foreach (var network in _networks.Values)
+ {
+ network.Update(frameTime);
+ }
+ }
+
+ ///
+ /// This method is called by after
+ /// is called.
+ ///
+ public void PostMetabolism(float frameTime)
+ {
+ if (CurrentDamageState == DamageState.Dead)
+ {
+ return;
+ }
+
+ foreach (var part in Parts.Values)
+ {
+ part.PostMetabolism(frameTime);
+ }
+
+ foreach (var network in _networks.Values)
+ {
+ network.Update(frameTime);
+ }
+ }
+
+ ///
+ /// Called when the layout of this body changes.
+ ///
+ private void OnBodyChanged()
+ {
+ // Calculate move speed based on this body.
+ if (Owner.HasComponent())
+ {
+ _activeLegs.Clear();
+ var legParts = Parts.Values.Where(x => x.HasProperty(typeof(LegProperty)));
+
+ foreach (var part in legParts)
+ {
+ var footDistance = DistanceToNearestFoot(this, part);
+
+ if (Math.Abs(footDistance - float.MinValue) > 0.001f)
+ {
+ _activeLegs.Add(part, footDistance);
+ }
+ }
+
+ CalculateSpeed();
+ }
+ }
+
+ private void CalculateSpeed()
+ {
+ if (!Owner.TryGetComponent(out MovementSpeedModifierComponent playerMover))
+ {
+ return;
+ }
+
+ float speedSum = 0;
+ foreach (var part in _activeLegs.Keys)
+ {
+ if (!part.HasProperty())
+ {
+ _activeLegs.Remove(part);
+ }
+ }
+
+ foreach (var (key, value) in _activeLegs)
+ {
+ if (key.TryGetProperty(out LegProperty legProperty))
+ {
+ // Speed of a leg = base speed * (1+log1024(leg length))
+ speedSum += legProperty.Speed * (1 + (float) Math.Log(value, 1024.0));
+ }
+ }
+
+ if (speedSum <= 0.001f || _activeLegs.Count <= 0)
+ {
+ // Case: no way of moving. Fall down.
+ StandingStateHelper.Down(Owner);
+ playerMover.BaseWalkSpeed = 0.8f;
+ playerMover.BaseSprintSpeed = 2.0f;
+ }
+ else
+ {
+ // Case: have at least one leg. Set move speed.
+ StandingStateHelper.Standing(Owner);
+
+ // Extra legs stack diminishingly.
+ // Final speed = speed sum/(leg count-log4(leg count))
+ playerMover.BaseWalkSpeed =
+ speedSum / (_activeLegs.Count - (float) Math.Log(_activeLegs.Count, 4.0));
+
+ playerMover.BaseSprintSpeed = playerMover.BaseWalkSpeed * 1.75f;
+ }
+ }
+
+ void IRelayMoveInput.MoveInputPressed(ICommonSession session)
+ {
+ if (CurrentDamageState == DamageState.Dead)
+ {
+ new Ghost().Execute(null, (IPlayerSession) session, null);
+ }
+ }
+
+ #region BodyPart Functions
+
+ ///
+ /// Recursively searches for if is connected to
+ /// the center. Not efficient (O(n^2)), but most bodies don't have a ton
+ /// of s.
+ ///
+ /// The body part to find the center for.
+ /// True if it is connected to the center, false otherwise.
+ private bool ConnectedToCenterPart(BodyPart target)
+ {
+ var searchedSlots = new List();
+
+ return TryGetSlotName(target, out var result) &&
+ ConnectedToCenterPartRecursion(searchedSlots, result);
+ }
+
+ private bool ConnectedToCenterPartRecursion(ICollection searchedSlots, string slotName)
+ {
+ TryGetBodyPart(slotName, out var part);
+
+ if (part == null)
+ {
+ return false;
+ }
+
+ if (part == GetCenterBodyPart())
+ {
+ return true;
+ }
+
+ searchedSlots.Add(slotName);
+
+ if (!TryGetBodyPartConnections(slotName, out List connections))
+ {
+ return false;
+ }
+
+ foreach (var connection in connections)
+ {
+ if (!searchedSlots.Contains(connection) &&
+ ConnectedToCenterPartRecursion(searchedSlots, connection))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Finds the central , if any, of this body based on
+ /// the . For humans, this is the torso.
+ ///
+ /// The if one exists, null otherwise.
+ private BodyPart? GetCenterBodyPart()
+ {
+ Parts.TryGetValue(Template.CenterSlot, out var center);
+ return center!;
+ }
+
+ ///
+ /// Returns whether the given slot name exists within the current
+ /// .
+ ///
+ private bool SlotExists(string slotName)
+ {
+ return Template.SlotExists(slotName);
+ }
+
+ ///
+ /// Finds the in the given if
+ /// one exists.
+ ///
+ /// The slot to search in.
+ /// The body part in that slot, if any.
+ /// True if found, false otherwise.
+ private bool TryGetBodyPart(string slotName, [NotNullWhen(true)] out BodyPart result)
+ {
+ return Parts.TryGetValue(slotName, out result!);
+ }
+
+ ///
+ /// Finds the slotName that the given resides in.
+ ///
+ /// The to find the slot for.
+ /// The slot found, if any.
+ /// True if a slot was found, false otherwise
+ private bool TryGetSlotName(BodyPart part, [NotNullWhen(true)] out string result)
+ {
+ // We enforce that there is only one of each value in the dictionary,
+ // so we can iterate through the dictionary values to get the key from there.
+ result = Parts.FirstOrDefault(x => x.Value == part).Key;
+ return result != null;
+ }
+
+ ///
+ /// Finds the in the given
+ /// if one exists.
+ ///
+ /// The slot to search in.
+ ///
+ /// The of that slot, if any.
+ ///
+ /// True if found, false otherwise.
+ public bool TryGetSlotType(string slotName, out BodyPartType result)
+ {
+ return Template.Slots.TryGetValue(slotName, out result);
+ }
+
+ ///
+ /// Finds the names of all slots connected to the given
+ /// for the template.
+ ///
+ /// The slot to search in.
+ /// The connections found, if any.
+ /// True if the connections are found, false otherwise.
+ private bool TryGetBodyPartConnections(string slotName, [NotNullWhen(true)] out List connections)
+ {
+ return Template.Connections.TryGetValue(slotName, out connections!);
+ }
+
+ ///
+ /// Grabs all occupied slots connected to the given slot,
+ /// regardless of whether the given is occupied.
+ ///
+ /// The slot name to find connections from.
+ /// The connected body parts, if any.
+ ///
+ /// True if successful, false if there was an error or no connected
+ /// s were found.
+ ///
+ public bool TryGetBodyPartConnections(string slotName, [NotNullWhen(true)] out List result)
+ {
+ result = null!;
+
+ if (!Template.Connections.TryGetValue(slotName, out var connections))
+ {
+ return false;
+ }
+
+ var toReturn = new List();
+ foreach (var connection in connections)
+ {
+ if (TryGetBodyPart(connection, out var bodyPartResult))
+ {
+ toReturn.Add(bodyPartResult);
+ }
+ }
+
+ if (toReturn.Count <= 0)
+ {
+ return false;
+ }
+
+ result = toReturn;
+ return true;
+ }
+
+ ///
+ /// Grabs all parts connected to the given , regardless
+ /// of whether the given is occupied.
+ ///
+ ///
+ /// True if successful, false if there was an error or no connected
+ /// s were found.
+ ///
+ private bool TryGetBodyPartConnections(BodyPart part, [NotNullWhen(true)] out List result)
+ {
+ result = null!;
+
+ return TryGetSlotName(part, out var slotName) &&
+ TryGetBodyPartConnections(slotName, out result);
+ }
+
+ ///
+ /// Grabs all of the given type in this body.
+ ///
+ public List GetBodyPartsOfType(BodyPartType type)
+ {
+ var toReturn = new List();
+
+ foreach (var part in Parts.Values)
+ {
+ if (part.PartType == type)
+ {
+ toReturn.Add(part);
+ }
+ }
+
+ return toReturn;
+ }
+
+ ///
+ /// Installs the given into the given slot.
+ ///
+ /// True if successful, false otherwise.
+ public bool InstallBodyPart(BodyPart part, string slotName)
+ {
+ DebugTools.AssertNotNull(part);
+
+ // Make sure the given slot exists
+ if (!SlotExists(slotName))
+ {
+ return false;
+ }
+
+ // And that nothing is in it
+ if (TryGetBodyPart(slotName, out _))
+ {
+ return false;
+ }
+
+ AddBodyPart(part, slotName); // TODO: Sort this duplicate out
+ OnBodyChanged();
+
+ return true;
+ }
+
+ ///
+ /// Installs the given into the
+ /// given slot, deleting the afterwards.
+ ///
+ /// True if successful, false otherwise.
+ public bool InstallDroppedBodyPart(DroppedBodyPartComponent part, string slotName)
+ {
+ DebugTools.AssertNotNull(part);
+
+ if (!InstallBodyPart(part.ContainedBodyPart, slotName))
+ {
+ return false;
+ }
+
+ part.Owner.Delete();
+ return true;
+ }
+
+ ///
+ /// Disconnects the given reference, potentially
+ /// dropping other BodyParts if they were hanging
+ /// off of it.
+ ///
+ ///
+ /// The representing the dropped
+ /// , or null if none was dropped.
+ ///
+ public IEntity? DropPart(BodyPart part)
+ {
+ DebugTools.AssertNotNull(part);
+
+ if (!_parts.ContainsValue(part))
+ {
+ return null;
+ }
+
+ if (!RemoveBodyPart(part, out var slotName))
+ {
+ return null;
+ }
+
+ // Call disconnect on all limbs that were hanging off this limb.
+ if (TryGetBodyPartConnections(slotName, out List connections))
+ {
+ // This loop is an unoptimized travesty. TODO: optimize to be less shit
+ foreach (var connectionName in connections)
+ {
+ if (TryGetBodyPart(connectionName, out var result) && !ConnectedToCenterPart(result))
+ {
+ DisconnectBodyPart(connectionName, true);
+ }
+ }
+ }
+
+ part.SpawnDropped(out var dropped);
+
+ OnBodyChanged();
+ return dropped;
+ }
+
+ ///
+ /// Disconnects the given reference, potentially
+ /// dropping other BodyParts if they were hanging
+ /// off of it.
+ ///
+ public void DisconnectBodyPart(BodyPart part, bool dropEntity)
+ {
+ DebugTools.AssertNotNull(part);
+
+ if (!_parts.ContainsValue(part))
+ {
+ return;
+ }
+
+ var slotName = Parts.FirstOrDefault(x => x.Value == part).Key;
+ RemoveBodyPart(slotName, dropEntity);
+
+ // Call disconnect on all limbs that were hanging off this limb
+ if (TryGetBodyPartConnections(slotName, out List connections))
+ {
+ // TODO: Optimize
+ foreach (var connectionName in connections)
+ {
+ if (TryGetBodyPart(connectionName, out var result) && !ConnectedToCenterPart(result))
+ {
+ DisconnectBodyPart(connectionName, dropEntity);
+ }
+ }
+ }
+
+ OnBodyChanged();
+ }
+
+ ///
+ /// Disconnects a body part in the given slot if one exists,
+ /// optionally dropping it.
+ ///
+ /// The slot to remove the body part from
+ ///
+ /// Whether or not to drop the body part as an entity if it exists.
+ ///
+ private void DisconnectBodyPart(string slotName, bool dropEntity)
+ {
+ DebugTools.AssertNotNull(slotName);
+
+ if (!TryGetBodyPart(slotName, out var part))
+ {
+ return;
+ }
+
+ if (part == null)
+ {
+ return;
+ }
+
+ RemoveBodyPart(slotName, dropEntity);
+
+ if (TryGetBodyPartConnections(slotName, out List connections))
+ {
+ foreach (var connectionName in connections)
+ {
+ if (TryGetBodyPart(connectionName, out var result) && !ConnectedToCenterPart(result))
+ {
+ DisconnectBodyPart(connectionName, dropEntity);
+ }
+ }
+ }
+
+ OnBodyChanged();
+ }
+
+ private void AddBodyPart(BodyPart part, string slotName)
+ {
+ DebugTools.AssertNotNull(part);
+ DebugTools.AssertNotNull(slotName);
+
+ _parts.Add(slotName, part);
+
+ part.Body = this;
+
+ var argsAdded = new BodyPartAddedEventArgs(part, slotName);
+
+ foreach (var component in Owner.GetAllComponents().ToArray())
+ {
+ component.BodyPartAdded(argsAdded);
+ }
+
+ if (!Template.Layers.TryGetValue(slotName, out var partMap) ||
+ !_reflectionManager.TryParseEnumReference(partMap, out var partEnum))
+ {
+ Logger.Warning($"Template {Template.Name} has an invalid RSI map key {partMap} for body part {part.Name}.");
+ return;
+ }
+
+ part.RSIMap = partEnum;
+
+ var partMessage = new BodyPartAddedMessage(part.RSIPath, part.RSIState, partEnum);
+
+ SendNetworkMessage(partMessage);
+
+ foreach (var mechanism in part.Mechanisms)
+ {
+ if (!Template.MechanismLayers.TryGetValue(mechanism.Id, out var mechanismMap))
+ {
+ continue;
+ }
+
+ if (!_reflectionManager.TryParseEnumReference(mechanismMap, out var mechanismEnum))
+ {
+ Logger.Warning($"Template {Template.Name} has an invalid RSI map key {mechanismMap} for mechanism {mechanism.Id}.");
+ continue;
+ }
+
+ var mechanismMessage = new MechanismSpriteAddedMessage(mechanismEnum);
+
+ SendNetworkMessage(mechanismMessage);
+ }
+ }
+
+ ///
+ /// Removes the body part in slot from this body,
+ /// if one exists.
+ ///
+ /// The slot to remove it from.
+ ///
+ /// Whether or not to drop the removed .
+ ///
+ ///
+ private bool RemoveBodyPart(string slotName, bool drop)
+ {
+ DebugTools.AssertNotNull(slotName);
+
+ if (!_parts.Remove(slotName, out var part))
+ {
+ return false;
+ }
+
+ IEntity? dropped = null;
+ if (drop)
+ {
+ part.SpawnDropped(out dropped);
+ }
+
+ part.Body = null;
+
+ var args = new BodyPartRemovedEventArgs(part, slotName);
+
+ foreach (var component in Owner.GetAllComponents())
+ {
+ component.BodyPartRemoved(args);
+ }
+
+ if (part.RSIMap != null)
+ {
+ var message = new BodyPartRemovedMessage(part.RSIMap, dropped?.Uid);
+ SendNetworkMessage(message);
+ }
+
+ foreach (var mechanism in part.Mechanisms)
+ {
+ if (!Template.MechanismLayers.TryGetValue(mechanism.Id, out var mechanismMap))
+ {
+ continue;
+ }
+
+ if (!_reflectionManager.TryParseEnumReference(mechanismMap, out var mechanismEnum))
+ {
+ Logger.Warning($"Template {Template.Name} has an invalid RSI map key {mechanismMap} for mechanism {mechanism.Id}.");
+ continue;
+ }
+
+ var mechanismMessage = new MechanismSpriteRemovedMessage(mechanismEnum);
+
+ SendNetworkMessage(mechanismMessage);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Removes the body part from this body, if one exists.
+ ///
+ /// The part to remove from this body.
+ /// The slot that the part was in, if any.
+ /// True if was removed, false otherwise.
+ private bool RemoveBodyPart(BodyPart part, [NotNullWhen(true)] out string slotName)
+ {
+ DebugTools.AssertNotNull(part);
+
+ slotName = _parts.FirstOrDefault(pair => pair.Value == part).Key;
+
+ if (slotName == null)
+ {
+ return false;
+ }
+
+ return RemoveBodyPart(slotName, false);
+ }
+
+ #endregion
+
+ #region BodyNetwork Functions
+
+ private bool EnsureNetwork(BodyNetwork network)
+ {
+ DebugTools.AssertNotNull(network);
+
+ if (_networks.ContainsKey(network.GetType()))
+ {
+ return false;
+ }
+
+ _networks.Add(network.GetType(), network);
+ network.OnAdd(Owner);
+
+ return true;
+ }
+
+ ///
+ /// Attempts to add a of the given type to this body.
+ ///
+ ///
+ /// True if successful, false if there was an error
+ /// (such as passing in an invalid type or a network of that type already
+ /// existing).
+ ///
+ public bool EnsureNetwork(Type networkType)
+ {
+ DebugTools.Assert(networkType.IsSubclassOf(typeof(BodyNetwork)));
+
+ var network = _bodyNetworkFactory.GetNetwork(networkType);
+ return EnsureNetwork(network);
+ }
+
+ ///
+ /// Attempts to add a of the given type to
+ /// this body.
+ ///
+ /// The type of network to add.
+ ///
+ /// True if successful, false if there was an error
+ /// (such as passing in an invalid type or a network of that type already
+ /// existing).
+ ///
+ public bool EnsureNetwork() where T : BodyNetwork
+ {
+ return EnsureNetwork(typeof(T));
+ }
+
+ ///
+ /// Attempts to add a of the given name to
+ /// this body.
+ ///
+ ///
+ /// True if successful, false if there was an error
+ /// (such as passing in an invalid type or a network of that type already
+ /// existing).
+ ///
+ private bool EnsureNetwork(string networkName)
+ {
+ DebugTools.AssertNotNull(networkName);
+
+ var network = _bodyNetworkFactory.GetNetwork(networkName);
+ return EnsureNetwork(network);
+ }
+
+ ///
+ /// Removes the of the given type in this body,
+ /// if there is one.
+ ///
+ /// The type of the network to remove.
+ public void RemoveNetwork(Type type)
+ {
+ DebugTools.AssertNotNull(type);
+
+ if (_networks.Remove(type, out var network))
+ {
+ network.OnRemove();
+ }
+ }
+
+ ///
+ /// Removes the of the given type in this body,
+ /// if one exists.
+ ///
+ /// The type of the network to remove.
+ public void RemoveNetwork() where T : BodyNetwork
+ {
+ RemoveNetwork(typeof(T));
+ }
+
+ ///
+ /// Removes the with the given name in this body,
+ /// if there is one.
+ ///
+ private void RemoveNetwork(string networkName)
+ {
+ var type = _bodyNetworkFactory.GetNetwork(networkName).GetType();
+
+ if (_networks.Remove(type, out var network))
+ {
+ network.OnRemove();
+ }
+ }
+
+ ///
+ /// Attempts to get the of the given type in this body.
+ ///
+ /// The type to search for.
+ ///
+ /// The if found, null otherwise.
+ ///
+ /// True if found, false otherwise.
+ public bool TryGetNetwork(Type networkType, [NotNullWhen(true)] out BodyNetwork result)
+ {
+ return _networks.TryGetValue(networkType, out result!);
+ }
+
+ #endregion
+
+ #region Recursion Functions
+
+ ///
+ /// Returns the combined length of the distance to the nearest with a
+ /// . Returns
+ /// if there is no foot found. If you consider a a node map, then it will look for
+ /// a foot node from the given node. It can
+ /// only search through BodyParts with .
+ ///
+ private static float DistanceToNearestFoot(BodyManagerComponent body, BodyPart source)
+ {
+ if (source.HasProperty() && source.TryGetProperty(out var property))
+ {
+ return property.ReachDistance;
+ }
+
+ return LookForFootRecursion(body, source, new List());
+ }
+
+ private static float LookForFootRecursion(BodyManagerComponent body, BodyPart current,
+ ICollection searchedParts)
+ {
+ if (!current.TryGetProperty(out var extProperty))
+ {
+ return float.MinValue;
+ }
+
+ // Get all connected parts if the current part has an extension property
+ if (!body.TryGetBodyPartConnections(current, out var connections))
+ {
+ return float.MinValue;
+ }
+
+ // If a connected BodyPart is a foot, return this BodyPart's length.
+ foreach (var connection in connections)
+ {
+ if (!searchedParts.Contains(connection) && connection.HasProperty())
+ {
+ return extProperty.ReachDistance;
+ }
+ }
+
+ // Otherwise, get the recursion values of all connected BodyParts and
+ // store them in a list.
+ var distances = new List();
+ foreach (var connection in connections)
+ {
+ if (!searchedParts.Contains(connection))
+ {
+ continue;
+ }
+
+ var result = LookForFootRecursion(body, connection, searchedParts);
+
+ if (Math.Abs(result - float.MinValue) > 0.001f)
+ {
+ distances.Add(result);
+ }
+ }
+
+ // If one or more of the searches found a foot, return the smallest one
+ // and add this ones length.
+ if (distances.Count > 0)
+ {
+ return distances.Min() + extProperty.ReachDistance;
+ }
+
+ return float.MinValue;
+
+ // No extension property, no go.
+ }
+
+ #endregion
+ }
+
+ public interface IBodyManagerHealthChangeParams
+ {
+ BodyPartType Part { get; }
+ }
+
+ public class BodyManagerHealthChangeParams : HealthChangeParams, IBodyManagerHealthChangeParams
+ {
+ public BodyManagerHealthChangeParams(BodyPartType part)
+ {
+ Part = part;
+ }
+
+ public BodyPartType Part { get; }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Body/BodyScannerComponent.cs b/Content.Server/GameObjects/Components/Body/BodyScannerComponent.cs
new file mode 100644
index 0000000000..2c49665757
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Body/BodyScannerComponent.cs
@@ -0,0 +1,73 @@
+using System.Collections.Generic;
+using Content.Server.Body;
+using Content.Shared.Body.Scanner;
+using Content.Shared.Interfaces.GameObjects.Components;
+using Robust.Server.GameObjects.Components.UserInterface;
+using Robust.Server.Interfaces.GameObjects;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.GameObjects.Components.Body
+{
+ [RegisterComponent]
+ [ComponentReference(typeof(IActivate))]
+ public class BodyScannerComponent : Component, IActivate
+ {
+ private BoundUserInterface _userInterface;
+ public sealed override string Name => "BodyScanner";
+
+ void IActivate.Activate(ActivateEventArgs eventArgs)
+ {
+ if (!eventArgs.User.TryGetComponent(out IActorComponent actor) ||
+ actor.playerSession.AttachedEntity == null)
+ {
+ return;
+ }
+
+ if (actor.playerSession.AttachedEntity.TryGetComponent(out BodyManagerComponent attempt))
+ {
+ var state = InterfaceState(attempt.Template, attempt.Parts);
+ _userInterface.SetState(state);
+ }
+
+ _userInterface.Open(actor.playerSession);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _userInterface = Owner.GetComponent()
+ .GetBoundUserInterface(BodyScannerUiKey.Key);
+ _userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
+ }
+
+ private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg) { }
+
+ ///
+ /// Copy BodyTemplate and BodyPart data into a common data class that the client can read.
+ ///
+ private BodyScannerInterfaceState InterfaceState(BodyTemplate template, IReadOnlyDictionary bodyParts)
+ {
+ var partsData = new Dictionary();
+
+ foreach (var (slotName, part) in bodyParts)
+ {
+ var mechanismData = new List();
+
+ foreach (var mechanism in part.Mechanisms)
+ {
+ mechanismData.Add(new BodyScannerMechanismData(mechanism.Name, mechanism.Description,
+ mechanism.RSIPath,
+ mechanism.RSIState, mechanism.MaxDurability, mechanism.CurrentDurability));
+ }
+
+ partsData.Add(slotName,
+ new BodyScannerBodyPartData(part.Name, part.RSIPath, part.RSIState, part.MaxDurability,
+ part.CurrentDurability, mechanismData));
+ }
+
+ var templateData = new BodyScannerTemplateData(template.Name, template.Slots);
+
+ return new BodyScannerInterfaceState(partsData, templateData);
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Body/Circulatory/BloodstreamComponent.cs b/Content.Server/GameObjects/Components/Body/Circulatory/BloodstreamComponent.cs
new file mode 100644
index 0000000000..9bf5f7e906
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Body/Circulatory/BloodstreamComponent.cs
@@ -0,0 +1,83 @@
+using Content.Server.Atmos;
+using Content.Server.GameObjects.Components.Chemistry;
+using Content.Server.GameObjects.Components.Metabolism;
+using Content.Server.Interfaces;
+using Content.Shared.Chemistry;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.GameObjects.Components.Body.Circulatory
+{
+ [RegisterComponent]
+ public class BloodstreamComponent : Component, IGasMixtureHolder
+ {
+ public override string Name => "Bloodstream";
+
+ ///
+ /// Max volume of internal solution storage
+ ///
+ [ViewVariables] private ReagentUnit _initialMaxVolume;
+
+ ///
+ /// Internal solution for reagent storage
+ ///
+ [ViewVariables] private SolutionComponent _internalSolution;
+
+ ///
+ /// Empty volume of internal solution
+ ///
+ [ViewVariables] public ReagentUnit EmptyVolume => _internalSolution.EmptyVolume;
+
+ [ViewVariables] public GasMixture Air { get; set; } = new GasMixture(6);
+
+ [ViewVariables] public SolutionComponent Solution => _internalSolution;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _internalSolution = Owner.EnsureComponent();
+ _internalSolution.MaxVolume = _initialMaxVolume;
+ }
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(ref _initialMaxVolume, "maxVolume", ReagentUnit.New(250));
+ }
+
+ ///
+ /// Attempt to transfer provided solution to internal solution.
+ /// Only supports complete transfers
+ ///
+ /// Solution to be transferred
+ /// Whether or not transfer was a success
+ public bool TryTransferSolution(Solution solution)
+ {
+ // For now doesn't support partial transfers
+ if (solution.TotalVolume + _internalSolution.CurrentVolume > _internalSolution.MaxVolume)
+ {
+ return false;
+ }
+
+ _internalSolution.TryAddSolution(solution, false, true);
+ return true;
+ }
+
+ public void PumpToxins(GasMixture into, float pressure)
+ {
+ if (!Owner.TryGetComponent(out MetabolismComponent metabolism))
+ {
+ Air.PumpGasTo(into, pressure);
+ return;
+ }
+
+ var toxins = metabolism.Clean(this);
+
+ toxins.PumpGasTo(into, pressure);
+ Air.Merge(toxins);
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs b/Content.Server/GameObjects/Components/Body/Digestive/StomachComponent.cs
similarity index 65%
rename from Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs
rename to Content.Server/GameObjects/Components/Body/Digestive/StomachComponent.cs
index db6e16831e..fa6b76caea 100644
--- a/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs
+++ b/Content.Server/GameObjects/Components/Body/Digestive/StomachComponent.cs
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
+using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.GameObjects.Components.Chemistry;
-using Content.Server.GameObjects.Components.Metabolism;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Nutrition;
using Robust.Shared.GameObjects;
@@ -11,7 +11,7 @@ using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
-namespace Content.Server.GameObjects.Components.Nutrition
+namespace Content.Server.GameObjects.Components.Body.Digestive
{
///
/// Where reagents go when ingested. Tracks ingested reagents over time, and
@@ -25,7 +25,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
#pragma warning restore 649
///
- /// Max volume of internal solution storage
+ /// Max volume of internal solution storage
///
public ReagentUnit MaxVolume
{
@@ -34,33 +34,29 @@ namespace Content.Server.GameObjects.Components.Nutrition
}
///
- /// Internal solution storage
+ /// Internal solution storage
///
[ViewVariables]
private SolutionComponent _stomachContents;
///
- /// Initial internal solution storage volume
+ /// Initial internal solution storage volume
///
[ViewVariables]
private ReagentUnit _initialMaxVolume;
///
- /// Time in seconds between reagents being ingested and them being transferred to
+ /// Time in seconds between reagents being ingested and them being transferred
+ /// to
///
[ViewVariables]
private float _digestionDelay;
///
- /// Used to track how long each reagent has been in the stomach
+ /// Used to track how long each reagent has been in the stomach
///
private readonly List _reagentDeltas = new List();
- ///
- /// Reference to bloodstream where digested reagents are transferred to
- ///
- private BloodstreamComponent _bloodstream;
-
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
@@ -70,14 +66,10 @@ namespace Content.Server.GameObjects.Components.Nutrition
protected override void Startup()
{
+ base.Startup();
+
_stomachContents = Owner.GetComponent();
_stomachContents.MaxVolume = _initialMaxVolume;
- if (!Owner.TryGetComponent(out _bloodstream))
- {
- Logger.Warning(_localizationManager.GetString(
- "StomachComponent entity does not have a BloodstreamComponent, which is required for it to function. Owner entity name: {0}",
- Owner.Name));
- }
}
public bool TryTransferSolution(Solution solution)
@@ -88,9 +80,9 @@ namespace Content.Server.GameObjects.Components.Nutrition
return false;
}
- //Add solution to _stomachContents
+ // Add solution to _stomachContents
_stomachContents.TryAddSolution(solution, false, true);
- //Add each reagent to _reagentDeltas. Used to track how long each reagent has been in the stomach
+ // Add each reagent to _reagentDeltas. Used to track how long each reagent has been in the stomach
foreach (var reagent in solution.Contents)
{
_reagentDeltas.Add(new ReagentDelta(reagent.ReagentId, reagent.Quantity));
@@ -100,23 +92,26 @@ namespace Content.Server.GameObjects.Components.Nutrition
}
///
- /// Updates digestion status of ingested reagents. Once reagents surpass _digestionDelay
- /// they are moved to the bloodstream, where they are then metabolized.
+ /// Updates digestion status of ingested reagents.
+ /// Once reagents surpass _digestionDelay they are moved to the bloodstream,
+ /// where they are then metabolized.
///
- /// The time since the last update in seconds.
- public void OnUpdate(float tickTime)
+ /// The time since the last update in seconds.
+ public void Update(float frameTime)
{
- if (_bloodstream == null)
+ if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream))
{
return;
}
- //Add reagents ready for transfer to bloodstream to transferSolution
+ // Add reagents ready for transfer to bloodstream to transferSolution
var transferSolution = new Solution();
- foreach (var delta in _reagentDeltas.ToList()) //Use ToList here to remove entries while iterating
+
+ // Use ToList here to remove entries while iterating
+ foreach (var delta in _reagentDeltas.ToList())
{
//Increment lifetime of reagents
- delta.Increment(tickTime);
+ delta.Increment(frameTime);
if (delta.Lifetime > _digestionDelay)
{
_stomachContents.TryRemoveReagent(delta.ReagentId, delta.Quantity);
@@ -124,12 +119,13 @@ namespace Content.Server.GameObjects.Components.Nutrition
_reagentDeltas.Remove(delta);
}
}
- //Transfer digested reagents to bloodstream
- _bloodstream.TryTransferSolution(transferSolution);
+
+ // Transfer digested reagents to bloodstream
+ bloodstream.TryTransferSolution(transferSolution);
}
///
- /// Used to track quantity changes when ingesting & digesting reagents
+ /// Used to track quantity changes when ingesting & digesting reagents
///
private class ReagentDelta
{
diff --git a/Content.Server/Health/BodySystem/BodyPart/DroppedBodyPartComponent.cs b/Content.Server/GameObjects/Components/Body/DroppedBodyPartComponent.cs
similarity index 52%
rename from Content.Server/Health/BodySystem/BodyPart/DroppedBodyPartComponent.cs
rename to Content.Server/GameObjects/Components/Body/DroppedBodyPartComponent.cs
index a49ba2c656..431f60e026 100644
--- a/Content.Server/Health/BodySystem/BodyPart/DroppedBodyPartComponent.cs
+++ b/Content.Server/GameObjects/Components/Body/DroppedBodyPartComponent.cs
@@ -1,10 +1,9 @@
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
-using Content.Shared.Health.BodySystem;
-using Content.Shared.Health.BodySystem.Surgery;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
+using Content.Server.Body;
+using Content.Shared.Body.Surgery;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.Interfaces.Player;
@@ -14,98 +13,114 @@ using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.ViewVariables;
-namespace Content.Server.Health.BodySystem.BodyPart
+namespace Content.Server.GameObjects.Components.Body
{
-
///
- /// Component representing a dropped, tangible entity.
+ /// Component representing a dropped, tangible entity.
///
[RegisterComponent]
public class DroppedBodyPartComponent : Component, IAfterInteract, IBodyPartContainer
{
-
#pragma warning disable 649
[Dependency] private readonly ISharedNotifyManager _sharedNotifyManager;
#pragma warning restore 649
- public sealed override string Name => "DroppedBodyPart";
-
- [ViewVariables]
- public BodyPart ContainedBodyPart { get; set; }
+ private readonly Dictionary _optionsCache = new Dictionary();
+ private BodyManagerComponent _bodyManagerComponentCache;
+ private int _idHash;
+ private IEntity _performerCache;
private BoundUserInterface _userInterface;
- private Dictionary _optionsCache = new Dictionary();
- private IEntity _performerCache;
- private BodyManagerComponent _bodyManagerComponentCache;
- private int _idHash = 0;
- public override void Initialize()
- {
- base.Initialize();
- _userInterface = Owner.GetComponent().GetBoundUserInterface(GenericSurgeryUiKey.Key);
- _userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
- }
+ public sealed override string Name => "DroppedBodyPart";
- public void TransferBodyPartData(BodyPart data)
- {
- ContainedBodyPart = data;
- Owner.Name = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(ContainedBodyPart.Name);
- if (Owner.TryGetComponent(out SpriteComponent component))
- {
- component.LayerSetRSI(0, data.RSIPath);
- component.LayerSetState(0, data.RSIState);
- }
- }
+ [ViewVariables] public BodyPart ContainedBodyPart { get; private set; }
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
+ {
return;
+ }
CloseAllSurgeryUIs();
_optionsCache.Clear();
_performerCache = null;
_bodyManagerComponentCache = null;
- if (eventArgs.Target.TryGetComponent(out BodyManagerComponent bodyManager))
+ if (eventArgs.Target.TryGetComponent(out BodyManagerComponent bodyManager))
{
SendBodySlotListToUser(eventArgs, bodyManager);
}
}
- private void SendBodySlotListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager)
+ public override void Initialize()
{
- var toSend = new Dictionary(); //Create dictionary to send to client (text to be shown : data sent back if selected)
+ base.Initialize();
- //Here we are trying to grab a list of all empty BodySlots adjancent to an existing BodyPart that can be attached to. i.e. an empty left hand slot, connected to an occupied left arm slot would be valid.
- List unoccupiedSlots = bodyManager.AllSlots.ToList().Except(bodyManager.OccupiedSlots.ToList()).ToList();
- foreach (string slot in unoccupiedSlots)
+ _userInterface = Owner.GetComponent()
+ .GetBoundUserInterface(GenericSurgeryUiKey.Key);
+ _userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
+ }
+
+ public void TransferBodyPartData(BodyPart data)
+ {
+ ContainedBodyPart = data;
+ Owner.Name = Loc.GetString(ContainedBodyPart.Name);
+
+ if (Owner.TryGetComponent(out SpriteComponent component))
{
- if (bodyManager.TryGetSlotType(slot, out BodyPartType typeResult) && typeResult == ContainedBodyPart.PartType)
+ component.LayerSetRSI(0, data.RSIPath);
+ component.LayerSetState(0, data.RSIState);
+
+ if (data.RSIColor.HasValue)
{
- if (bodyManager.TryGetBodyPartConnections(slot, out List bodypartResult))
- {
- foreach (BodyPart connectedPart in bodypartResult)
- {
- if (connectedPart.CanAttachBodyPart(ContainedBodyPart))
- {
- _optionsCache.Add(_idHash, slot);
- toSend.Add(slot, _idHash++);
- }
- }
- }
+ component.LayerSetColor(0, data.RSIColor.Value);
}
}
+ }
+
+ private void SendBodySlotListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager)
+ {
+ // Create dictionary to send to client (text to be shown : data sent back if selected)
+ var toSend = new Dictionary();
+
+ // Here we are trying to grab a list of all empty BodySlots adjacent to an existing BodyPart that can be
+ // attached to. i.e. an empty left hand slot, connected to an occupied left arm slot would be valid.
+ var unoccupiedSlots = bodyManager.AllSlots.ToList().Except(bodyManager.OccupiedSlots.ToList()).ToList();
+ foreach (var slot in unoccupiedSlots)
+ {
+ if (!bodyManager.TryGetSlotType(slot, out var typeResult) ||
+ typeResult != ContainedBodyPart.PartType ||
+ !bodyManager.TryGetBodyPartConnections(slot, out var parts))
+ {
+ continue;
+ }
+
+ foreach (var connectedPart in parts)
+ {
+ if (!connectedPart.CanAttachBodyPart(ContainedBodyPart))
+ {
+ continue;
+ }
+
+ _optionsCache.Add(_idHash, slot);
+ toSend.Add(slot, _idHash++);
+ }
+ }
+
if (_optionsCache.Count > 0)
{
OpenSurgeryUI(eventArgs.User.GetComponent().playerSession);
- UpdateSurgeryUIBodyPartSlotRequest(eventArgs.User.GetComponent().playerSession, toSend);
+ UpdateSurgeryUIBodyPartSlotRequest(eventArgs.User.GetComponent().playerSession,
+ toSend);
_performerCache = eventArgs.User;
_bodyManagerComponentCache = bodyManager;
}
- else //If surgery cannot be performed, show message saying so.
+ else // If surgery cannot be performed, show message saying so.
{
- _sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User, Loc.GetString("You see no way to install {0:theName}.", Owner));
+ _sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User,
+ Loc.GetString("You see no way to install {0:theName}.", Owner));
}
}
@@ -115,47 +130,50 @@ namespace Content.Server.Health.BodySystem.BodyPart
private void HandleReceiveBodyPartSlot(int key)
{
CloseSurgeryUI(_performerCache.GetComponent().playerSession);
- //TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
- if (!_optionsCache.TryGetValue(key, out object targetObject))
+
+ // TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
+ if (!_optionsCache.TryGetValue(key, out var targetObject))
{
- _sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You see no useful way to attach {0:theName} anymore.", Owner));
- }
- string target = targetObject as string;
- if (!_bodyManagerComponentCache.InstallDroppedBodyPart(this, target))
- {
- _sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You can't attach it!"));
- }
- else
- {
- _sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You attach {0:theName}.", ContainedBodyPart));
+ _sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache,
+ Loc.GetString("You see no useful way to attach {0:theName} anymore.", Owner));
}
+
+ var target = targetObject as string;
+
+ _sharedNotifyManager.PopupMessage(
+ _bodyManagerComponentCache.Owner,
+ _performerCache,
+ !_bodyManagerComponentCache.InstallDroppedBodyPart(this, target)
+ ? Loc.GetString("You can't attach it!")
+ : Loc.GetString("You attach {0:theName}.", ContainedBodyPart));
}
-
- public void OpenSurgeryUI(IPlayerSession session)
+ private void OpenSurgeryUI(IPlayerSession session)
{
_userInterface.Open(session);
}
- public void UpdateSurgeryUIBodyPartSlotRequest(IPlayerSession session, Dictionary options)
+
+ private void UpdateSurgeryUIBodyPartSlotRequest(IPlayerSession session, Dictionary options)
{
_userInterface.SendMessage(new RequestBodyPartSlotSurgeryUIMessage(options), session);
}
- public void CloseSurgeryUI(IPlayerSession session)
+
+ private void CloseSurgeryUI(IPlayerSession session)
{
_userInterface.Close(session);
}
- public void CloseAllSurgeryUIs()
+
+ private void CloseAllSurgeryUIs()
{
_userInterface.CloseAll();
}
-
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case ReceiveBodyPartSlotSurgeryUIMessage msg:
- HandleReceiveBodyPartSlot(msg.SelectedOptionID);
+ HandleReceiveBodyPartSlot(msg.SelectedOptionId);
break;
}
}
diff --git a/Content.Server/Health/BodySystem/Mechanism/DroppedMechanismComponent.cs b/Content.Server/GameObjects/Components/Body/DroppedMechanismComponent.cs
similarity index 60%
rename from Content.Server/Health/BodySystem/Mechanism/DroppedMechanismComponent.cs
rename to Content.Server/GameObjects/Components/Body/DroppedMechanismComponent.cs
index 788082c0d9..58cab75d74 100644
--- a/Content.Server/Health/BodySystem/Mechanism/DroppedMechanismComponent.cs
+++ b/Content.Server/GameObjects/Components/Body/DroppedMechanismComponent.cs
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
-using Content.Server.Health.BodySystem.BodyPart;
-using Content.Shared.Health.BodySystem.Mechanism;
-using Content.Shared.Health.BodySystem.Surgery;
+using Content.Server.Body;
+using Content.Server.Body.Mechanisms;
+using Content.Shared.Body.Mechanism;
+using Content.Shared.Body.Surgery;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
@@ -18,15 +18,14 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
-namespace Content.Server.Health.BodySystem.Mechanism {
-
+namespace Content.Server.GameObjects.Components.Body
+{
///
- /// Component representing a dropped, tangible entity.
+ /// Component representing a dropped, tangible entity.
///
[RegisterComponent]
public class DroppedMechanismComponent : Component, IAfterInteract
{
-
#pragma warning disable 649
[Dependency] private readonly ISharedNotifyManager _sharedNotifyManager;
[Dependency] private IPrototypeManager _prototypeManager;
@@ -34,98 +33,120 @@ namespace Content.Server.Health.BodySystem.Mechanism {
public sealed override string Name => "DroppedMechanism";
- [ViewVariables]
- public Mechanism ContainedMechanism { get; private set; }
+ private readonly Dictionary _optionsCache = new Dictionary();
+
+ private BodyManagerComponent _bodyManagerComponentCache;
+
+ private int _idHash;
+
+ private IEntity _performerCache;
private BoundUserInterface _userInterface;
- private Dictionary _optionsCache = new Dictionary();
- private IEntity _performerCache;
- private BodyManagerComponent _bodyManagerComponentCache;
- private int _idHash = 0;
- public override void Initialize()
- {
- base.Initialize();
- _userInterface = Owner.GetComponent().GetBoundUserInterface(GenericSurgeryUiKey.Key);
- _userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
- }
-
- public void InitializeDroppedMechanism(Mechanism data)
- {
- ContainedMechanism = data;
- Owner.Name = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(ContainedMechanism.Name);
- if (Owner.TryGetComponent(out SpriteComponent component))
- {
- component.LayerSetRSI(0, data.RSIPath);
- component.LayerSetState(0, data.RSIState);
- }
- }
+ [ViewVariables] public Mechanism ContainedMechanism { get; private set; }
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
+ {
return;
+ }
CloseAllSurgeryUIs();
_optionsCache.Clear();
_performerCache = null;
_bodyManagerComponentCache = null;
- if (eventArgs.Target.TryGetComponent(out BodyManagerComponent bodyManager))
+ if (eventArgs.Target.TryGetComponent(out var bodyManager))
{
SendBodyPartListToUser(eventArgs, bodyManager);
}
- else if (eventArgs.Target.TryGetComponent(out DroppedBodyPartComponent droppedBodyPart))
+ else if (eventArgs.Target.TryGetComponent(out var droppedBodyPart))
{
if (droppedBodyPart.ContainedBodyPart == null)
{
- Logger.Debug("Installing a mechanism was attempted on an IEntity with a DroppedBodyPartComponent that doesn't have a BodyPart in it!");
+ Logger.Debug(
+ "Installing a mechanism was attempted on an IEntity with a DroppedBodyPartComponent that doesn't have a BodyPart in it!");
throw new InvalidOperationException("A DroppedBodyPartComponent exists without a BodyPart in it!");
}
+
if (!droppedBodyPart.ContainedBodyPart.TryInstallDroppedMechanism(this))
{
- _sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User, Loc.GetString("You can't fit it in!"));
+ _sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User,
+ Loc.GetString("You can't fit it in!"));
}
}
}
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _userInterface = Owner.GetComponent()
+ .GetBoundUserInterface(GenericSurgeryUiKey.Key);
+ _userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
+ }
+
+ public void InitializeDroppedMechanism(Mechanism data)
+ {
+ ContainedMechanism = data;
+ Owner.Name = Loc.GetString(ContainedMechanism.Name);
+
+ if (Owner.TryGetComponent(out SpriteComponent component))
+ {
+ component.LayerSetRSI(0, data.RSIPath);
+ component.LayerSetState(0, data.RSIState);
+ }
+ }
+
public override void ExposeData(ObjectSerializer serializer)
{
- //This is a temporary way to have spawnable hard-coded DroppedMechanismComponent prototypes
- //In the future (when it becomes possible) DroppedMechanismComponent should be auto-generated from the Mechanism prototypes
- string debugLoadMechanismData = "";
+ // This is a temporary way to have spawnable hard-coded DroppedMechanismComponent prototypes
+ // In the future (when it becomes possible) DroppedMechanismComponent should be auto-generated from
+ // the Mechanism prototypes
+ var debugLoadMechanismData = "";
base.ExposeData(serializer);
+
serializer.DataField(ref debugLoadMechanismData, "debugLoadMechanismData", "");
+
if (serializer.Reading && debugLoadMechanismData != "")
{
_prototypeManager.TryIndex(debugLoadMechanismData, out MechanismPrototype data);
- InitializeDroppedMechanism(new Mechanism(data));
+
+ var mechanism = new Mechanism(data);
+ mechanism.EnsureInitialize();
+
+ InitializeDroppedMechanism(mechanism);
}
}
-
-
private void SendBodyPartListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager)
{
- var toSend = new Dictionary(); //Create dictionary to send to client (text to be shown : data sent back if selected)
- foreach (var (key, value) in bodyManager.PartDictionary)
- { //For each limb in the target, add it to our cache if it is a valid option.
+ // Create dictionary to send to client (text to be shown : data sent back if selected)
+ var toSend = new Dictionary();
+
+ foreach (var (key, value) in bodyManager.Parts)
+ {
+ // For each limb in the target, add it to our cache if it is a valid option.
if (value.CanInstallMechanism(ContainedMechanism))
{
_optionsCache.Add(_idHash, value);
toSend.Add(key + ": " + value.Name, _idHash++);
}
}
+
if (_optionsCache.Count > 0)
{
OpenSurgeryUI(eventArgs.User.GetComponent().playerSession);
- UpdateSurgeryUIBodyPartRequest(eventArgs.User.GetComponent().playerSession, toSend);
+ UpdateSurgeryUIBodyPartRequest(eventArgs.User.GetComponent().playerSession,
+ toSend);
_performerCache = eventArgs.User;
_bodyManagerComponentCache = bodyManager;
}
- else //If surgery cannot be performed, show message saying so.
+ else // If surgery cannot be performed, show message saying so.
{
- _sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User, Loc.GetString("You see no way to install the {0}.", Owner.Name));
+ _sharedNotifyManager.PopupMessage(eventArgs.Target, eventArgs.User,
+ Loc.GetString("You see no way to install the {0}.", Owner.Name));
}
}
@@ -135,52 +156,55 @@ namespace Content.Server.Health.BodySystem.Mechanism {
private void HandleReceiveBodyPart(int key)
{
CloseSurgeryUI(_performerCache.GetComponent().playerSession);
- //TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
- if (!_optionsCache.TryGetValue(key, out object targetObject))
+
+ // TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
+ if (!_optionsCache.TryGetValue(key, out var targetObject))
{
- _sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You see no useful way to use the {0} anymore.", Owner.Name));
- }
- BodyPart.BodyPart target = targetObject as BodyPart.BodyPart;
- if (!target.TryInstallDroppedMechanism(this))
- {
- _sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You can't fit it in!"));
- }
- else
- {
- _sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You jam the {1} inside {0:them}.", _performerCache, ContainedMechanism.Name));
+ _sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache,
+ Loc.GetString("You see no useful way to use the {0} anymore.", Owner.Name));
+ return;
}
+
+ var target = targetObject as BodyPart;
+
+ _sharedNotifyManager.PopupMessage(
+ _bodyManagerComponentCache.Owner,
+ _performerCache,
+ !target.TryInstallDroppedMechanism(this)
+ ? Loc.GetString("You can't fit it in!")
+ : Loc.GetString("You jam the {1} inside {0:them}.", _performerCache, ContainedMechanism.Name));
+
+ // TODO: {1:theName}
}
-
-
-
- public void OpenSurgeryUI(IPlayerSession session)
+ private void OpenSurgeryUI(IPlayerSession session)
{
_userInterface.Open(session);
}
- public void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary options)
+
+ private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary options)
{
_userInterface.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session);
}
- public void CloseSurgeryUI(IPlayerSession session)
+
+ private void CloseSurgeryUI(IPlayerSession session)
{
_userInterface.Close(session);
}
- public void CloseAllSurgeryUIs()
+
+ private void CloseAllSurgeryUIs()
{
_userInterface.CloseAll();
}
-
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case ReceiveBodyPartSurgeryUIMessage msg:
- HandleReceiveBodyPart(msg.SelectedOptionID);
+ HandleReceiveBodyPart(msg.SelectedOptionId);
break;
}
}
}
}
-
diff --git a/Content.Server/GameObjects/Components/Body/Respiratory/LungComponent.cs b/Content.Server/GameObjects/Components/Body/Respiratory/LungComponent.cs
new file mode 100644
index 0000000000..eb625d7a64
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Body/Respiratory/LungComponent.cs
@@ -0,0 +1,127 @@
+using System;
+using Content.Server.Atmos;
+using Content.Server.GameObjects.Components.Body.Circulatory;
+using Content.Server.Interfaces;
+using Content.Shared.Atmos;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.GameObjects.Components.Body.Respiratory
+{
+ [RegisterComponent]
+ public class LungComponent : Component, IGasMixtureHolder
+ {
+ public override string Name => "Lung";
+
+ private float _accumulatedFrameTime;
+
+ ///
+ /// The pressure that this lung exerts on the air around it
+ ///
+ [ViewVariables(VVAccess.ReadWrite)] private float Pressure { get; set; }
+
+ [ViewVariables] public GasMixture Air { get; set; } = new GasMixture();
+
+ [ViewVariables] public LungStatus Status { get; set; }
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataReadWriteFunction(
+ "volume",
+ 6,
+ vol => Air.Volume = vol,
+ () => Air.Volume);
+ serializer.DataField(this, l => l.Pressure, "pressure", 100);
+ }
+
+ public void Update(float frameTime)
+ {
+ if (Status == LungStatus.None)
+ {
+ Status = LungStatus.Inhaling;
+ }
+
+ _accumulatedFrameTime += Status switch
+ {
+ LungStatus.Inhaling => frameTime,
+ LungStatus.Exhaling => -frameTime,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ var absoluteTime = Math.Abs(_accumulatedFrameTime);
+ if (absoluteTime < 2)
+ {
+ return;
+ }
+
+ switch (Status)
+ {
+ case LungStatus.Inhaling:
+ Inhale(absoluteTime);
+ Status = LungStatus.Exhaling;
+ break;
+ case LungStatus.Exhaling:
+ Exhale(absoluteTime);
+ Status = LungStatus.Inhaling;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ _accumulatedFrameTime = absoluteTime - 2;
+ }
+
+ public void Inhale(float frameTime)
+ {
+ if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream))
+ {
+ return;
+ }
+
+ if (!Owner.Transform.GridPosition.TryGetTileAir(out var tileAir))
+ {
+ return;
+ }
+
+ var amount = Atmospherics.BreathPercentage * frameTime;
+ var volumeRatio = amount / tileAir.Volume;
+ var temp = tileAir.RemoveRatio(volumeRatio);
+
+ temp.PumpGasTo(Air, Pressure);
+ Air.PumpGasTo(bloodstream.Air, Pressure);
+ tileAir.Merge(temp);
+ }
+
+ public void Exhale(float frameTime)
+ {
+ if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream))
+ {
+ return;
+ }
+
+ if (!Owner.Transform.GridPosition.TryGetTileAir(out var tileAir))
+ {
+ return;
+ }
+
+ bloodstream.PumpToxins(Air, Pressure);
+
+ var amount = Atmospherics.BreathPercentage * frameTime;
+ var volumeRatio = amount / tileAir.Volume;
+ var temp = tileAir.RemoveRatio(volumeRatio);
+
+ temp.PumpGasTo(tileAir, Pressure);
+ Air.Merge(temp);
+ }
+ }
+
+ public enum LungStatus
+ {
+ None = 0,
+ Inhaling,
+ Exhaling
+ }
+}
diff --git a/Content.Server/Health/BodySystem/Surgery/Surgeon/SurgeryToolComponent.cs b/Content.Server/GameObjects/Components/Body/SurgeryToolComponent.cs
similarity index 55%
rename from Content.Server/Health/BodySystem/Surgery/Surgeon/SurgeryToolComponent.cs
rename to Content.Server/GameObjects/Components/Body/SurgeryToolComponent.cs
index 42e9e3f2ca..7f4ece43a0 100644
--- a/Content.Server/Health/BodySystem/Surgery/Surgeon/SurgeryToolComponent.cs
+++ b/Content.Server/GameObjects/Components/Body/SurgeryToolComponent.cs
@@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
-using Content.Server.Health.BodySystem.BodyPart;
-using Content.Server.Health.BodySystem.Mechanism;
+using Content.Server.Body;
+using Content.Server.Body.Mechanisms;
+using Content.Server.Body.Surgery;
+using Content.Shared.Body.Surgery;
using Content.Shared.GameObjects;
-using Content.Shared.Health.BodySystem;
-using Content.Shared.Health.BodySystem.Surgery;
+using Content.Shared.GameObjects.Components.Body;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.UserInterface;
+using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
@@ -17,110 +19,136 @@ using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
-namespace Content.Server.Health.BodySystem.Surgery.Surgeon
+namespace Content.Server.GameObjects.Components.Body
{
- //TODO: add checks to close UI if user walks too far away from tool or target.
+ // TODO: add checks to close UI if user walks too far away from tool or target.
///
- /// Server-side component representing a generic tool capable of performing surgery. For instance, the scalpel.
+ /// Server-side component representing a generic tool capable of performing surgery.
+ /// For instance, the scalpel.
///
[RegisterComponent]
public class SurgeryToolComponent : Component, ISurgeon, IAfterInteract
{
- public override string Name => "SurgeryTool";
- public override uint? NetID => ContentNetIDs.SURGERY;
-
#pragma warning disable 649
[Dependency] private readonly ISharedNotifyManager _sharedNotifyManager;
#pragma warning restore 649
- public float BaseOperationTime { get => _baseOperateTime; set => _baseOperateTime = value; }
+ public override string Name => "SurgeryTool";
+ public override uint? NetID => ContentNetIDs.SURGERY;
+
+ private readonly Dictionary _optionsCache = new Dictionary();
+
private float _baseOperateTime;
- private SurgeryType _surgeryType;
- private HashSet _subscribedSessions = new HashSet();
- private BoundUserInterface _userInterface;
- private Dictionary _optionsCache = new Dictionary();
- private IEntity _performerCache;
private BodyManagerComponent _bodyManagerComponentCache;
- private ISurgeon.MechanismRequestCallback _callbackCache;
- private int _idHash = 0;
- public override void Initialize()
- {
- base.Initialize();
- _userInterface = Owner.GetComponent().GetBoundUserInterface(GenericSurgeryUiKey.Key);
- _userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
- }
+ private ISurgeon.MechanismRequestCallback _callbackCache;
+
+ private int _idHash;
+
+ private IEntity _performerCache;
+
+ private SurgeryType _surgeryType;
+
+ private BoundUserInterface _userInterface;
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
+ {
return;
+ }
+
+ if (!eventArgs.User.TryGetComponent(out IActorComponent actor))
+ {
+ return;
+ }
CloseAllSurgeryUIs();
_optionsCache.Clear();
+
_performerCache = null;
_bodyManagerComponentCache = null;
_callbackCache = null;
- if (eventArgs.Target.TryGetComponent(out BodyManagerComponent bodyManager)) //Attempt surgery on a BodyManagerComponent by sending a list of operatable BodyParts to the client to choose from
+ // Attempt surgery on a BodyManagerComponent by sending a list of operable BodyParts to the client to choose from
+ if (eventArgs.Target.TryGetComponent(out BodyManagerComponent body))
{
- var toSend = new Dictionary(); //Create dictionary to send to client (text to be shown : data sent back if selected)
- foreach (var(key, value) in bodyManager.PartDictionary) { //For each limb in the target, add it to our cache if it is a valid option.
+ // Create dictionary to send to client (text to be shown : data sent back if selected)
+ var toSend = new Dictionary();
+
+ foreach (var (key, value) in body.Parts)
+ {
+ // For each limb in the target, add it to our cache if it is a valid option.
if (value.SurgeryCheck(_surgeryType))
{
_optionsCache.Add(_idHash, value);
toSend.Add(key + ": " + value.Name, _idHash++);
}
}
+
if (_optionsCache.Count > 0)
{
- OpenSurgeryUI(eventArgs.User.GetComponent().playerSession);
- UpdateSurgeryUIBodyPartRequest(eventArgs.User.GetComponent().playerSession, toSend);
- _performerCache = eventArgs.User; //Also, cache the data.
- _bodyManagerComponentCache = bodyManager;
+ OpenSurgeryUI(actor.playerSession);
+ UpdateSurgeryUIBodyPartRequest(actor.playerSession, toSend);
+ _performerCache = eventArgs.User; // Also, cache the data.
+ _bodyManagerComponentCache = body;
}
- else //If surgery cannot be performed, show message saying so.
+ else // If surgery cannot be performed, show message saying so.
{
SendNoUsefulWayToUsePopup();
}
}
- else if (eventArgs.Target.TryGetComponent(out DroppedBodyPartComponent droppedBodyPart)) //Attempt surgery on a DroppedBodyPart - there's only one possible target so no need for selection UI
+ else if (eventArgs.Target.TryGetComponent(out var droppedBodyPart))
{
+ // Attempt surgery on a DroppedBodyPart - there's only one possible target so no need for selection UI
_performerCache = eventArgs.User;
- if (droppedBodyPart.ContainedBodyPart == null) //Throw error if the DroppedBodyPart has no data in it.
+
+ if (droppedBodyPart.ContainedBodyPart == null)
{
- Logger.Debug("Surgery was attempted on an IEntity with a DroppedBodyPartComponent that doesn't have a BodyPart in it!");
+ // Throw error if the DroppedBodyPart has no data in it.
+ Logger.Debug(
+ "Surgery was attempted on an IEntity with a DroppedBodyPartComponent that doesn't have a BodyPart in it!");
throw new InvalidOperationException("A DroppedBodyPartComponent exists without a BodyPart in it!");
}
- if (droppedBodyPart.ContainedBodyPart.SurgeryCheck(_surgeryType)) //If surgery can be performed...
- {
- if (!droppedBodyPart.ContainedBodyPart.AttemptSurgery(_surgeryType, droppedBodyPart, this, eventArgs.User)) //...do the surgery.
- {
- Logger.Debug("Error when trying to perform surgery on bodypart " + eventArgs.User.Name + "!"); //Log error if the surgery fails somehow.
- throw new InvalidOperationException();
- }
- }
- else //If surgery cannot be performed, show message saying so.
+
+ // If surgery can be performed...
+ if (!droppedBodyPart.ContainedBodyPart.SurgeryCheck(_surgeryType))
{
SendNoUsefulWayToUsePopup();
+ return;
}
+
+ //...do the surgery.
+ if (droppedBodyPart.ContainedBodyPart.AttemptSurgery(_surgeryType, droppedBodyPart, this,
+ eventArgs.User))
+ {
+ return;
+ }
+
+ // Log error if the surgery fails somehow.
+ Logger.Debug($"Error when trying to perform surgery on ${nameof(BodyPart)} {eventArgs.User.Name}");
+ throw new InvalidOperationException();
}
}
- public void RequestMechanism(List options, ISurgeon.MechanismRequestCallback callback)
+ public float BaseOperationTime { get => _baseOperateTime; set => _baseOperateTime = value; }
+
+ public void RequestMechanism(IEnumerable options, ISurgeon.MechanismRequestCallback callback)
{
- var toSend = new Dictionary ();
- foreach (Mechanism.Mechanism mechanism in options)
+ var toSend = new Dictionary();
+ foreach (var mechanism in options)
{
_optionsCache.Add(_idHash, mechanism);
toSend.Add(mechanism.Name, _idHash++);
}
+
if (_optionsCache.Count > 0)
{
OpenSurgeryUI(_performerCache.GetComponent().playerSession);
- UpdateSurgeryUIMechanismRequest(_performerCache.GetComponent().playerSession, toSend);
+ UpdateSurgeryUIMechanismRequest(_performerCache.GetComponent().playerSession,
+ toSend);
_callbackCache = callback;
}
else
@@ -130,95 +158,112 @@ namespace Content.Server.Health.BodySystem.Surgery.Surgeon
}
}
+ public override void Initialize()
+ {
+ base.Initialize();
+ _userInterface = Owner.GetComponent()
+ .GetBoundUserInterface(GenericSurgeryUiKey.Key);
+ _userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
+ }
-
-
-
-
- public void OpenSurgeryUI(IPlayerSession session)
+ private void OpenSurgeryUI(IPlayerSession session)
{
_userInterface.Open(session);
}
- public void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary options)
+
+ private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary options)
{
_userInterface.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session);
}
- public void UpdateSurgeryUIMechanismRequest(IPlayerSession session, Dictionary options)
+
+ private void UpdateSurgeryUIMechanismRequest(IPlayerSession session, Dictionary options)
{
_userInterface.SendMessage(new RequestMechanismSurgeryUIMessage(options), session);
}
- public void CloseSurgeryUI(IPlayerSession session)
+
+ private void CloseSurgeryUI(IPlayerSession session)
{
_userInterface.Close(session);
}
- public void CloseAllSurgeryUIs()
+
+ private void CloseAllSurgeryUIs()
{
_userInterface.CloseAll();
}
-
-
-
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case ReceiveBodyPartSurgeryUIMessage msg:
- HandleReceiveBodyPart(msg.SelectedOptionID);
+ HandleReceiveBodyPart(msg.SelectedOptionId);
break;
case ReceiveMechanismSurgeryUIMessage msg:
- HandleReceiveMechanism(msg.SelectedOptionID);
+ HandleReceiveMechanism(msg.SelectedOptionId);
break;
}
}
///
- /// Called after the client chooses from a list of possible BodyParts that can be operated on.
+ /// Called after the client chooses from a list of possible
+ /// that can be operated on.
///
private void HandleReceiveBodyPart(int key)
{
CloseSurgeryUI(_performerCache.GetComponent().playerSession);
- //TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
- if (!_optionsCache.TryGetValue(key, out object targetObject))
- {
- SendNoUsefulWayToUseAnymorePopup();
- }
- BodyPart.BodyPart target = targetObject as BodyPart.BodyPart;
- if (!target.AttemptSurgery(_surgeryType, _bodyManagerComponentCache, this, _performerCache))
+ // TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
+ if (!_optionsCache.TryGetValue(key, out var targetObject))
{
SendNoUsefulWayToUseAnymorePopup();
}
- }
- ///
- /// Called after the client chooses from a list of possible Mechanisms to choose from.
- ///
- private void HandleReceiveMechanism(int key)
- {
- //TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
- if (!_optionsCache.TryGetValue(key, out object targetObject))
+ var target = targetObject as BodyPart;
+
+ if (!target.AttemptSurgery(_surgeryType, _bodyManagerComponentCache, this, _performerCache))
{
SendNoUsefulWayToUseAnymorePopup();
}
- Mechanism.Mechanism target = targetObject as Mechanism.Mechanism;
+ }
+
+ ///
+ /// Called after the client chooses from a list of possible
+ /// to choose from.
+ ///
+ private void HandleReceiveMechanism(int key)
+ {
+ // TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
+ if (!_optionsCache.TryGetValue(key, out var targetObject))
+ {
+ SendNoUsefulWayToUseAnymorePopup();
+ }
+
+ var target = targetObject as Mechanism;
+
CloseSurgeryUI(_performerCache.GetComponent().playerSession);
_callbackCache(target, _bodyManagerComponentCache, this, _performerCache);
}
private void SendNoUsefulWayToUsePopup()
{
- _sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You see no useful way to use {0:theName}.", Owner));
+ _sharedNotifyManager.PopupMessage(
+ _bodyManagerComponentCache.Owner,
+ _performerCache,
+ Loc.GetString("You see no useful way to use {0:theName}.", Owner));
}
private void SendNoUsefulWayToUseAnymorePopup()
{
- _sharedNotifyManager.PopupMessage(_bodyManagerComponentCache.Owner, _performerCache, Loc.GetString("You see no useful way to use {0:theName} anymore.", Owner));
+ _sharedNotifyManager.PopupMessage(
+ _bodyManagerComponentCache.Owner,
+ _performerCache,
+ Loc.GetString("You see no useful way to use {0:theName} anymore.", Owner));
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
+
serializer.DataField(ref _surgeryType, "surgeryType", SurgeryType.Incision);
serializer.DataField(ref _baseOperateTime, "baseOperateTime", 5);
}
diff --git a/Content.Server/GameObjects/Components/Buckle/BuckleComponent.cs b/Content.Server/GameObjects/Components/Buckle/BuckleComponent.cs
index 5c9870c53f..877a0c2727 100644
--- a/Content.Server/GameObjects/Components/Buckle/BuckleComponent.cs
+++ b/Content.Server/GameObjects/Components/Buckle/BuckleComponent.cs
@@ -373,9 +373,9 @@ namespace Content.Server.GameObjects.Components.Buckle
StandingStateHelper.Standing(Owner);
}
- if (Owner.TryGetComponent(out SpeciesComponent species))
+ if (Owner.TryGetComponent(out MobStateManagerComponent stateManager))
{
- species.CurrentDamageState.EnterState(Owner);
+ stateManager.CurrentMobState.EnterState(Owner);
}
BuckleStatus();
diff --git a/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs b/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs
index 9f78004304..03ac3855e7 100644
--- a/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs
+++ b/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs
@@ -1,5 +1,5 @@
using System;
-using Content.Server.GameObjects.Components.Metabolism;
+using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.Interfaces;
using Content.Server.Utility;
using Content.Shared.Chemistry;
@@ -134,7 +134,8 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
else //Handle injecting into bloodstream
{
- if (targetEntity.TryGetComponent(out var bloodstream) && _toggleState == InjectorToggleMode.Inject)
+ if (targetEntity.TryGetComponent(out BloodstreamComponent bloodstream) &&
+ _toggleState == InjectorToggleMode.Inject)
{
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
}
diff --git a/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs b/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs
index 6d682e594f..7ff05c4398 100644
--- a/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs
+++ b/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs
@@ -1,4 +1,5 @@
-using Content.Server.GameObjects.Components.Nutrition;
+using Content.Server.GameObjects.Components.Body.Digestive;
+using Content.Server.GameObjects.Components.Nutrition;
using Content.Server.GameObjects.Components.Utensil;
using Content.Server.Utility;
using Content.Shared.Chemistry;
diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs
index 5218f996ce..53e3ca2463 100644
--- a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs
+++ b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs
@@ -27,7 +27,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// ECS component that manages a liquid solution of reagents.
///
[RegisterComponent]
- internal class SolutionComponent : SharedSolutionComponent, IExamine
+ public class SolutionComponent : SharedSolutionComponent, IExamine
{
#pragma warning disable 649
[Dependency] private readonly IPrototypeManager _prototypeManager;
diff --git a/Content.Server/GameObjects/Components/Damage/BreakableComponent.cs b/Content.Server/GameObjects/Components/Damage/BreakableComponent.cs
index 0a9e53e03a..15ad3e8a35 100644
--- a/Content.Server/GameObjects/Components/Damage/BreakableComponent.cs
+++ b/Content.Server/GameObjects/Components/Damage/BreakableComponent.cs
@@ -1,39 +1,59 @@
using System.Collections.Generic;
-using Content.Server.GameObjects.EntitySystems;
-using Content.Server.Interfaces.GameObjects;
using Content.Shared.GameObjects.Components.Damage;
+using Content.Shared.GameObjects.EntitySystems;
+using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Random;
-using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Damage
{
+ // TODO: Repair needs to set CurrentDamageState to DamageState.Alive, but it doesn't exist... should be easy enough if it's just an interface you can slap on BreakableComponent
+
+ ///
+ /// When attached to an , allows it to take damage and sets it to a "broken state" after taking
+ /// enough damage.
+ ///
[RegisterComponent]
- public class BreakableComponent : Component, IOnDamageBehavior, IExAct
+ [ComponentReference(typeof(IDamageableComponent))]
+ public class BreakableComponent : RuinableComponent, IExAct
{
-
- #pragma warning disable 649
+#pragma warning disable 649
[Dependency] private readonly IEntitySystemManager _entitySystemManager;
- #pragma warning restore 649
- ///
- public override string Name => "Breakable";
- public DamageThreshold Threshold { get; private set; }
+ [Dependency] private readonly IRobustRandom _random;
+#pragma warning restore 649
- public DamageType damageType = DamageType.Total;
- public int damageValue = 0;
- public bool broken = false;
+ public override string Name => "Breakable";
private ActSystem _actSystem;
+ private DamageState _currentDamageState;
- public override void ExposeData(ObjectSerializer serializer)
+ public override List SupportedDamageStates =>
+ new List {DamageState.Alive, DamageState.Dead};
+
+ public override DamageState CurrentDamageState => _currentDamageState;
+
+ void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
{
- base.ExposeData(serializer);
+ switch (eventArgs.Severity)
+ {
+ case ExplosionSeverity.Destruction:
+ PerformDestruction();
+ break;
+ case ExplosionSeverity.Heavy:
+ PerformDestruction();
+ break;
+ case ExplosionSeverity.Light:
+ if (_random.Prob(0.5f))
+ {
+ PerformDestruction();
+ }
- serializer.DataField(ref damageValue, "thresholdvalue", 100);
- serializer.DataField(ref damageType, "thresholdtype", DamageType.Total);
+ break;
+ }
}
public override void Initialize()
@@ -42,38 +62,21 @@ namespace Content.Server.GameObjects.Components.Damage
_actSystem = _entitySystemManager.GetEntitySystem();
}
- public List GetAllDamageThresholds()
+ // Might want to move this down and have a more standardized method of revival
+ public void FixAllDamage()
{
- Threshold = new DamageThreshold(damageType, damageValue, ThresholdType.Breakage);
- return new List() {Threshold};
+ Heal();
+ _currentDamageState = DamageState.Alive;
}
- public void OnDamageThresholdPassed(object obj, DamageThresholdPassedEventArgs e)
+ protected override void DestructionBehavior()
{
- if (e.Passed && e.DamageThreshold == Threshold && broken == false)
+ _actSystem.HandleBreakage(Owner);
+ if (!Owner.Deleted && DestroySound != string.Empty)
{
- broken = true;
- _actSystem.HandleBreakage(Owner);
+ var pos = Owner.Transform.GridPosition;
+ EntitySystem.Get().PlayAtCoords(DestroySound, pos);
}
}
-
- public void OnExplosion(ExplosionEventArgs eventArgs)
- {
- var prob = IoCManager.Resolve();
- switch (eventArgs.Severity)
- {
- case ExplosionSeverity.Destruction:
- _actSystem.HandleBreakage(Owner);
- break;
- case ExplosionSeverity.Heavy:
- _actSystem.HandleBreakage(Owner);
- break;
- case ExplosionSeverity.Light:
- if(prob.Prob(0.4f))
- _actSystem.HandleBreakage(Owner);
- break;
- }
- }
-
}
}
diff --git a/Content.Server/GameObjects/Components/Damage/DamageOnHighSpeedImpactComponent.cs b/Content.Server/GameObjects/Components/Damage/DamageOnHighSpeedImpactComponent.cs
index 0dff2a90f2..756e417e0e 100644
--- a/Content.Server/GameObjects/Components/Damage/DamageOnHighSpeedImpactComponent.cs
+++ b/Content.Server/GameObjects/Components/Damage/DamageOnHighSpeedImpactComponent.cs
@@ -1,6 +1,7 @@
using System;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Audio;
+using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
@@ -23,7 +24,7 @@ namespace Content.Server.GameObjects.Components.Damage
public override string Name => "DamageOnHighSpeedImpact";
- public DamageType Damage { get; set; } = DamageType.Brute;
+ public DamageType Damage { get; set; } = DamageType.Blunt;
public float MinimumSpeed { get; set; } = 20f;
public int BaseDamage { get; set; } = 5;
public float Factor { get; set; } = 0.75f;
@@ -38,7 +39,7 @@ namespace Content.Server.GameObjects.Components.Damage
{
base.ExposeData(serializer);
- serializer.DataField(this, x => Damage, "damage", DamageType.Brute);
+ serializer.DataField(this, x => Damage, "damage", DamageType.Blunt);
serializer.DataField(this, x => MinimumSpeed, "minimumSpeed", 20f);
serializer.DataField(this, x => BaseDamage, "baseDamage", 5);
serializer.DataField(this, x => Factor, "factor", 1f);
@@ -51,7 +52,7 @@ namespace Content.Server.GameObjects.Components.Damage
public void CollideWith(IEntity collidedWith)
{
- if (!Owner.TryGetComponent(out ICollidableComponent collidable) || !Owner.TryGetComponent(out DamageableComponent damageable)) return;
+ if (!Owner.TryGetComponent(out ICollidableComponent collidable) || !Owner.TryGetComponent(out IDamageableComponent damageable)) return;
var speed = collidable.LinearVelocity.Length;
@@ -70,7 +71,7 @@ namespace Content.Server.GameObjects.Components.Damage
if (Owner.TryGetComponent(out StunnableComponent stun) && _robustRandom.Prob(StunChance))
stun.Stun(StunSeconds);
- damageable.TakeDamage(Damage, damage, collidedWith, Owner);
+ damageable.ChangeDamage(Damage, damage, false, collidedWith);
}
}
}
diff --git a/Content.Server/GameObjects/Components/Damage/DamageOnToolInteractComponent.cs b/Content.Server/GameObjects/Components/Damage/DamageOnToolInteractComponent.cs
index 836dd9d000..034d92c9cf 100644
--- a/Content.Server/GameObjects/Components/Damage/DamageOnToolInteractComponent.cs
+++ b/Content.Server/GameObjects/Components/Damage/DamageOnToolInteractComponent.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Interactable;
-using Content.Shared.GameObjects.Components.Damage;
+using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
@@ -9,7 +9,7 @@ using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Damage
{
[RegisterComponent]
- class DamageOnToolInteractComponent : Component, IInteractUsing
+ public class DamageOnToolInteractComponent : Component, IInteractUsing
{
public override string Name => "DamageOnToolInteract";
@@ -29,7 +29,7 @@ namespace Content.Server.GameObjects.Components.Damage
public override void Initialize()
{
base.Initialize();
- Owner.EnsureComponent();
+ Owner.EnsureComponent();
}
public bool InteractUsing(InteractUsingEventArgs eventArgs)
@@ -40,12 +40,12 @@ namespace Content.Server.GameObjects.Components.Damage
{
if (tool.HasQuality(ToolQuality.Welding) && toolQuality == ToolQuality.Welding)
{
- if (eventArgs.Using.TryGetComponent(out WelderComponent welder))
- {
+ if (eventArgs.Using.TryGetComponent(out WelderComponent welder))
+ {
if (welder.WelderLit) return CallDamage(eventArgs, tool);
- }
+ }
break; //If the tool quality is welding and its not lit or its not actually a welder that can be lit then its pointless to continue.
- }
+ }
if (tool.HasQuality(toolQuality)) return CallDamage(eventArgs, tool);
}
@@ -55,14 +55,17 @@ namespace Content.Server.GameObjects.Components.Damage
protected bool CallDamage(InteractUsingEventArgs eventArgs, ToolComponent tool)
{
- if (eventArgs.Target.TryGetComponent(out var damageable))
+ if (eventArgs.Target.TryGetComponent(out var damageable))
{
- if(tool.HasQuality(ToolQuality.Welding)) damageable.TakeDamage(DamageType.Heat, Damage, eventArgs.Using, eventArgs.User);
- else
- damageable.TakeDamage(DamageType.Brute, Damage, eventArgs.Using, eventArgs.User);
+ damageable.ChangeDamage(tool.HasQuality(ToolQuality.Welding)
+ ? DamageType.Heat
+ : DamageType.Blunt,
+ Damage, false, eventArgs.User);
+
return true;
}
- return false;
+
+ return false;
}
}
}
diff --git a/Content.Server/GameObjects/Components/Damage/DamageThreshold.cs b/Content.Server/GameObjects/Components/Damage/DamageThreshold.cs
deleted file mode 100644
index 1efd2ba7a4..0000000000
--- a/Content.Server/GameObjects/Components/Damage/DamageThreshold.cs
+++ /dev/null
@@ -1,98 +0,0 @@
-using System;
-using Content.Shared.GameObjects.Components.Damage;
-using Robust.Shared.Interfaces.GameObjects;
-
-namespace Content.Server.GameObjects.Components.Damage
-{
- ///
- /// Triggers an event when values rise above or drop below this threshold
- ///
- public struct DamageThreshold
- {
- public DamageType DamageType { get; }
- public int Value { get; }
- public ThresholdType ThresholdType { get; }
-
- public DamageThreshold(DamageType damageType, int value, ThresholdType thresholdType)
- {
- DamageType = damageType;
- Value = value;
- ThresholdType = thresholdType;
- }
-
- public override bool Equals(Object obj)
- {
- return obj is DamageThreshold threshold && this == threshold;
- }
- public override int GetHashCode()
- {
- return DamageType.GetHashCode() ^ Value.GetHashCode();
- }
- public static bool operator ==(DamageThreshold x, DamageThreshold y)
- {
- return x.DamageType == y.DamageType && x.Value == y.Value;
- }
- public static bool operator !=(DamageThreshold x, DamageThreshold y)
- {
- return !(x == y);
- }
- }
-
- public enum ThresholdType
- {
- None,
- Destruction,
- Death,
- Critical,
- HUDUpdate,
- Breakage,
- }
-
- public class DamageThresholdPassedEventArgs : EventArgs
- {
- public DamageThreshold DamageThreshold { get; }
- public bool Passed { get; }
- public int ExcessDamage { get; }
-
- public DamageThresholdPassedEventArgs(DamageThreshold threshold, bool passed, int excess)
- {
- DamageThreshold = threshold;
- Passed = passed;
- ExcessDamage = excess;
- }
- }
-
- public class DamageEventArgs : EventArgs
- {
- ///
- /// Type of damage.
- ///
- public DamageType Type { get; }
-
- ///
- /// Change in damage.
- ///
- public int Damage { get; }
-
- ///
- /// The entity that damaged this one.
- /// Could be null.
- ///
- public IEntity Source { get; }
-
- ///
- /// The mob entity that damaged this one.
- /// Could be null.
- ///
- public IEntity SourceMob { get; }
-
- public DamageEventArgs(DamageType type, int damage, IEntity source, IEntity sourceMob)
- {
- Type = type;
- Damage = damage;
- Source = source;
- SourceMob = sourceMob;
- }
- }
-}
-
diff --git a/Content.Server/GameObjects/Components/Damage/DamageableComponent.cs b/Content.Server/GameObjects/Components/Damage/DamageableComponent.cs
deleted file mode 100644
index 613355bce2..0000000000
--- a/Content.Server/GameObjects/Components/Damage/DamageableComponent.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Content.Server.Interfaces.GameObjects;
-using Content.Server.Interfaces.GameObjects.Components.Damage;
-using Content.Shared.GameObjects.Components.Damage;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Interfaces.GameObjects;
-using Robust.Shared.Serialization;
-using Robust.Shared.ViewVariables;
-
-namespace Content.Server.GameObjects.Components.Damage
-{
- //TODO: add support for component add/remove
-
- ///
- /// A component that handles receiving damage and healing,
- /// as well as informing other components of it.
- ///
- [RegisterComponent]
- public class DamageableComponent : SharedDamageableComponent, IDamageableComponent
- {
- ///
- public override string Name => "Damageable";
-
- ///
- /// The resistance set of this object.
- /// Affects receiving damage of various types.
- ///
- [ViewVariables]
- public ResistanceSet Resistances { get; private set; }
-
- [ViewVariables]
- public IReadOnlyDictionary CurrentDamage => _currentDamage;
- private Dictionary _currentDamage = new Dictionary();
-
- [ViewVariables]
- public Dictionary> Thresholds = new Dictionary>();
-
- public event EventHandler DamageThresholdPassed;
- public event EventHandler Damaged;
-
- public override ComponentState GetComponentState()
- {
- return new DamageComponentState(_currentDamage);
- }
-
- public override void ExposeData(ObjectSerializer serializer)
- {
- base.ExposeData(serializer);
- serializer.DataField(this, x => Resistances, "resistances", ResistanceSet.DefaultResistanceSet);
- }
-
- public bool IsDead()
- {
- var currentDamage = _currentDamage[DamageType.Total];
- foreach (var threshold in Thresholds[DamageType.Total])
- {
- if (threshold.Value <= currentDamage)
- {
- if (threshold.ThresholdType != ThresholdType.Death) continue;
- return true;
- }
- }
-
- return false;
- }
-
- ///
- public override void Initialize()
- {
- base.Initialize();
- InitializeDamageType(DamageType.Total);
-
- foreach (var damagebehavior in Owner.GetAllComponents())
- {
- AddThresholdsFrom(damagebehavior);
- Damaged += damagebehavior.OnDamaged;
- }
-
- RecalculateComponentThresholds();
- }
-
- ///
- public void TakeDamage(DamageType damageType, int amount, IEntity source = null, IEntity sourceMob = null)
- {
- if (damageType == DamageType.Total)
- {
- foreach (DamageType e in Enum.GetValues(typeof(DamageType)))
- {
- if (e == damageType) continue;
- TakeDamage(e, amount, source, sourceMob);
- }
-
- return;
- }
- InitializeDamageType(damageType);
-
- int oldValue = _currentDamage[damageType];
- int oldTotalValue = -1;
-
- if (amount == 0)
- {
- return;
- }
-
- amount = Resistances.CalculateDamage(damageType, amount);
- _currentDamage[damageType] = Math.Max(0, _currentDamage[damageType] + amount);
- UpdateForDamageType(damageType, oldValue);
-
- Damaged?.Invoke(this, new DamageEventArgs(damageType, amount, source, sourceMob));
-
- if (Resistances.AppliesToTotal(damageType))
- {
- oldTotalValue = _currentDamage[DamageType.Total];
- _currentDamage[DamageType.Total] = Math.Max(0, _currentDamage[DamageType.Total] + amount);
- UpdateForDamageType(DamageType.Total, oldTotalValue);
- }
- }
-
- ///
- public void TakeHealing(DamageType damageType, int amount, IEntity source = null, IEntity sourceMob = null)
- {
- if (damageType == DamageType.Total)
- {
- throw new ArgumentException("Cannot heal for DamageType.Total");
- }
- TakeDamage(damageType, -amount, source, sourceMob);
- }
-
- public void HealAllDamage()
- {
- var values = Enum.GetValues(typeof(DamageType)).Cast();
- foreach (var damageType in values)
- {
- if (CurrentDamage.ContainsKey(damageType) && damageType != DamageType.Total)
- {
- TakeHealing(damageType, CurrentDamage[damageType]);
- }
- }
- }
-
- void UpdateForDamageType(DamageType damageType, int oldValue)
- {
- int change = _currentDamage[damageType] - oldValue;
-
- if (change == 0)
- {
- return;
- }
-
- int changeSign = Math.Sign(change);
-
- foreach (var threshold in Thresholds[damageType])
- {
- var value = threshold.Value;
- if (((value * changeSign) > (oldValue * changeSign)) && ((value * changeSign) <= (_currentDamage[damageType] * changeSign)))
- {
- var excessDamage = change - value;
- var typeOfDamage = damageType;
- if (change - value < 0)
- {
- excessDamage = 0;
- }
- var args = new DamageThresholdPassedEventArgs(threshold, (changeSign > 0), excessDamage);
- DamageThresholdPassed?.Invoke(this, args);
- }
- }
- }
-
- void RecalculateComponentThresholds()
- {
- foreach (IOnDamageBehavior onDamageBehaviorComponent in Owner.GetAllComponents())
- {
- AddThresholdsFrom(onDamageBehaviorComponent);
- }
- }
-
- void AddThresholdsFrom(IOnDamageBehavior onDamageBehavior)
- {
- if (onDamageBehavior == null)
- {
- throw new ArgumentNullException(nameof(onDamageBehavior));
- }
-
- List thresholds = onDamageBehavior.GetAllDamageThresholds();
-
- if (thresholds == null)
- return;
-
- foreach (DamageThreshold threshold in thresholds)
- {
- if (!Thresholds[threshold.DamageType].Contains(threshold))
- {
- Thresholds[threshold.DamageType].Add(threshold);
- }
- }
-
- DamageThresholdPassed += onDamageBehavior.OnDamageThresholdPassed;
- }
-
- void InitializeDamageType(DamageType damageType)
- {
- if (!_currentDamage.ContainsKey(damageType))
- {
- _currentDamage.Add(damageType, 0);
- Thresholds.Add(damageType, new List());
- }
- }
- }
-}
-
diff --git a/Content.Server/GameObjects/Components/Damage/DestructibleComponent.cs b/Content.Server/GameObjects/Components/Damage/DestructibleComponent.cs
index 86a7abb03f..b808bc6c76 100644
--- a/Content.Server/GameObjects/Components/Damage/DestructibleComponent.cs
+++ b/Content.Server/GameObjects/Components/Damage/DestructibleComponent.cs
@@ -1,55 +1,49 @@
-using System.Collections.Generic;
-using Content.Server.GameObjects.EntitySystems;
-using Content.Server.Interfaces.GameObjects;
-using Content.Shared.GameObjects.Components.Damage;
+using Content.Shared.GameObjects.Components.Damage;
+using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
-using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
-using Robust.Shared.Random;
using Robust.Shared.Serialization;
-using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Damage
{
///
- /// Deletes the entity once a certain damage threshold has been reached.
+ /// When attached to an , allows it to take damage and deletes it after taking enough damage.
///
[RegisterComponent]
- public class DestructibleComponent : Component, IOnDamageBehavior, IDestroyAct, IExAct
+ [ComponentReference(typeof(IDamageableComponent))]
+ public class DestructibleComponent : RuinableComponent, IDestroyAct
{
- #pragma warning disable 649
+#pragma warning disable 649
[Dependency] private readonly IEntitySystemManager _entitySystemManager;
- #pragma warning restore 649
+#pragma warning restore 649
+
+ protected ActSystem _actSystem;
+
+ protected string _spawnOnDestroy;
///
public override string Name => "Destructible";
///
- /// Damage threshold calculated from the values
- /// given in the prototype declaration.
+ /// Entity spawned upon destruction.
///
- [ViewVariables]
- public DamageThreshold Threshold { get; private set; }
+ public string SpawnOnDestroy => _spawnOnDestroy;
- public DamageType damageType = DamageType.Total;
- public int damageValue = 0;
- public string spawnOnDestroy = "";
- public string destroySound = "";
- public bool destroyed = false;
-
- ActSystem _actSystem;
+ void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
+ {
+ if (!string.IsNullOrWhiteSpace(_spawnOnDestroy) && eventArgs.IsSpawnWreck)
+ {
+ Owner.EntityManager.SpawnEntity(_spawnOnDestroy, Owner.Transform.GridPosition);
+ }
+ }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
-
- serializer.DataField(ref damageValue, "thresholdvalue", 100);
- serializer.DataField(ref damageType, "thresholdtype", DamageType.Total);
- serializer.DataField(ref spawnOnDestroy, "spawnondestroy", "");
- serializer.DataField(ref destroySound, "destroysound", "");
+ serializer.DataField(ref _spawnOnDestroy, "spawnondestroy", string.Empty);
}
public override void Initialize()
@@ -58,57 +52,18 @@ namespace Content.Server.GameObjects.Components.Damage
_actSystem = _entitySystemManager.GetEntitySystem();
}
- ///
- List IOnDamageBehavior.GetAllDamageThresholds()
- {
- Threshold = new DamageThreshold(damageType, damageValue, ThresholdType.Destruction);
- return new List() { Threshold };
- }
- ///
- void IOnDamageBehavior.OnDamageThresholdPassed(object obj, DamageThresholdPassedEventArgs e)
+ protected override void DestructionBehavior()
{
- if (e.Passed && e.DamageThreshold == Threshold && destroyed == false)
+ if (!Owner.Deleted)
{
- destroyed = true;
var pos = Owner.Transform.GridPosition;
- _actSystem.HandleDestruction(Owner, true);
- if(destroySound != string.Empty)
+ _actSystem.HandleDestruction(Owner,
+ true); //This will call IDestroyAct.OnDestroy on this component (and all other components on this entity)
+ if (DestroySound != string.Empty)
{
- EntitySystem.Get().PlayAtCoords(destroySound, pos);
+ EntitySystem.Get().PlayAtCoords(DestroySound, pos);
}
-
-
- }
-
- }
-
- void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
- {
- var prob = IoCManager.Resolve();
- switch (eventArgs.Severity)
- {
- case ExplosionSeverity.Destruction:
- _actSystem.HandleDestruction(Owner, false);
- break;
- case ExplosionSeverity.Heavy:
- var spawnWreckOnHeavy = prob.Prob(0.5f);
- _actSystem.HandleDestruction(Owner, spawnWreckOnHeavy);
- break;
- case ExplosionSeverity.Light:
- if (prob.Prob(0.4f))
- _actSystem.HandleDestruction(Owner, true);
- break;
- }
-
- }
-
-
- void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
- {
- if (!string.IsNullOrWhiteSpace(spawnOnDestroy) && eventArgs.IsSpawnWreck)
- {
- Owner.EntityManager.SpawnEntity(spawnOnDestroy, Owner.Transform.GridPosition);
}
}
}
diff --git a/Content.Server/GameObjects/Components/Damage/ResistanceSet.cs b/Content.Server/GameObjects/Components/Damage/ResistanceSet.cs
deleted file mode 100644
index e9a5e941a1..0000000000
--- a/Content.Server/GameObjects/Components/Damage/ResistanceSet.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Content.Shared.GameObjects.Components.Damage;
-using Robust.Shared.Interfaces.Serialization;
-using Robust.Shared.Serialization;
-using Robust.Shared.ViewVariables;
-
-namespace Content.Server.GameObjects.Components.Damage
-{
- ///
- /// Resistance set used by damageable objects.
- /// For each damage type, has a coefficient, damage reduction and "included in total" value.
- ///
- public class ResistanceSet : IExposeData
- {
- public static ResistanceSet DefaultResistanceSet = new ResistanceSet();
-
- [ViewVariables]
- private readonly Dictionary _resistances = new Dictionary();
-
- public ResistanceSet()
- {
- foreach (DamageType damageType in Enum.GetValues(typeof(DamageType)))
- {
- _resistances[damageType] = new ResistanceSetSettings();
- }
- }
-
- public void ExposeData(ObjectSerializer serializer)
- {
- foreach (DamageType damageType in Enum.GetValues(typeof(DamageType)))
- {
- var resistanceName = damageType.ToString().ToLower();
- serializer.DataReadFunction(resistanceName, new ResistanceSetSettings(), resistanceSetting =>
- {
- _resistances[damageType] = resistanceSetting;
- });
- }
- }
-
- ///
- /// Adjusts input damage with the resistance set values.
- ///
- /// Type of the damage.
- /// Incoming amount of the damage.
- /// Damage adjusted by the resistance set.
- public int CalculateDamage(DamageType damageType, int amount)
- {
- if (amount > 0) //if it's damage, reduction applies
- {
- amount -= _resistances[damageType].DamageReduction;
-
- if (amount <= 0)
- return 0;
- }
-
- amount = (int)Math.Floor(amount * _resistances[damageType].Coefficient);
-
- return amount;
- }
-
- public bool AppliesToTotal(DamageType damageType)
- {
- //Damage that goes straight to total (for whatever reason) never applies twice
-
- return damageType != DamageType.Total && _resistances[damageType].AppliesToTotal;
- }
-
- ///
- /// Settings for a specific damage type in a resistance set.
- ///
- public class ResistanceSetSettings : IExposeData
- {
- public float Coefficient { get; private set; } = 1;
- public int DamageReduction { get; private set; } = 0;
- public bool AppliesToTotal { get; private set; } = true;
-
- public void ExposeData(ObjectSerializer serializer)
- {
- serializer.DataField(this, x => Coefficient, "coefficient", 1);
- serializer.DataField(this, x => DamageReduction, "damageReduction", 0);
- serializer.DataField(this, x => AppliesToTotal, "appliesToTotal", true);
- }
- }
- }
-}
diff --git a/Content.Server/GameObjects/Components/Damage/RuinableComponent.cs b/Content.Server/GameObjects/Components/Damage/RuinableComponent.cs
new file mode 100644
index 0000000000..7137171f34
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Damage/RuinableComponent.cs
@@ -0,0 +1,84 @@
+using System.Collections.Generic;
+using Content.Shared.GameObjects.Components.Damage;
+using Robust.Server.GameObjects.EntitySystems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Systems;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Serialization;
+
+namespace Content.Server.GameObjects.Components.Damage
+{
+ ///
+ /// When attached to an , allows it to take damage and
+ /// "ruins" or "destroys" it after enough damage is taken.
+ ///
+ [ComponentReference(typeof(IDamageableComponent))]
+ public abstract class RuinableComponent : DamageableComponent
+ {
+ private DamageState _currentDamageState;
+
+ ///
+ /// How much HP this component can sustain before triggering
+ /// .
+ ///
+ public int MaxHp { get; private set; }
+
+ ///
+ /// Sound played upon destruction.
+ ///
+ protected string DestroySound { get; private set; }
+
+ public override List SupportedDamageStates =>
+ new List {DamageState.Alive, DamageState.Dead};
+
+ public override DamageState CurrentDamageState => _currentDamageState;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ HealthChangedEvent += OnHealthChanged;
+ }
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(this, ruinable => ruinable.MaxHp, "maxHP", 100);
+ serializer.DataField(this, ruinable => ruinable.DestroySound, "destroySound", string.Empty);
+ }
+
+ public override void OnRemove()
+ {
+ base.OnRemove();
+ HealthChangedEvent -= OnHealthChanged;
+ }
+
+ private void OnHealthChanged(HealthChangedEventArgs e)
+ {
+ if (CurrentDamageState != DamageState.Dead && TotalDamage >= MaxHp)
+ {
+ PerformDestruction();
+ }
+ }
+
+ ///
+ /// Destroys the Owner , setting
+ /// to
+ ///
+ ///
+ protected void PerformDestruction()
+ {
+ _currentDamageState = DamageState.Dead;
+
+ if (!Owner.Deleted && DestroySound != string.Empty)
+ {
+ var pos = Owner.Transform.GridPosition;
+ EntitySystem.Get().PlayAtCoords(DestroySound, pos);
+ }
+
+ DestructionBehavior();
+ }
+
+ protected abstract void DestructionBehavior();
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Disposal/DisposalHolderComponent.cs b/Content.Server/GameObjects/Components/Disposal/DisposalHolderComponent.cs
index f74eecb284..ee39a57d76 100644
--- a/Content.Server/GameObjects/Components/Disposal/DisposalHolderComponent.cs
+++ b/Content.Server/GameObjects/Components/Disposal/DisposalHolderComponent.cs
@@ -1,7 +1,7 @@
#nullable enable
using System.Linq;
using Content.Server.GameObjects.Components.Items.Storage;
-using Content.Server.GameObjects.Components.Mobs;
+using Content.Shared.GameObjects.Components.Body;
using Robust.Server.GameObjects.Components.Container;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
@@ -49,7 +49,7 @@ namespace Content.Server.GameObjects.Components.Disposal
}
return entity.HasComponent() ||
- entity.HasComponent();
+ entity.HasComponent();
}
public bool TryInsert(IEntity entity)
diff --git a/Content.Server/GameObjects/Components/Disposal/DisposalTubeComponent.cs b/Content.Server/GameObjects/Components/Disposal/DisposalTubeComponent.cs
index 29d5132925..37b2f2275e 100644
--- a/Content.Server/GameObjects/Components/Disposal/DisposalTubeComponent.cs
+++ b/Content.Server/GameObjects/Components/Disposal/DisposalTubeComponent.cs
@@ -1,9 +1,9 @@
#nullable enable
using System;
using System.Linq;
-using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Disposal;
using Content.Shared.GameObjects.Verbs;
+using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Robust.Server.Console;
using Robust.Server.GameObjects;
diff --git a/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs b/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs
index 89a6049b66..3fd51adb20 100644
--- a/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs
+++ b/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs
@@ -5,10 +5,10 @@ using System.Linq;
using System.Threading;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
-using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.Interfaces;
using Content.Server.Interfaces.GameObjects.Components.Items;
+using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Disposal;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
@@ -122,7 +122,7 @@ namespace Content.Server.GameObjects.Components.Disposal
}
if (!entity.HasComponent() &&
- !entity.HasComponent())
+ !entity.HasComponent())
{
return false;
}
diff --git a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs
index 7f4228b272..ba2bc39377 100644
--- a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs
+++ b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs
@@ -3,9 +3,10 @@ using System.Linq;
using System.Threading;
using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.Atmos;
-using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Mobs;
+using Content.Shared.Damage;
+using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Doors;
using Content.Shared.GameObjects.Components.Movement;
@@ -113,7 +114,7 @@ namespace Content.Server.GameObjects.Components.Doors
// Disabled because it makes it suck hard to walk through double doors.
- if (entity.HasComponent(typeof(SpeciesComponent)))
+ if (entity.HasComponent())
{
if (!entity.TryGetComponent(out var mover)) return;
@@ -237,7 +238,7 @@ namespace Content.Server.GameObjects.Components.Doors
foreach (var e in collidesWith)
{
if (!e.TryGetComponent(out StunnableComponent stun)
- || !e.TryGetComponent(out DamageableComponent damage)
+ || !e.TryGetComponent(out IDamageableComponent damage)
|| !e.TryGetComponent(out ICollidableComponent otherBody)
|| !Owner.TryGetComponent(out ICollidableComponent body))
continue;
@@ -247,7 +248,7 @@ namespace Content.Server.GameObjects.Components.Doors
if (percentage < 0.1f)
continue;
- damage.TakeDamage(DamageType.Brute, DoorCrushDamage);
+ damage.ChangeDamage(DamageType.Blunt, DoorCrushDamage, false, Owner);
stun.Paralyze(DoorStunTime);
hitSomeone = true;
}
diff --git a/Content.Server/GameObjects/Components/Explosion/ExplosiveComponent.cs b/Content.Server/GameObjects/Components/Explosion/ExplosiveComponent.cs
index f23c611e24..39b49c13fb 100644
--- a/Content.Server/GameObjects/Components/Explosion/ExplosiveComponent.cs
+++ b/Content.Server/GameObjects/Components/Explosion/ExplosiveComponent.cs
@@ -1,5 +1,6 @@
using Content.Server.Explosions;
using Content.Server.GameObjects.EntitySystems;
+using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
diff --git a/Content.Server/GameObjects/Components/Explosion/FlashExplosiveComponent.cs b/Content.Server/GameObjects/Components/Explosion/FlashExplosiveComponent.cs
index a547e5f0b3..c8d6120edd 100644
--- a/Content.Server/GameObjects/Components/Explosion/FlashExplosiveComponent.cs
+++ b/Content.Server/GameObjects/Components/Explosion/FlashExplosiveComponent.cs
@@ -1,6 +1,7 @@
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Weapon;
using Content.Server.GameObjects.EntitySystems;
+using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
diff --git a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs
index 6336845970..572e504f53 100644
--- a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs
+++ b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs
@@ -8,11 +8,11 @@ using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems.Click;
using Content.Server.Interfaces.GameObjects.Components.Interaction;
+using Content.Shared.GameObjects.Components.Body;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems;
-using Content.Shared.Health.BodySystem;
using Content.Shared.Physics.Pull;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
diff --git a/Content.Server/GameObjects/Components/Gravity/GravityGeneratorComponent.cs b/Content.Server/GameObjects/Components/Gravity/GravityGeneratorComponent.cs
index 637933dd06..d47f94fd93 100644
--- a/Content.Server/GameObjects/Components/Gravity/GravityGeneratorComponent.cs
+++ b/Content.Server/GameObjects/Components/Gravity/GravityGeneratorComponent.cs
@@ -5,6 +5,7 @@ using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces;
using Content.Shared.GameObjects.Components.Gravity;
using Content.Shared.GameObjects.Components.Interactable;
+using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.UserInterface;
@@ -18,7 +19,7 @@ using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Gravity
{
[RegisterComponent]
- public class GravityGeneratorComponent: SharedGravityGeneratorComponent, IInteractUsing, IBreakAct, IInteractHand
+ public class GravityGeneratorComponent : SharedGravityGeneratorComponent, IInteractUsing, IBreakAct, IInteractHand
{
private BoundUserInterface _userInterface;
@@ -106,10 +107,8 @@ namespace Content.Server.GameObjects.Components.Gravity
return false;
// Repair generator
- var damageable = Owner.GetComponent();
var breakable = Owner.GetComponent();
- damageable.HealAllDamage();
- breakable.broken = false;
+ breakable.FixAllDamage();
_intact = true;
var notifyManager = IoCManager.Resolve();
@@ -130,13 +129,16 @@ namespace Content.Server.GameObjects.Components.Gravity
if (!Intact)
{
MakeBroken();
- } else if (!Powered)
+ }
+ else if (!Powered)
{
MakeUnpowered();
- } else if (!SwitchedOn)
+ }
+ else if (!SwitchedOn)
{
MakeOff();
- } else
+ }
+ else
{
MakeOn();
}
diff --git a/Content.Server/GameObjects/Components/Healing/HealingComponent.cs b/Content.Server/GameObjects/Components/Healing/HealingComponent.cs
deleted file mode 100644
index 65bbf4154a..0000000000
--- a/Content.Server/GameObjects/Components/Healing/HealingComponent.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using Content.Server.GameObjects.Components.Damage;
-using Content.Server.GameObjects.Components.Stack;
-using Content.Server.Utility;
-using Content.Shared.GameObjects.Components.Damage;
-using Content.Shared.Interfaces.GameObjects.Components;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Serialization;
-
-namespace Content.Server.GameObjects.Components.Healing
-{
- [RegisterComponent]
- public class HealingComponent : Component, IAfterInteract, IUse
- {
- public override string Name => "Healing";
-
- public int Heal = 100;
- public DamageType Damage = DamageType.Brute;
-
- public override void ExposeData(ObjectSerializer serializer)
- {
- base.ExposeData(serializer);
-
- serializer.DataField(ref Heal, "heal", 100);
- serializer.DataField(ref Damage, "damage", DamageType.Brute);
- }
-
- void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
- {
- if (!InteractionChecks.InRangeUnobstructed(eventArgs)) return;
-
- if (eventArgs.Target == null)
- {
- return;
- }
-
- if (!eventArgs.Target.TryGetComponent(out DamageableComponent damagecomponent)) return;
- if (Owner.TryGetComponent(out StackComponent stackComponent))
- {
- if (!stackComponent.Use(1))
- {
- Owner.Delete();
- return;
- }
-
- damagecomponent.TakeHealing(Damage, Heal);
- return;
- }
- damagecomponent.TakeHealing(Damage, Heal);
- Owner.Delete();
- }
-
- bool IUse.UseEntity(UseEntityEventArgs eventArgs)
- {
- if (!eventArgs.User.TryGetComponent(out DamageableComponent damagecomponent)) return false;
- if (Owner.TryGetComponent(out StackComponent stackComponent))
- {
- if (!stackComponent.Use(1))
- {
- Owner.Delete();
- return false;
- }
-
- damagecomponent.TakeHealing(Damage, Heal);
- return false;
- }
- damagecomponent.TakeHealing(Damage, Heal);
- Owner.Delete();
- return false;
- }
- }
-}
diff --git a/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs b/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs
index f85e7b64ea..3b96b57733 100644
--- a/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs
+++ b/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs
@@ -239,8 +239,9 @@ namespace Content.Server.GameObjects.Components.Interactable
chat.EntityMe(victim, Loc.GetString("welds {0:their} every orifice closed! It looks like {0:theyre} trying to commit suicide!", victim)); //TODO: theyre macro
return SuicideKind.Heat;
}
+
chat.EntityMe(victim, Loc.GetString("bashes {0:themselves} with the {1}!", victim, Owner.Name));
- return SuicideKind.Brute;
+ return SuicideKind.Blunt;
}
public void SolutionChanged(SolutionChangeEventArgs eventArgs)
diff --git a/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs b/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs
index 73d1fd09ca..df502ee9aa 100644
--- a/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs
+++ b/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs
@@ -1,10 +1,12 @@
using System;
using System.Linq;
+using Content.Server.GameObjects.Components.Body;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Interactable;
+using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Storage;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
@@ -168,7 +170,8 @@ namespace Content.Server.GameObjects.Components.Items.Storage
continue;
// only items that can be stored in an inventory, or a mob, can be eaten by a locker
- if (!entity.HasComponent() && !entity.HasComponent())
+ if (!entity.HasComponent() &&
+ !entity.HasComponent())
continue;
if (!AddToContents(entity))
diff --git a/Content.Server/GameObjects/Components/Items/Storage/ItemComponent.cs b/Content.Server/GameObjects/Components/Items/Storage/ItemComponent.cs
index 67eca347f2..a82183da4f 100644
--- a/Content.Server/GameObjects/Components/Items/Storage/ItemComponent.cs
+++ b/Content.Server/GameObjects/Components/Items/Storage/ItemComponent.cs
@@ -91,7 +91,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage
return false;
}
- if (Owner.TryGetComponent(out CollidableComponent physics) &&
+ if (Owner.TryGetComponent(out ICollidableComponent physics) &&
physics.Anchored)
{
return false;
diff --git a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs
index 51b79141f9..672f09c14a 100644
--- a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs
+++ b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs
@@ -1,18 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Content.Server.GameObjects.Components.Body;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.ViewVariables;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.EntitySystems;
-using Content.Server.Health.BodySystem;
using Content.Server.Interfaces;
using Content.Server.Interfaces.Chat;
using Content.Server.Interfaces.GameObjects;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Power;
-using Content.Shared.Health.BodySystem;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Kitchen;
@@ -23,14 +25,12 @@ using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.Audio;
-using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
+using Content.Shared.GameObjects.Components.Body;
using Robust.Shared.Interfaces.GameObjects;
-using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Timers;
-using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Kitchen
{
@@ -456,13 +456,19 @@ namespace Content.Server.GameObjects.Components.Kitchen
public SuicideKind Suicide(IEntity victim, IChatManager chat)
{
- int headCount = 0;
+ var headCount = 0;
if (victim.TryGetComponent(out var bodyManagerComponent))
{
var heads = bodyManagerComponent.GetBodyPartsOfType(BodyPartType.Head);
foreach (var head in heads)
{
- var droppedHead = bodyManagerComponent.DropBodyPart(head);
+ var droppedHead = bodyManagerComponent.DropPart(head);
+
+ if (droppedHead == null)
+ {
+ continue;
+ }
+
_storage.Insert(droppedHead);
headCount++;
}
diff --git a/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs b/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs
index c6a051b2c4..4e36ffed71 100644
--- a/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs
+++ b/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
-using Content.Server.GameObjects.Components.Damage;
-using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Medical;
@@ -15,7 +13,8 @@ using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Maths;
-using Robust.Shared.Utility;
+using Content.Shared.Damage;
+using Robust.Shared.Localization;
namespace Content.Server.GameObjects.Components.Medical
{
@@ -35,19 +34,21 @@ namespace Content.Server.GameObjects.Components.Medical
public override void Initialize()
{
base.Initialize();
+
_appearance = Owner.GetComponent();
_userInterface = Owner.GetComponent()
.GetBoundUserInterface(MedicalScannerUiKey.Key);
_bodyContainer = ContainerManagerComponent.Ensure($"{Name}-bodyContainer", Owner);
_powerReceiver = Owner.GetComponent();
+
UpdateUserInterface();
}
private static readonly MedicalScannerBoundUserInterfaceState EmptyUIState =
new MedicalScannerBoundUserInterfaceState(
- 0,
- 0,
- null);
+ null,
+ new Dictionary(),
+ new Dictionary());
private MedicalScannerBoundUserInterfaceState GetUserInterfaceState()
{
@@ -58,49 +59,36 @@ namespace Content.Server.GameObjects.Components.Medical
return EmptyUIState;
}
- var damageable = body.GetComponent();
- var species = body.GetComponent();
- var deathThreshold =
- species.DamageTemplate.DamageThresholds.FirstOrNull(x => x.ThresholdType == ThresholdType.Death);
- if (!deathThreshold.HasValue)
+ if (!body.TryGetComponent(out IDamageableComponent damageable) ||
+ damageable.CurrentDamageState == DamageState.Dead)
{
return EmptyUIState;
}
- var deathThresholdValue = deathThreshold.Value.Value;
- var currentHealth = damageable.CurrentDamage[DamageType.Total];
+ var classes = new Dictionary(damageable.DamageClasses);
+ var types = new Dictionary(damageable.DamageTypes);
- var dmgDict = new Dictionary();
-
- foreach (var dmgType in (DamageType[]) Enum.GetValues(typeof(DamageType)))
- {
- if (damageable.CurrentDamage.TryGetValue(dmgType, out var amount))
- {
- dmgDict[dmgType.ToString()] = amount;
- }
- }
-
- return new MedicalScannerBoundUserInterfaceState(
- deathThresholdValue - currentHealth,
- deathThresholdValue,
- dmgDict);
+ return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types);
}
private void UpdateUserInterface()
{
if (!Powered)
+ {
return;
+ }
+
var newState = GetUserInterfaceState();
_userInterface.SetState(newState);
}
- private MedicalScannerStatus GetStatusFromDamageState(IDamageState damageState)
+ private MedicalScannerStatus GetStatusFromDamageState(DamageState damageState)
{
switch (damageState)
{
- case NormalState _: return MedicalScannerStatus.Green;
- case CriticalState _: return MedicalScannerStatus.Red;
- case DeadState _: return MedicalScannerStatus.Death;
+ case DamageState.Alive: return MedicalScannerStatus.Green;
+ case DamageState.Critical: return MedicalScannerStatus.Red;
+ case DamageState.Dead: return MedicalScannerStatus.Death;
default: throw new ArgumentException(nameof(damageState));
}
}
@@ -109,7 +97,7 @@ namespace Content.Server.GameObjects.Components.Medical
var body = _bodyContainer.ContainedEntity;
return body == null
? MedicalScannerStatus.Open
- : GetStatusFromDamageState(body.GetComponent().CurrentDamageState);
+ : GetStatusFromDamageState(body.GetComponent().CurrentDamageState);
}
private void UpdateAppearance()
@@ -141,7 +129,7 @@ namespace Content.Server.GameObjects.Components.Medical
return;
}
- data.Text = "Enter";
+ data.Text = Loc.GetString("Enter");
data.Visibility = component.IsOccupied ? VerbVisibility.Invisible : VerbVisibility.Visible;
}
@@ -162,7 +150,7 @@ namespace Content.Server.GameObjects.Components.Medical
return;
}
- data.Text = "Eject";
+ data.Text = Loc.GetString("Eject");
data.Visibility = component.IsOccupied ? VerbVisibility.Visible : VerbVisibility.Invisible;
}
@@ -195,6 +183,7 @@ namespace Content.Server.GameObjects.Components.Medical
// There's no need to update if there's no one inside
return;
}
+
UpdateUserInterface();
UpdateAppearance();
}
diff --git a/Content.Server/GameObjects/Components/Metabolism/BloodstreamComponent.cs b/Content.Server/GameObjects/Components/Metabolism/BloodstreamComponent.cs
deleted file mode 100644
index e428c72f17..0000000000
--- a/Content.Server/GameObjects/Components/Metabolism/BloodstreamComponent.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-using System.Linq;
-using Content.Server.GameObjects.Components.Chemistry;
-using Content.Server.GameObjects.EntitySystems;
-using Content.Shared.Chemistry;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Robust.Shared.ViewVariables;
-
-namespace Content.Server.GameObjects.Components.Metabolism
-{
- ///
- /// Handles all metabolism for mobs. All delivery methods eventually bring reagents
- /// to the bloodstream. For example, injectors put reagents directly into the bloodstream,
- /// and the stomach does with some delay.
- ///
- [RegisterComponent]
- public class BloodstreamComponent : Component
- {
-#pragma warning disable 649
- [Dependency] private readonly IPrototypeManager _prototypeManager;
-#pragma warning restore 649
-
- public override string Name => "Bloodstream";
-
- ///
- /// Internal solution for reagent storage
- ///
- [ViewVariables]
- private SolutionComponent _internalSolution;
-
- ///
- /// Max volume of internal solution storage
- ///
- [ViewVariables]
- private ReagentUnit _initialMaxVolume;
-
- ///
- /// Empty volume of internal solution
- ///
- public ReagentUnit EmptyVolume => _internalSolution.EmptyVolume;
-
- public override void ExposeData(ObjectSerializer serializer)
- {
- base.ExposeData(serializer);
- serializer.DataField(ref _initialMaxVolume, "maxVolume", ReagentUnit.New(250));
- }
-
- protected override void Startup()
- {
- base.Startup();
- _internalSolution = Owner.GetComponent();
- _internalSolution.MaxVolume = _initialMaxVolume;
- }
-
- ///
- /// Attempt to transfer provided solution to internal solution. Only supports complete transfers
- ///
- /// Solution to be transferred
- /// Whether or not transfer was a success
- public bool TryTransferSolution(Solution solution)
- {
- //For now doesn't support partial transfers
- if (solution.TotalVolume + _internalSolution.CurrentVolume > _internalSolution.MaxVolume)
- {
- return false;
- }
-
- _internalSolution.TryAddSolution(solution, false, true);
- return true;
- }
-
- ///
- /// Loops through each reagent in _internalSolution, and calls the IMetabolizable for each of them./>
- ///
- /// The time since the last metabolism tick in seconds.
- private void Metabolize(float tickTime)
- {
- if (_internalSolution.CurrentVolume == 0)
- {
- return;
- }
-
- //Run metabolism for each reagent, remove metabolized reagents
- foreach (var reagent in _internalSolution.ReagentList.ToList()) //Using ToList here lets us edit reagents while iterating
- {
- if (!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto))
- {
- continue;
- }
-
- //Run metabolism code for each reagent
- foreach (var metabolizable in proto.Metabolism)
- {
- var reagentDelta = metabolizable.Metabolize(Owner, reagent.ReagentId, tickTime);
- _internalSolution.TryRemoveReagent(reagent.ReagentId, reagentDelta);
- }
- }
- }
-
- ///
- /// Triggers metabolism of the reagents inside _internalSolution. Called by
- ///
- /// The time since the last metabolism tick in seconds.
- public void OnUpdate(float tickTime)
- {
- Metabolize(tickTime);
- }
- }
-}
diff --git a/Content.Server/GameObjects/Components/Metabolism/MetabolismComponent.cs b/Content.Server/GameObjects/Components/Metabolism/MetabolismComponent.cs
new file mode 100644
index 0000000000..913d571dd6
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Metabolism/MetabolismComponent.cs
@@ -0,0 +1,249 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.Atmos;
+using Content.Server.GameObjects.Components.Atmos;
+using Content.Server.GameObjects.Components.Body.Circulatory;
+using Content.Shared.Atmos;
+using Content.Shared.Chemistry;
+using Content.Shared.Damage;
+using Content.Shared.GameObjects.Components.Damage;
+using Content.Shared.Interfaces.Chemistry;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.GameObjects.Components.Metabolism
+{
+ [RegisterComponent]
+ public class MetabolismComponent : Component
+ {
+#pragma warning disable 649
+ [Dependency] private readonly IPrototypeManager _prototypeManager;
+#pragma warning restore 649
+
+ public override string Name => "Metabolism";
+
+ private float _accumulatedFrameTime;
+
+ [ViewVariables(VVAccess.ReadWrite)] private int _suffocationDamage;
+
+ [ViewVariables] public Dictionary NeedsGases { get; set; }
+
+ [ViewVariables] public Dictionary ProducesGases { get; set; }
+
+ [ViewVariables] public Dictionary DeficitGases { get; set; }
+
+ [ViewVariables] public bool Suffocating => SuffocatingPercentage() > 0;
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(this, b => b.NeedsGases, "needsGases", new Dictionary());
+ serializer.DataField(this, b => b.ProducesGases, "producesGases", new Dictionary());
+ serializer.DataField(this, b => b.DeficitGases, "deficitGases", new Dictionary());
+ serializer.DataField(ref _suffocationDamage, "suffocationDamage", 1);
+ }
+
+ private Dictionary NeedsAndDeficit(float frameTime)
+ {
+ var needs = new Dictionary(NeedsGases);
+ foreach (var (gas, amount) in DeficitGases)
+ {
+ var newAmount = (needs.GetValueOrDefault(gas) + amount) * frameTime;
+ needs[gas] = newAmount;
+ }
+
+ return needs;
+ }
+
+ private void ClampDeficit()
+ {
+ var deficitGases = new Dictionary(DeficitGases);
+
+ foreach (var (gas, deficit) in deficitGases)
+ {
+ if (!NeedsGases.TryGetValue(gas, out var need))
+ {
+ DeficitGases.Remove(gas);
+ continue;
+ }
+
+ if (deficit > need)
+ {
+ DeficitGases[gas] = need;
+ }
+ }
+ }
+
+ private float SuffocatingPercentage()
+ {
+ var percentages = new float[Atmospherics.TotalNumberOfGases];
+
+ foreach (var (gas, deficit) in DeficitGases)
+ {
+ if (!NeedsGases.TryGetValue(gas, out var needed))
+ {
+ percentages[(int) gas] = 1;
+ continue;
+ }
+
+ percentages[(int) gas] = deficit / needed;
+ }
+
+ return percentages.Average();
+ }
+
+ private float GasProducedMultiplier(Gas gas, float usedAverage)
+ {
+ if (!NeedsGases.TryGetValue(gas, out var needs) ||
+ !ProducesGases.TryGetValue(gas, out var produces))
+ {
+ return 0;
+ }
+
+ return needs * produces * usedAverage;
+ }
+
+ private Dictionary GasProduced(float usedAverage)
+ {
+ return ProducesGases.ToDictionary(pair => pair.Key, pair => GasProducedMultiplier(pair.Key, usedAverage));
+ }
+
+ private void ProcessGases(float frameTime)
+ {
+ if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream))
+ {
+ return;
+ }
+
+ var usedPercentages = new float[Atmospherics.TotalNumberOfGases];
+ var needs = NeedsAndDeficit(frameTime);
+ foreach (var (gas, amountNeeded) in needs)
+ {
+ var bloodstreamAmount = bloodstream.Air.GetMoles(gas);
+ var deficit = 0f;
+
+ if (bloodstreamAmount >= amountNeeded)
+ {
+ bloodstream.Air.AdjustMoles(gas, -amountNeeded);
+ }
+ else
+ {
+ deficit = amountNeeded - bloodstreamAmount;
+ bloodstream.Air.SetMoles(gas, 0);
+ }
+
+ DeficitGases[gas] = deficit;
+
+ var used = amountNeeded - deficit;
+ usedPercentages[(int) gas] = used / amountNeeded;
+ }
+
+ var usedAverage = usedPercentages.Average();
+ var produced = GasProduced(usedAverage);
+
+ foreach (var (gas, amountProduced) in produced)
+ {
+ bloodstream.Air.AdjustMoles(gas, amountProduced);
+ }
+
+ ClampDeficit();
+ }
+
+ ///
+ /// Loops through each reagent in _internalSolution,
+ /// and calls for each of them.
+ ///
+ /// The time since the last metabolism tick in seconds.
+ private void ProcessNutrients(float frameTime)
+ {
+ if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream))
+ {
+ return;
+ }
+
+ if (bloodstream.Solution.CurrentVolume == 0)
+ {
+ return;
+ }
+
+ // Run metabolism for each reagent, remove metabolized reagents
+ // Using ToList here lets us edit reagents while iterating
+ foreach (var reagent in bloodstream.Solution.ReagentList.ToList())
+ {
+ if (!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype prototype))
+ {
+ continue;
+ }
+
+ // Run metabolism code for each reagent
+ foreach (var metabolizable in prototype.Metabolism)
+ {
+ var reagentDelta = metabolizable.Metabolize(Owner, reagent.ReagentId, frameTime);
+ bloodstream.Solution.TryRemoveReagent(reagent.ReagentId, reagentDelta);
+ }
+ }
+ }
+
+ ///
+ /// Processes gases in the bloodstream and triggers metabolism of the
+ /// reagents inside of it.
+ ///
+ ///
+ /// The time since the last metabolism tick in seconds.
+ ///
+ public void Update(float frameTime)
+ {
+ _accumulatedFrameTime += frameTime;
+
+ if (_accumulatedFrameTime < 1)
+ {
+ return;
+ }
+
+ _accumulatedFrameTime -= 1;
+
+ ProcessGases(frameTime);
+ ProcessNutrients(frameTime);
+
+ if (Suffocating &&
+ Owner.TryGetComponent(out IDamageableComponent damageable))
+ {
+ damageable.ChangeDamage(DamageClass.Airloss, _suffocationDamage, false);
+ }
+ }
+
+ public void Transfer(BloodstreamComponent @from, GasMixture to, Gas gas, float pressure)
+ {
+ var transfer = new GasMixture();
+ var molesInBlood = @from.Air.GetMoles(gas);
+
+ transfer.SetMoles(gas, molesInBlood);
+ transfer.ReleaseGasTo(to, pressure);
+
+ @from.Air.Merge(transfer);
+ }
+
+ public GasMixture Clean(BloodstreamComponent bloodstream, float pressure = 100)
+ {
+ var gasMixture = new GasMixture(bloodstream.Air.Volume);
+
+ for (Gas gas = 0; gas < (Gas) Atmospherics.TotalNumberOfGases; gas++)
+ {
+ if (NeedsGases.TryGetValue(gas, out var needed) &&
+ bloodstream.Air.GetMoles(gas) < needed * 1.5f)
+ {
+ continue;
+ }
+
+ Transfer(bloodstream, gasMixture, gas, pressure);
+ }
+
+ return gasMixture;
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Mining/AsteroidRockComponent.cs b/Content.Server/GameObjects/Components/Mining/AsteroidRockComponent.cs
index a7fee5d76b..ec174cca63 100644
--- a/Content.Server/GameObjects/Components/Mining/AsteroidRockComponent.cs
+++ b/Content.Server/GameObjects/Components/Mining/AsteroidRockComponent.cs
@@ -1,6 +1,6 @@
-using Content.Server.GameObjects.Components.Damage;
-using Content.Server.GameObjects.Components.Weapon.Melee;
+using Content.Server.GameObjects.Components.Weapon.Melee;
using Content.Shared.GameObjects.Components.Damage;
+using Content.Shared.Damage;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
@@ -35,7 +35,7 @@ namespace Content.Server.GameObjects.Components.Mining
var item = eventArgs.Using;
if (!item.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent)) return false;
- Owner.GetComponent().TakeDamage(DamageType.Brute, meleeWeaponComponent.Damage, item, eventArgs.User);
+ Owner.GetComponent().ChangeDamage(DamageType.Blunt, meleeWeaponComponent.Damage, false, item);
if (!item.TryGetComponent(out PickaxeComponent pickaxeComponent)) return true;
if (!string.IsNullOrWhiteSpace(pickaxeComponent.MiningSound))
diff --git a/Content.Server/GameObjects/Components/Mobs/DamageStates.cs b/Content.Server/GameObjects/Components/Mobs/DamageStates.cs
deleted file mode 100644
index 89aea5beb8..0000000000
--- a/Content.Server/GameObjects/Components/Mobs/DamageStates.cs
+++ /dev/null
@@ -1,275 +0,0 @@
-using Content.Server.Mobs;
-using Content.Shared.GameObjects.Components.Mobs;
-using Content.Shared.GameObjects.EntitySystems;
-using Robust.Server.GameObjects;
-using Robust.Shared.GameObjects.Components;
-using Robust.Shared.Interfaces.GameObjects;
-
-namespace Content.Server.GameObjects.Components.Mobs
-{
- ///
- /// Defines the blocking effect of each damage state, and what effects to apply upon entering or exiting the state
- ///
- public interface IDamageState : IActionBlocker
- {
- void EnterState(IEntity entity);
-
- void ExitState(IEntity entity);
-
- bool IsConscious { get; }
- }
-
- ///
- /// Standard state that a species is at with no damage or negative effect
- ///
- public struct NormalState : IDamageState
- {
- public void EnterState(IEntity entity)
- {
- entity.TryGetComponent(out AppearanceComponent appearanceComponent);
- appearanceComponent?.SetData(DamageStateVisuals.State, DamageStateVisualData.Normal);
- }
-
- public void ExitState(IEntity entity)
- {
- }
-
- public bool IsConscious => true;
-
- bool IActionBlocker.CanInteract()
- {
- return true;
- }
-
- bool IActionBlocker.CanMove()
- {
- return true;
- }
-
- bool IActionBlocker.CanUse()
- {
- return true;
- }
-
- bool IActionBlocker.CanThrow()
- {
- return true;
- }
-
- bool IActionBlocker.CanSpeak()
- {
- return true;
- }
-
- bool IActionBlocker.CanDrop()
- {
- return true;
- }
-
- bool IActionBlocker.CanPickup()
- {
- return true;
- }
-
- bool IActionBlocker.CanEmote()
- {
- return true;
- }
-
- bool IActionBlocker.CanAttack()
- {
- return true;
- }
-
- bool IActionBlocker.CanEquip()
- {
- return true;
- }
-
- bool IActionBlocker.CanUnequip()
- {
- return true;
- }
-
- bool IActionBlocker.CanChangeDirection()
- {
- return true;
- }
- }
-
- ///
- /// A state in which you are disabled from acting due to damage
- ///
- public struct CriticalState : IDamageState
- {
- public void EnterState(IEntity entity)
- {
- if(entity.TryGetComponent(out StunnableComponent stun))
- stun.CancelAll();
-
- entity.TryGetComponent(out AppearanceComponent appearanceComponent);
- appearanceComponent?.SetData(DamageStateVisuals.State, DamageStateVisualData.Crit);
- StandingStateHelper.Down(entity);
- }
-
- public void ExitState(IEntity entity)
- {
- StandingStateHelper.Standing(entity);
- }
-
- public bool IsConscious => false;
-
- bool IActionBlocker.CanInteract()
- {
- return false;
- }
-
- bool IActionBlocker.CanMove()
- {
- return false;
- }
-
- bool IActionBlocker.CanUse()
- {
- return false;
- }
-
- bool IActionBlocker.CanThrow()
- {
- return false;
- }
-
- bool IActionBlocker.CanSpeak()
- {
- return false;
- }
-
- bool IActionBlocker.CanDrop()
- {
- return false;
- }
-
- bool IActionBlocker.CanPickup()
- {
- return false;
- }
-
- bool IActionBlocker.CanEmote()
- {
- return false;
- }
-
- bool IActionBlocker.CanAttack()
- {
- return false;
- }
-
- bool IActionBlocker.CanEquip()
- {
- return false;
- }
-
- bool IActionBlocker.CanUnequip()
- {
- return false;
- }
-
- bool IActionBlocker.CanChangeDirection()
- {
- return true;
- }
- }
-
- ///
- /// A damage state which will allow ghosting out of mobs
- ///
- public struct DeadState : IDamageState
- {
- public void EnterState(IEntity entity)
- {
- if(entity.TryGetComponent(out StunnableComponent stun))
- stun.CancelAll();
-
- StandingStateHelper.Down(entity);
- entity.TryGetComponent(out AppearanceComponent appearanceComponent);
- appearanceComponent?.SetData(DamageStateVisuals.State, DamageStateVisualData.Dead);
-
- if (entity.TryGetComponent(out ICollidableComponent collidable))
- {
- collidable.CanCollide = false;
- }
- }
-
- public void ExitState(IEntity entity)
- {
- StandingStateHelper.Standing(entity);
-
- if (entity.TryGetComponent(out ICollidableComponent collidable))
- {
- collidable.CanCollide = true;
- }
- }
-
- public bool IsConscious => false;
-
- bool IActionBlocker.CanInteract()
- {
- return false;
- }
-
- bool IActionBlocker.CanMove()
- {
- return false;
- }
-
- bool IActionBlocker.CanUse()
- {
- return false;
- }
-
- bool IActionBlocker.CanThrow()
- {
- return false;
- }
-
- bool IActionBlocker.CanSpeak()
- {
- return false;
- }
-
- bool IActionBlocker.CanDrop()
- {
- return false;
- }
-
- bool IActionBlocker.CanPickup()
- {
- return false;
- }
-
- bool IActionBlocker.CanEmote()
- {
- return false;
- }
-
- bool IActionBlocker.CanAttack()
- {
- return false;
- }
-
- bool IActionBlocker.CanEquip()
- {
- return false;
- }
-
- bool IActionBlocker.CanUnequip()
- {
- return false;
- }
-
- bool IActionBlocker.CanChangeDirection()
- {
- return false;
- }
- }
-}
diff --git a/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/DamageThresholdTemplates.cs b/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/DamageThresholdTemplates.cs
deleted file mode 100644
index 80295df021..0000000000
--- a/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/DamageThresholdTemplates.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using System.Collections.Generic;
-using Content.Server.GameObjects.Components.Damage;
-using Content.Shared.GameObjects.Components.Damage;
-
-namespace Content.Server.GameObjects.Components.Mobs.DamageThresholdTemplates
-{
- ///
- /// Defines the threshold values for each damage state for any kind of species
- ///
- public abstract class DamageTemplates
- {
- public abstract List HealthHudThresholds { get; }
-
- ///
- /// Changes the hud state when a threshold is reached
- ///
- ///
- ///
- public abstract void ChangeHudState(DamageableComponent damage);
-
- //public abstract ResistanceSet resistanceset { get; }
-
- ///
- /// Shows allowed states, ordered by priority, closest to last value to have threshold reached is preferred
- ///
- public abstract List<(DamageType, int, ThresholdType)> AllowedStates { get; }
-
- ///
- /// Map of ALL POSSIBLE damage states to the threshold enum value that will trigger them, normal state wont be triggered by this value but is a default that is fell back onto
- ///
- public static Dictionary StateThresholdMap = new Dictionary()
- {
- { ThresholdType.None, new NormalState() },
- { ThresholdType.Critical, new CriticalState() },
- { ThresholdType.Death, new DeadState() }
- };
-
- public List DamageThresholds
- {
- get
- {
- List thresholds = new List();
- foreach (var element in AllowedStates)
- {
- thresholds.Add(new DamageThreshold(element.Item1, element.Item2, element.Item3));
- }
- return thresholds;
- }
- }
-
- public ThresholdType CalculateDamageState(DamageableComponent damage)
- {
- ThresholdType healthstate = ThresholdType.None;
- foreach(var element in AllowedStates)
- {
- if(damage.CurrentDamage[element.Item1] >= element.Item2)
- {
- healthstate = element.Item3;
- }
- }
-
- return healthstate;
- }
- }
-}
diff --git a/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs b/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs
deleted file mode 100644
index 8045758b89..0000000000
--- a/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Content.Server.GameObjects.Components.Damage;
-using Content.Shared.GameObjects.Components.Damage;
-using Content.Shared.GameObjects.Components.Mobs;
-using JetBrains.Annotations;
-
-namespace Content.Server.GameObjects.Components.Mobs.DamageThresholdTemplates
-{
- [UsedImplicitly]
- public class Human : DamageTemplates
- {
- int critvalue = 200;
- int normalstates = 6;
- //string startsprite = "human0";
-
- public override List<(DamageType, int, ThresholdType)> AllowedStates => new List<(DamageType, int, ThresholdType)>()
- {
- (DamageType.Total, critvalue-1, ThresholdType.None),
- (DamageType.Total, critvalue, ThresholdType.Critical),
- (DamageType.Total, 300, ThresholdType.Death),
- };
-
- public override List HealthHudThresholds
- {
- get
- {
- List thresholds = new List();
- thresholds.Add(new DamageThreshold(DamageType.Total, 1, ThresholdType.HUDUpdate));
- for (var i = 1; i <= normalstates; i++)
- {
- thresholds.Add(new DamageThreshold(DamageType.Total, i * critvalue / normalstates, ThresholdType.HUDUpdate));
- }
- return thresholds; //we don't need to respecify the state damage thresholds since we'll update hud on damage state changes as well
- }
- }
-
- // for shared string dict, since we don't define these anywhere in content
- [UsedImplicitly]
- public static readonly string[] _humanStatusImages =
- {
- "/Textures/Interface/StatusEffects/Human/human0.png",
- "/Textures/Interface/StatusEffects/Human/human1.png",
- "/Textures/Interface/StatusEffects/Human/human2.png",
- "/Textures/Interface/StatusEffects/Human/human3.png",
- "/Textures/Interface/StatusEffects/Human/human4.png",
- "/Textures/Interface/StatusEffects/Human/human5.png",
- "/Textures/Interface/StatusEffects/Human/human6-0.png",
- "/Textures/Interface/StatusEffects/Human/human6-1.png",
- "/Textures/Interface/StatusEffects/Human/humancrit-0.png",
- "/Textures/Interface/StatusEffects/Human/humancrit-1.png",
- "/Textures/Interface/StatusEffects/Human/humandead.png",
- };
-
- public override void ChangeHudState(DamageableComponent damage)
- {
- ThresholdType healthstate = CalculateDamageState(damage);
- damage.Owner.TryGetComponent(out ServerStatusEffectsComponent statusEffectsComponent);
- damage.Owner.TryGetComponent(out ServerOverlayEffectsComponent overlayComponent);
- switch (healthstate)
- {
- case ThresholdType.None:
- var totaldamage = damage.CurrentDamage[DamageType.Total];
- if (totaldamage > critvalue)
- {
- throw new InvalidOperationException(); //these should all be below the crit value, possibly going over multiple thresholds at once?
- }
- var modifier = totaldamage / (critvalue / normalstates); //integer division floors towards zero
- statusEffectsComponent?.ChangeStatusEffectIcon(StatusEffect.Health,
- "/Textures/Interface/StatusEffects/Human/human" + modifier + ".png");
-
- overlayComponent?.RemoveOverlay(SharedOverlayID.GradientCircleMaskOverlay);
- overlayComponent?.RemoveOverlay(SharedOverlayID.CircleMaskOverlay);
-
- return;
- case ThresholdType.Critical:
- statusEffectsComponent?.ChangeStatusEffectIcon(
- StatusEffect.Health,
- "/Textures/Interface/StatusEffects/Human/humancrit-0.png");
- overlayComponent?.AddOverlay(SharedOverlayID.GradientCircleMaskOverlay);
- overlayComponent?.RemoveOverlay(SharedOverlayID.CircleMaskOverlay);
-
- return;
- case ThresholdType.Death:
- statusEffectsComponent?.ChangeStatusEffectIcon(
- StatusEffect.Health,
- "/Textures/Interface/StatusEffects/Human/humandead.png");
- overlayComponent?.RemoveOverlay(SharedOverlayID.GradientCircleMaskOverlay);
- overlayComponent?.AddOverlay(SharedOverlayID.CircleMaskOverlay);
-
- return;
- default:
- throw new InvalidOperationException();
- }
- }
- }
-}
diff --git a/Content.Server/GameObjects/Components/Mobs/HeatResistanceComponent.cs b/Content.Server/GameObjects/Components/Mobs/HeatResistanceComponent.cs
index 7d21ba3124..b0423e455b 100644
--- a/Content.Server/GameObjects/Components/Mobs/HeatResistanceComponent.cs
+++ b/Content.Server/GameObjects/Components/Mobs/HeatResistanceComponent.cs
@@ -13,10 +13,9 @@ namespace Content.Server.GameObjects.Components.Mobs
public int GetHeatResistance()
{
- if (Owner.GetComponent().TryGetSlotItem(EquipmentSlotDefines.Slots.GLOVES, itemComponent: out ClothingComponent gloves)
- | Owner.TryGetComponent(out SpeciesComponent speciesComponent))
+ if (Owner.GetComponent().TryGetSlotItem(EquipmentSlotDefines.Slots.GLOVES, itemComponent: out ClothingComponent gloves))
{
- return Math.Max(gloves?.HeatResistance ?? int.MinValue, speciesComponent?.HeatResistance ?? int.MinValue);
+ return gloves?.HeatResistance ?? int.MinValue;
}
return int.MinValue;
}
diff --git a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs
index fe033e2231..fd2fed7f5f 100644
--- a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs
+++ b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs
@@ -2,6 +2,7 @@
using Content.Server.GameObjects.Components.Observer;
using Content.Server.Interfaces.GameTicking;
using Content.Server.Mobs;
+using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
@@ -126,8 +127,8 @@ namespace Content.Server.GameObjects.Components.Mobs
}
var dead =
- Owner.TryGetComponent(out var species) &&
- species.CurrentDamageState is DeadState;
+ Owner.TryGetComponent(out var damageable) &&
+ damageable.CurrentDamageState == DamageState.Dead;
if (!HasMind)
{
diff --git a/Content.Server/GameObjects/Components/Mobs/MobStateManager.cs b/Content.Server/GameObjects/Components/Mobs/MobStateManager.cs
new file mode 100644
index 0000000000..acb501a1ea
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Mobs/MobStateManager.cs
@@ -0,0 +1,488 @@
+using System.Collections.Generic;
+using Content.Server.GameObjects.Components.Body;
+using Content.Server.GameObjects.Components.Damage;
+using Content.Server.Mobs;
+using Content.Shared.GameObjects.Components.Damage;
+using Content.Shared.GameObjects.Components.Mobs;
+using Content.Shared.GameObjects.EntitySystems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Components;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.GameObjects.Components.Mobs
+{
+ ///
+ /// When attacked to an , this component will handle critical and death behaviors
+ /// for mobs.
+ /// Additionally, it handles sending effects to clients (such as blur effect for unconsciousness) and managing the
+ /// health HUD.
+ ///
+ [RegisterComponent]
+ internal class MobStateManagerComponent : Component, IOnHealthChangedBehavior, IActionBlocker
+ {
+ private readonly Dictionary _behavior = new Dictionary
+ {
+ {DamageState.Alive, new NormalState()},
+ {DamageState.Critical, new CriticalState()},
+ {DamageState.Dead, new DeadState()}
+ };
+
+ public override string Name => "MobStateManager";
+
+ private DamageState _currentDamageState;
+
+ public IMobState CurrentMobState { get; private set; } = new NormalState();
+
+ bool IActionBlocker.CanInteract()
+ {
+ return CurrentMobState.CanInteract();
+ }
+
+ bool IActionBlocker.CanMove()
+ {
+ return CurrentMobState.CanMove();
+ }
+
+ bool IActionBlocker.CanUse()
+ {
+ return CurrentMobState.CanUse();
+ }
+
+ bool IActionBlocker.CanThrow()
+ {
+ return CurrentMobState.CanThrow();
+ }
+
+ bool IActionBlocker.CanSpeak()
+ {
+ return CurrentMobState.CanSpeak();
+ }
+
+ bool IActionBlocker.CanDrop()
+ {
+ return CurrentMobState.CanDrop();
+ }
+
+ bool IActionBlocker.CanPickup()
+ {
+ return CurrentMobState.CanPickup();
+ }
+
+ bool IActionBlocker.CanEmote()
+ {
+ return CurrentMobState.CanEmote();
+ }
+
+ bool IActionBlocker.CanAttack()
+ {
+ return CurrentMobState.CanAttack();
+ }
+
+ bool IActionBlocker.CanEquip()
+ {
+ return CurrentMobState.CanEquip();
+ }
+
+ bool IActionBlocker.CanUnequip()
+ {
+ return CurrentMobState.CanUnequip();
+ }
+
+ bool IActionBlocker.CanChangeDirection()
+ {
+ return CurrentMobState.CanChangeDirection();
+ }
+
+ public void OnHealthChanged(HealthChangedEventArgs e)
+ {
+ if (e.Damageable.CurrentDamageState != _currentDamageState)
+ {
+ _currentDamageState = e.Damageable.CurrentDamageState;
+ CurrentMobState.ExitState(Owner);
+ CurrentMobState = _behavior[_currentDamageState];
+ CurrentMobState.EnterState(Owner);
+ }
+
+ CurrentMobState.UpdateState(Owner);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _currentDamageState = DamageState.Alive;
+ CurrentMobState = _behavior[_currentDamageState];
+ CurrentMobState.EnterState(Owner);
+ CurrentMobState.UpdateState(Owner);
+ }
+
+ public override void OnRemove()
+ {
+ // TODO: Might want to add an OnRemove() to IMobState since those are where these components are being used
+ base.OnRemove();
+
+ if (Owner.TryGetComponent(out ServerStatusEffectsComponent status))
+ {
+ status.RemoveStatusEffect(StatusEffect.Health);
+ }
+
+ if (Owner.TryGetComponent(out ServerOverlayEffectsComponent overlay))
+ {
+ overlay.ClearOverlays();
+ }
+ }
+ }
+
+ ///