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 1cf96367a6..8553c6ed58 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:
@@ -225,13 +247,17 @@ namespace Content.Client.Chat
case OOCAlias:
{
var conInput = text.Substring(1);
+ if (string.IsNullOrWhiteSpace(conInput))
+ return;
_console.ProcessCommand($"ooc \"{CommandParsing.Escape(conInput)}\"");
break;
}
case AdminChatAlias:
{
var conInput = text.Substring(1);
- if(_groupController.CanCommand("asay")){
+ if (string.IsNullOrWhiteSpace(conInput))
+ return;
+ if (_groupController.CanCommand("asay")){
_console.ProcessCommand($"asay \"{CommandParsing.Escape(conInput)}\"");
}
else
@@ -243,6 +269,8 @@ namespace Content.Client.Chat
case MeAlias:
{
var conInput = text.Substring(1);
+ if (string.IsNullOrWhiteSpace(conInput))
+ return;
_console.ProcessCommand($"me \"{CommandParsing.Escape(conInput)}\"");
break;
}
@@ -323,8 +351,6 @@ namespace Content.Client.Chat
private void _onChatMessage(MsgChatMessage msg)
{
- Logger.Debug($"{msg.Channel}: {msg.Message}");
-
// Log all incoming chat to repopulate when filter is un-toggled
var storedMessage = new StoredChatMessage(msg);
filteredHistory.Add(storedMessage);
@@ -347,6 +373,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/ClientContentIoC.cs b/Content.Client/ClientContentIoC.cs
index 9fe86a4b73..8639ea4c24 100644
--- a/Content.Client/ClientContentIoC.cs
+++ b/Content.Client/ClientContentIoC.cs
@@ -5,6 +5,7 @@ using Content.Client.Interfaces.Chat;
using Content.Client.Interfaces.Parallax;
using Content.Client.Parallax;
using Content.Client.Sandbox;
+using Content.Client.StationEvents;
using Content.Client.UserInterface;
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
@@ -31,6 +32,7 @@ namespace Content.Client
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
+ IoCManager.Register();
}
}
}
diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs
index 9a3065d77c..ef26c72a49 100644
--- a/Content.Client/EntryPoint.cs
+++ b/Content.Client/EntryPoint.cs
@@ -7,6 +7,7 @@ using Content.Client.Interfaces.Parallax;
using Content.Client.Parallax;
using Content.Client.Sandbox;
using Content.Client.State;
+using Content.Client.StationEvents;
using Content.Client.UserInterface;
using Content.Client.UserInterface.Stylesheets;
using Content.Shared.GameObjects.Components;
@@ -150,6 +151,7 @@ namespace Content.Client
IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
+ IoCManager.Resolve().Initialize();
_baseClient.RunLevelChanged += (sender, args) =>
{
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/Atmos/ExtinguisherVisualizer.cs b/Content.Client/GameObjects/Components/Atmos/ExtinguisherVisualizer.cs
new file mode 100644
index 0000000000..6a33b51e00
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Atmos/ExtinguisherVisualizer.cs
@@ -0,0 +1,67 @@
+using System;
+using JetBrains.Annotations;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+using Robust.Client.GameObjects.Components.Animations;
+using Robust.Client.Interfaces.GameObjects.Components;
+using Robust.Shared.Animations;
+using Robust.Shared.Maths;
+using Content.Shared.GameObjects.Components;
+
+namespace Content.Client.GameObjects.Components.Atmos
+{
+ [UsedImplicitly]
+ public class ExtinguisherVisualizer : AppearanceVisualizer
+ {
+
+ public override void OnChangeData(AppearanceComponent component)
+ {
+ base.OnChangeData(component);
+
+ if (component.Deleted)
+ {
+ return;
+ }
+
+ if (component.TryGetData(ExtinguisherVisuals.Rotation, out var degrees))
+ {
+ SetRotation(component, Angle.FromDegrees(degrees));
+ }
+ }
+
+ private void SetRotation(AppearanceComponent component, Angle rotation)
+ {
+ var sprite = component.Owner.GetComponent();
+
+ if (!sprite.Owner.TryGetComponent(out AnimationPlayerComponent animation))
+ {
+ sprite.Rotation = rotation;
+ return;
+ }
+
+ if (animation.HasRunningAnimation("rotate"))
+ {
+ animation.Stop("rotate");
+ }
+
+ animation.Play(new Animation
+ {
+ Length = TimeSpan.FromSeconds(0.125),
+ AnimationTracks =
+ {
+ new AnimationTrackComponentProperty
+ {
+ ComponentType = typeof(ISpriteComponent),
+ Property = nameof(ISpriteComponent.Rotation),
+ InterpolationMode = AnimationInterpolationMode.Linear,
+ KeyFrames =
+ {
+ new AnimationTrackProperty.KeyFrame(sprite.Rotation, 0),
+ new AnimationTrackProperty.KeyFrame(rotation, 0.125f)
+ }
+ }
+ }
+ }, "rotate");
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs b/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs
new file mode 100644
index 0000000000..61049ec7a0
--- /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/ClickableComponent.cs b/Content.Client/GameObjects/Components/ClickableComponent.cs
index 57d9420167..a92af22451 100644
--- a/Content.Client/GameObjects/Components/ClickableComponent.cs
+++ b/Content.Client/GameObjects/Components/ClickableComponent.cs
@@ -37,7 +37,7 @@ namespace Content.Client.GameObjects.Components
/// True if the click worked, false otherwise.
public bool CheckClick(Vector2 worldPos, out int drawDepth, out uint renderOrder)
{
- if (!Owner.TryGetComponent(out ISpriteComponent sprite) || !sprite.Visible)
+ if (!Owner.TryGetComponent(out ISpriteComponent? sprite) || !sprite.Visible)
{
drawDepth = default;
renderOrder = default;
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/Disposal/DisposalRouterBoundUserInterface.cs b/Content.Client/GameObjects/Components/Disposal/DisposalRouterBoundUserInterface.cs
new file mode 100644
index 0000000000..1bb74c1bee
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Disposal/DisposalRouterBoundUserInterface.cs
@@ -0,0 +1,67 @@
+#nullable enable
+using JetBrains.Annotations;
+using Robust.Client.GameObjects.Components.UserInterface;
+using Robust.Shared.GameObjects.Components.UserInterface;
+using Robust.Shared.Localization;
+using static Content.Shared.GameObjects.Components.Disposal.SharedDisposalRouterComponent;
+
+namespace Content.Client.GameObjects.Components.Disposal
+{
+ ///
+ /// Initializes a and updates it when new server messages are received.
+ ///
+ [UsedImplicitly]
+ public class DisposalRouterBoundUserInterface : BoundUserInterface
+ {
+ private DisposalRouterWindow? _window;
+
+ public DisposalRouterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new DisposalRouterWindow();
+
+ _window.OpenCentered();
+ _window.OnClose += Close;
+
+ _window.Confirm.OnPressed += _ => ButtonPressed(UiAction.Ok, _window.TagInput.Text);
+ _window.TagInput.OnTextEntered += args => ButtonPressed(UiAction.Ok, args.Text);
+
+ }
+
+ private void ButtonPressed(UiAction action, string tag)
+ {
+ SendMessage(new UiActionMessage(action, tag));
+ _window?.Close();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (!(state is DisposalRouterUserInterfaceState cast))
+ {
+ return;
+ }
+
+ _window?.UpdateState(cast);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ {
+ _window?.Dispose();
+ }
+ }
+
+
+ }
+
+}
diff --git a/Content.Client/GameObjects/Components/Disposal/DisposalRouterWindow.cs b/Content.Client/GameObjects/Components/Disposal/DisposalRouterWindow.cs
new file mode 100644
index 0000000000..98e345f124
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Disposal/DisposalRouterWindow.cs
@@ -0,0 +1,51 @@
+using Content.Shared.GameObjects.Components.Disposal;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
+using static Content.Shared.GameObjects.Components.Disposal.SharedDisposalRouterComponent;
+
+namespace Content.Client.GameObjects.Components.Disposal
+{
+ ///
+ /// Client-side UI used to control a
+ ///
+ public class DisposalRouterWindow : SS14Window
+ {
+ public readonly LineEdit TagInput;
+ public readonly Button Confirm;
+
+ protected override Vector2? CustomSize => (400, 80);
+
+ public DisposalRouterWindow()
+ {
+ Title = Loc.GetString("Disposal Router");
+
+ Contents.AddChild(new VBoxContainer
+ {
+ Children =
+ {
+ new Label {Text = Loc.GetString("Tags:")},
+ new Control {CustomMinimumSize = (0, 10)},
+ new HBoxContainer
+ {
+ Children =
+ {
+ (TagInput = new LineEdit {SizeFlagsHorizontal = SizeFlags.Expand, CustomMinimumSize = (320, 0),
+ ToolTip = Loc.GetString("A comma separated list of tags"), IsValid = tags => TagRegex.IsMatch(tags)}),
+ new Control {CustomMinimumSize = (10, 0)},
+ (Confirm = new Button {Text = Loc.GetString("Confirm")})
+ }
+ }
+ }
+ });
+ }
+
+
+ public void UpdateState(DisposalRouterUserInterfaceState state)
+ {
+ TagInput.Text = state.Tags;
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/Disposal/DisposalTaggerBoundUserInterface.cs b/Content.Client/GameObjects/Components/Disposal/DisposalTaggerBoundUserInterface.cs
new file mode 100644
index 0000000000..76d8a4fd48
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Disposal/DisposalTaggerBoundUserInterface.cs
@@ -0,0 +1,67 @@
+#nullable enable
+using JetBrains.Annotations;
+using Robust.Client.GameObjects.Components.UserInterface;
+using Robust.Shared.GameObjects.Components.UserInterface;
+using Robust.Shared.Localization;
+using static Content.Shared.GameObjects.Components.Disposal.SharedDisposalTaggerComponent;
+
+namespace Content.Client.GameObjects.Components.Disposal
+{
+ ///
+ /// Initializes a and updates it when new server messages are received.
+ ///
+ [UsedImplicitly]
+ public class DisposalTaggerBoundUserInterface : BoundUserInterface
+ {
+ private DisposalTaggerWindow? _window;
+
+ public DisposalTaggerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new DisposalTaggerWindow();
+
+ _window.OpenCentered();
+ _window.OnClose += Close;
+
+ _window.Confirm.OnPressed += _ => ButtonPressed(UiAction.Ok, _window.TagInput.Text);
+ _window.TagInput.OnTextEntered += args => ButtonPressed(UiAction.Ok, args.Text);
+
+ }
+
+ private void ButtonPressed(UiAction action, string tag)
+ {
+ SendMessage(new UiActionMessage(action, tag));
+ _window?.Close();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (!(state is DisposalTaggerUserInterfaceState cast))
+ {
+ return;
+ }
+
+ _window?.UpdateState(cast);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ {
+ _window?.Dispose();
+ }
+ }
+
+
+ }
+
+}
diff --git a/Content.Client/GameObjects/Components/Disposal/DisposalTaggerWindow.cs b/Content.Client/GameObjects/Components/Disposal/DisposalTaggerWindow.cs
new file mode 100644
index 0000000000..54dec5b807
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Disposal/DisposalTaggerWindow.cs
@@ -0,0 +1,51 @@
+using Content.Shared.GameObjects.Components.Disposal;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
+using static Content.Shared.GameObjects.Components.Disposal.SharedDisposalTaggerComponent;
+
+namespace Content.Client.GameObjects.Components.Disposal
+{
+ ///
+ /// Client-side UI used to control a
+ ///
+ public class DisposalTaggerWindow : SS14Window
+ {
+ public readonly LineEdit TagInput;
+ public readonly Button Confirm;
+
+ protected override Vector2? CustomSize => (400, 80);
+
+ public DisposalTaggerWindow()
+ {
+ Title = Loc.GetString("Disposal Tagger");
+
+ Contents.AddChild(new VBoxContainer
+ {
+ Children =
+ {
+ new Label {Text = Loc.GetString("Tag:")},
+ new Control {CustomMinimumSize = (0, 10)},
+ new HBoxContainer
+ {
+ Children =
+ {
+ (TagInput = new LineEdit {SizeFlagsHorizontal = SizeFlags.Expand, CustomMinimumSize = (320, 0),
+ IsValid = tag => TagRegex.IsMatch(tag)}),
+ new Control {CustomMinimumSize = (10, 0)},
+ (Confirm = new Button {Text = Loc.GetString("Confirm")})
+ }
+ }
+ }
+ });
+ }
+
+
+ public void UpdateState(DisposalTaggerUserInterfaceState state)
+ {
+ TagInput.Text = state.Tag;
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/Items/HandsComponent.cs b/Content.Client/GameObjects/Components/Items/HandsComponent.cs
index daa03d74e1..30e32afad3 100644
--- a/Content.Client/GameObjects/Components/Items/HandsComponent.cs
+++ b/Content.Client/GameObjects/Components/Items/HandsComponent.cs
@@ -148,7 +148,7 @@ namespace Content.Client.GameObjects.Components.Items
return;
}
- if (!entity.TryGetComponent(out ItemComponent item)) return;
+ if (!entity.TryGetComponent(out ItemComponent? item)) return;
var maybeInHands = item.GetInHandStateInfo(hand.Location);
diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerBoundUserInterface.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerBoundUserInterface.cs
index 135a55f992..2280406c5b 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)
@@ -20,6 +22,7 @@ namespace Content.Client.GameObjects.Components.MedicalScanner
Title = Owner.Owner.Name,
};
_window.OnClose += Close;
+ _window.ScanButton.OnPressed += _ => SendMessage(new UiButtonPressedMessage(UiButton.ScanDNA));
_window.OpenCentered();
}
diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs
index 4f6eefb5d9..4668854fbf 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;
@@ -8,28 +12,63 @@ namespace Content.Client.GameObjects.Components.MedicalScanner
{
public class MedicalScannerWindow : SS14Window
{
+ public readonly Button ScanButton;
+ private readonly Label _diagnostics;
protected override Vector2? CustomSize => (485, 90);
+ public MedicalScannerWindow()
+ {
+ Contents.AddChild(new VBoxContainer
+ {
+ Children =
+ {
+ (ScanButton = new Button
+ {
+ Text = "Scan and Save DNA"
+ }),
+ (_diagnostics = new Label
+ {
+ Text = ""
+ })
+ }
+ });
+ }
+
public void Populate(MedicalScannerBoundUserInterfaceState state)
{
- 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)
- {
- foreach (var (dmgType, amount) in state.DamageDictionary)
- {
- text.Append($"\n{dmgType}: {amount}");
- }
- }
+ if (!state.Entity.HasValue ||
+ !state.HasDamage() ||
+ !IoCManager.Resolve().TryGetEntity(state.Entity.Value, out var entity))
+ {
+ _diagnostics.Text = Loc.GetString("No patient data.");
+ ScanButton.Disabled = true;
+ }
+ else
+ {
+ text.Append($"{entity.Name}{Loc.GetString("'s health:")}\n");
+
+ foreach (var (@class, classAmount) in state.DamageClasses)
+ {
+ text.Append($"\n{Loc.GetString("{0}: {1}", @class, classAmount)}");
+
+ foreach (var type in @class.ToTypes())
+ {
+ if (!state.DamageTypes.TryGetValue(type, out var typeAmount))
+ {
+ continue;
+ }
+
+ text.Append($"\n- {Loc.GetString("{0}: {1}", type, typeAmount)}");
+ }
+
+ text.Append("\n");
+ }
+
+ _diagnostics.Text = text.ToString();
+ ScanButton.Disabled = state.IsScanned;
}
- Contents.AddChild(new Label(){Text = text.ToString()});
}
}
}
diff --git a/Content.Client/GameObjects/Components/Mobs/CameraRecoilComponent.cs b/Content.Client/GameObjects/Components/Mobs/CameraRecoilComponent.cs
index 7cda73f26b..cfdbe607a3 100644
--- a/Content.Client/GameObjects/Components/Mobs/CameraRecoilComponent.cs
+++ b/Content.Client/GameObjects/Components/Mobs/CameraRecoilComponent.cs
@@ -23,7 +23,7 @@ namespace Content.Client.GameObjects.Components.Mobs
private const float RestoreRateRamp = 0.1f;
// The maximum magnitude of the kick applied to the camera at any point.
- private const float KickMagnitudeMax = 5f;
+ private const float KickMagnitudeMax = 2f;
private Vector2 _currentKick;
private float _lastKickTime;
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/StunnableComponent.cs b/Content.Client/GameObjects/Components/Mobs/StunnableComponent.cs
index 4c02d343cb..9d6ce08f8f 100644
--- a/Content.Client/GameObjects/Components/Mobs/StunnableComponent.cs
+++ b/Content.Client/GameObjects/Components/Mobs/StunnableComponent.cs
@@ -34,7 +34,7 @@ namespace Content.Client.GameObjects.Components.Mobs
WalkModifierOverride = state.WalkModifierOverride;
RunModifierOverride = state.RunModifierOverride;
- if (Owner.TryGetComponent(out MovementSpeedModifierComponent movement))
+ if (Owner.TryGetComponent(out MovementSpeedModifierComponent? movement))
{
movement.RefreshMovementSpeedModifiers();
}
diff --git a/Content.Client/GameObjects/Components/Movement/ClimbableComponent.cs b/Content.Client/GameObjects/Components/Movement/ClimbableComponent.cs
new file mode 100644
index 0000000000..d637853960
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Movement/ClimbableComponent.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.GameObjects;
+using Content.Shared.GameObjects.Components.Movement;
+
+namespace Content.Client.GameObjects.Components.Movement
+{
+ [RegisterComponent]
+ [ComponentReference(typeof(IClimbable))]
+ public class ClimbableComponent : SharedClimbableComponent
+ {
+
+ }
+}
diff --git a/Content.Client/GameObjects/Components/Movement/ClimbingComponent.cs b/Content.Client/GameObjects/Components/Movement/ClimbingComponent.cs
new file mode 100644
index 0000000000..07f8c7c5b6
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Movement/ClimbingComponent.cs
@@ -0,0 +1,34 @@
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Components;
+using Content.Shared.GameObjects.Components.Movement;
+using Content.Client.Interfaces.GameObjects.Components.Interaction;
+using Content.Shared.Physics;
+
+namespace Content.Client.GameObjects.Components.Movement
+{
+ [RegisterComponent]
+ public class ClimbingComponent : SharedClimbingComponent, IClientDraggable
+ {
+ public override void HandleComponentState(ComponentState curState, ComponentState nextState)
+ {
+ if (!(curState is ClimbModeComponentState climbModeState) || Body == null)
+ {
+ return;
+ }
+
+ IsClimbing = climbModeState.Climbing;
+ }
+
+ public override bool IsClimbing { get; set; }
+
+ bool IClientDraggable.ClientCanDropOn(CanDropEventArgs eventArgs)
+ {
+ return eventArgs.Target.HasComponent();
+ }
+
+ bool IClientDraggable.ClientCanDrag(CanDragEventArgs eventArgs)
+ {
+ return true;
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/Nutrition/HungerComponent.cs b/Content.Client/GameObjects/Components/Nutrition/HungerComponent.cs
index 8323fb5e7a..6db62b1bb3 100644
--- a/Content.Client/GameObjects/Components/Nutrition/HungerComponent.cs
+++ b/Content.Client/GameObjects/Components/Nutrition/HungerComponent.cs
@@ -20,7 +20,7 @@ namespace Content.Client.GameObjects.Components.Nutrition
_currentHungerThreshold = hunger.CurrentThreshold;
- if (Owner.TryGetComponent(out MovementSpeedModifierComponent movement))
+ if (Owner.TryGetComponent(out MovementSpeedModifierComponent? movement))
{
movement.RefreshMovementSpeedModifiers();
}
diff --git a/Content.Client/GameObjects/Components/Nutrition/ThirstComponent.cs b/Content.Client/GameObjects/Components/Nutrition/ThirstComponent.cs
index b77d59a34a..a211afe239 100644
--- a/Content.Client/GameObjects/Components/Nutrition/ThirstComponent.cs
+++ b/Content.Client/GameObjects/Components/Nutrition/ThirstComponent.cs
@@ -20,7 +20,7 @@ namespace Content.Client.GameObjects.Components.Nutrition
_currentThirstThreshold = thirst.CurrentThreshold;
- if (Owner.TryGetComponent(out MovementSpeedModifierComponent movement))
+ if (Owner.TryGetComponent(out MovementSpeedModifierComponent? movement))
{
movement.RefreshMovementSpeedModifiers();
}
diff --git a/Content.Client/GameObjects/Components/PDA/PDAVisualizer.cs b/Content.Client/GameObjects/Components/PDA/PDAVisualizer.cs
index 019bd03ae1..5ea8bbbbc1 100644
--- a/Content.Client/GameObjects/Components/PDA/PDAVisualizer.cs
+++ b/Content.Client/GameObjects/Components/PDA/PDAVisualizer.cs
@@ -1,4 +1,4 @@
-using Content.Shared.GameObjects.Components.PDA;
+using Content.Shared.GameObjects.Components.PDA;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
@@ -10,7 +10,7 @@ namespace Content.Client.GameObjects.Components.PDA
private enum PDAVisualLayers
{
Base,
- Unlit
+ Flashlight
}
@@ -22,13 +22,13 @@ namespace Content.Client.GameObjects.Components.PDA
return;
}
var sprite = component.Owner.GetComponent();
- sprite.LayerSetVisible(PDAVisualLayers.Unlit, false);
- if(!component.TryGetData(PDAVisuals.ScreenLit, out var isScreenLit))
+ sprite.LayerSetVisible(PDAVisualLayers.Flashlight, false);
+ if(!component.TryGetData(PDAVisuals.FlashlightLit, out var isScreenLit))
{
return;
}
- sprite.LayerSetState(PDAVisualLayers.Unlit, "unlit_pda_screen");
- sprite.LayerSetVisible(PDAVisualLayers.Unlit, isScreenLit);
+ sprite.LayerSetState(PDAVisualLayers.Flashlight, "light_overlay");
+ sprite.LayerSetVisible(PDAVisualLayers.Flashlight, isScreenLit);
}
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/DoAfter/DoAfterGui.cs b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterGui.cs
index b46b359ff4..a6724c0f6a 100644
--- a/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterGui.cs
+++ b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterGui.cs
@@ -143,7 +143,7 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter
{
base.FrameUpdate(args);
- if (AttachedEntity?.IsValid() != true || !AttachedEntity.TryGetComponent(out DoAfterComponent doAfterComponent))
+ if (AttachedEntity?.IsValid() != true || !AttachedEntity.TryGetComponent(out DoAfterComponent? doAfterComponent))
{
return;
}
diff --git a/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs
index 8aed094c7f..cc0337a941 100644
--- a/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs
+++ b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs
@@ -67,7 +67,7 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter
Gui ??= new DoAfterGui();
Gui.AttachedEntity = entity;
- if (entity.TryGetComponent(out DoAfterComponent doAfterComponent))
+ if (entity.TryGetComponent(out DoAfterComponent? doAfterComponent))
{
foreach (var (_, doAfter) in doAfterComponent.DoAfters)
{
@@ -87,7 +87,7 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter
return;
}
- if (!_player.TryGetComponent(out DoAfterComponent doAfterComponent))
+ if (!_player.TryGetComponent(out DoAfterComponent? doAfterComponent))
{
return;
}
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/GameObjects/EntitySystems/MoverSystem.cs b/Content.Client/GameObjects/EntitySystems/MoverSystem.cs
index cdabb87d7c..db3ad9fc33 100644
--- a/Content.Client/GameObjects/EntitySystems/MoverSystem.cs
+++ b/Content.Client/GameObjects/EntitySystems/MoverSystem.cs
@@ -25,7 +25,7 @@ namespace Content.Client.GameObjects.EntitySystems
{
var playerEnt = _playerManager.LocalPlayer?.ControlledEntity;
- if (playerEnt == null || !playerEnt.TryGetComponent(out IMoverComponent mover))
+ if (playerEnt == null || !playerEnt.TryGetComponent(out IMoverComponent? mover))
{
return;
}
diff --git a/Content.Client/GameTicking/ClientGameTicker.cs b/Content.Client/GameTicking/ClientGameTicker.cs
index a80f606215..1ae8bcd8a7 100644
--- a/Content.Client/GameTicking/ClientGameTicker.cs
+++ b/Content.Client/GameTicking/ClientGameTicker.cs
@@ -1,11 +1,15 @@
using System;
+using System.Collections.Generic;
using Content.Client.Interfaces;
using Content.Client.State;
using Content.Client.UserInterface;
using Content.Shared;
+using Content.Shared.Network.NetMessages;
+using Robust.Client.Interfaces.Graphics;
using Robust.Client.Interfaces.State;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
+using Robust.Shared.Network;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -22,12 +26,16 @@ namespace Content.Client.GameTicking
[ViewVariables] public bool AreWeReady { get; private set; }
[ViewVariables] public bool IsGameStarted { get; private set; }
+ [ViewVariables] public bool DisallowedLateJoin { get; private set; }
[ViewVariables] public string ServerInfoBlob { get; private set; }
[ViewVariables] public DateTime StartTime { get; private set; }
[ViewVariables] public bool Paused { get; private set; }
+ [ViewVariables] public Dictionary Ready { get; private set; }
public event Action InfoBlobUpdated;
public event Action LobbyStatusUpdated;
+ public event Action LobbyReadyUpdated;
+ public event Action LobbyLateJoinStatusUpdated;
public void Initialize()
{
@@ -38,11 +46,23 @@ namespace Content.Client.GameTicking
_netManager.RegisterNetMessage(nameof(MsgTickerLobbyStatus), LobbyStatus);
_netManager.RegisterNetMessage(nameof(MsgTickerLobbyInfo), LobbyInfo);
_netManager.RegisterNetMessage(nameof(MsgTickerLobbyCountdown), LobbyCountdown);
+ _netManager.RegisterNetMessage(nameof(MsgTickerLobbyReady), LobbyReady);
_netManager.RegisterNetMessage(nameof(MsgRoundEndMessage), RoundEnd);
+ _netManager.RegisterNetMessage(nameof(MsgRequestWindowAttention), msg =>
+ {
+ IoCManager.Resolve().RequestWindowAttention();
+ });
+ _netManager.RegisterNetMessage(nameof(MsgTickerLateJoinStatus), LateJoinStatus);
+ Ready = new Dictionary();
_initialized = true;
}
+ private void LateJoinStatus(MsgTickerLateJoinStatus message)
+ {
+ DisallowedLateJoin = message.Disallowed;
+ LobbyLateJoinStatusUpdated?.Invoke();
+ }
private void JoinLobby(MsgTickerJoinLobby message)
@@ -56,6 +76,8 @@ namespace Content.Client.GameTicking
IsGameStarted = message.IsRoundStarted;
AreWeReady = message.YouAreReady;
Paused = message.Paused;
+ if (IsGameStarted)
+ Ready.Clear();
LobbyStatusUpdated?.Invoke();
}
@@ -78,11 +100,20 @@ namespace Content.Client.GameTicking
Paused = message.Paused;
}
+ private void LobbyReady(MsgTickerLobbyReady message)
+ {
+ // Merge the Dictionaries
+ foreach (var p in message.PlayerReady)
+ {
+ Ready[p.Key] = p.Value;
+ }
+ LobbyReadyUpdated?.Invoke();
+ }
+
private void RoundEnd(MsgRoundEndMessage message)
{
-
//This is not ideal at all, but I don't see an immediately better fit anywhere else.
- var roundEnd = new RoundEndSummaryWindow(message.GamemodeTitle, message.RoundDuration, message.AllPlayersEndInfo);
+ var roundEnd = new RoundEndSummaryWindow(message.GamemodeTitle, message.RoundEndText, message.RoundDuration, message.AllPlayersEndInfo);
}
}
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..6e67d79995 100644
--- a/Content.Client/IgnoredComponents.cs
+++ b/Content.Client/IgnoredComponents.cs
@@ -96,7 +96,6 @@
"BarSign",
"DroppedBodyPart",
"DroppedMechanism",
- "BodyManager",
"SolarPanel",
"BodyScanner",
"Stunbaton",
@@ -143,6 +142,8 @@
"Listening",
"Radio",
"DisposalHolder",
+ "DisposalTagger",
+ "DisposalRouter",
"DisposalTransit",
"DisposalEntry",
"DisposalJunction",
@@ -157,6 +158,10 @@
"Vapor",
"DamageOnHighSpeedImpact",
"Barotrauma",
+ "GasSprayer",
+ "GasVapor",
+ "MobStateManager",
+ "Metabolism",
};
}
}
diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs
index 1250627a4f..89ddd64268 100644
--- a/Content.Client/Input/ContentContexts.cs
+++ b/Content.Client/Input/ContentContexts.cs
@@ -19,6 +19,7 @@ namespace Content.Client.Input
common.AddFunction(ContentKeyFunctions.OpenTutorial);
common.AddFunction(ContentKeyFunctions.TakeScreenshot);
common.AddFunction(ContentKeyFunctions.TakeScreenshotNoUI);
+ common.AddFunction(ContentKeyFunctions.Point);
var human = contexts.GetContext("human");
human.AddFunction(ContentKeyFunctions.SwapHands);
@@ -37,9 +38,6 @@ namespace Content.Client.Input
human.AddFunction(ContentKeyFunctions.MouseMiddle);
human.AddFunction(ContentKeyFunctions.ToggleCombatMode);
human.AddFunction(ContentKeyFunctions.WideAttack);
- human.AddFunction(ContentKeyFunctions.Point);
- human.AddFunction(ContentKeyFunctions.TryPullObject);
- human.AddFunction(ContentKeyFunctions.MovePulledObject);
var ghost = contexts.New("ghost", "common");
ghost.AddFunction(EngineKeyFunctions.MoveUp);
diff --git a/Content.Client/Interfaces/IClientGameTicker.cs b/Content.Client/Interfaces/IClientGameTicker.cs
index 8d47a0e1dc..e1e584e2f3 100644
--- a/Content.Client/Interfaces/IClientGameTicker.cs
+++ b/Content.Client/Interfaces/IClientGameTicker.cs
@@ -1,4 +1,6 @@
+using Robust.Shared.Network;
using System;
+using System.Collections.Generic;
namespace Content.Client.Interfaces
{
@@ -7,11 +9,15 @@ namespace Content.Client.Interfaces
bool IsGameStarted { get; }
string ServerInfoBlob { get; }
bool AreWeReady { get; }
+ bool DisallowedLateJoin { get; }
DateTime StartTime { get; }
bool Paused { get; }
+ Dictionary Ready { get; }
void Initialize();
event Action InfoBlobUpdated;
event Action LobbyStatusUpdated;
+ event Action LobbyReadyUpdated;
+ event Action LobbyLateJoinStatusUpdated;
}
}
diff --git a/Content.Client/State/GameScreenBase.cs b/Content.Client/State/GameScreenBase.cs
index 1372dacce5..64e62d2b16 100644
--- a/Content.Client/State/GameScreenBase.cs
+++ b/Content.Client/State/GameScreenBase.cs
@@ -226,7 +226,10 @@ namespace Content.Client.State
// client side command handlers will always be sent the local player session.
var session = PlayerManager.LocalPlayer.Session;
- inputSys.HandleInputCommand(session, func, message);
+ if (inputSys.HandleInputCommand(session, func, message))
+ {
+ args.Handle();
+ }
}
}
}
diff --git a/Content.Client/State/LobbyState.cs b/Content.Client/State/LobbyState.cs
index 23f1d28d83..2580dadb64 100644
--- a/Content.Client/State/LobbyState.cs
+++ b/Content.Client/State/LobbyState.cs
@@ -99,14 +99,20 @@ namespace Content.Client.State
_playerManager.PlayerListUpdated += PlayerManagerOnPlayerListUpdated;
_clientGameTicker.InfoBlobUpdated += UpdateLobbyUi;
- _clientGameTicker.LobbyStatusUpdated += UpdateLobbyUi;
+ _clientGameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
+ _clientGameTicker.LobbyReadyUpdated += LobbyReadyUpdated;
+ _clientGameTicker.LobbyLateJoinStatusUpdated += LobbyLateJoinStatusUpdated;
}
public override void Shutdown()
{
_playerManager.PlayerListUpdated -= PlayerManagerOnPlayerListUpdated;
_clientGameTicker.InfoBlobUpdated -= UpdateLobbyUi;
- _clientGameTicker.LobbyStatusUpdated -= UpdateLobbyUi;
+ _clientGameTicker.LobbyStatusUpdated -= LobbyStatusUpdated;
+ _clientGameTicker.LobbyReadyUpdated -= LobbyReadyUpdated;
+ _clientGameTicker.LobbyLateJoinStatusUpdated -= LobbyLateJoinStatusUpdated;
+
+ _clientGameTicker.Ready.Clear();
_lobby.Dispose();
_characterSetup.Dispose();
@@ -149,7 +155,30 @@ namespace Content.Client.State
_lobby.StartTime.Text = Loc.GetString("Round Starts In: {0}", text);
}
- private void PlayerManagerOnPlayerListUpdated(object sender, EventArgs e) => UpdatePlayerList();
+ private void PlayerManagerOnPlayerListUpdated(object sender, EventArgs e)
+ {
+ // Remove disconnected sessions from the Ready Dict
+ foreach (var p in _clientGameTicker.Ready)
+ {
+ if (!_playerManager.SessionsDict.TryGetValue(p.Key, out _))
+ {
+ _clientGameTicker.Ready.Remove(p.Key);
+ }
+ }
+ UpdatePlayerList();
+ }
+ private void LobbyReadyUpdated() => UpdatePlayerList();
+
+ private void LobbyStatusUpdated()
+ {
+ UpdatePlayerList();
+ UpdateLobbyUi();
+ }
+
+ private void LobbyLateJoinStatusUpdated()
+ {
+ _lobby.ReadyButton.Disabled = _clientGameTicker.DisallowedLateJoin;
+ }
private void UpdateLobbyUi()
{
@@ -169,6 +198,7 @@ namespace Content.Client.State
_lobby.StartTime.Text = "";
_lobby.ReadyButton.Text = Loc.GetString("Ready Up");
_lobby.ReadyButton.ToggleMode = true;
+ _lobby.ReadyButton.Disabled = false;
_lobby.ReadyButton.Pressed = _clientGameTicker.AreWeReady;
}
@@ -178,10 +208,24 @@ namespace Content.Client.State
private void UpdatePlayerList()
{
_lobby.OnlinePlayerItemList.Clear();
+ _lobby.PlayerReadyList.Clear();
foreach (var session in _playerManager.Sessions.OrderBy(s => s.Name))
{
_lobby.OnlinePlayerItemList.AddItem(session.Name);
+
+ var readyState = "";
+ // Don't show ready state if we're ingame
+ if (!_clientGameTicker.IsGameStarted)
+ {
+ var ready = false;
+ if (session.SessionId == _playerManager.LocalPlayer.SessionId)
+ ready = _clientGameTicker.AreWeReady;
+ else
+ _clientGameTicker.Ready.TryGetValue(session.SessionId, out ready);
+ readyState = ready ? Loc.GetString("Ready") : Loc.GetString("Not Ready");
+ }
+ _lobby.PlayerReadyList.AddItem(readyState, null, false);
}
}
@@ -193,6 +237,7 @@ namespace Content.Client.State
}
_console.ProcessCommand($"toggleready {newReady}");
+ UpdatePlayerList();
}
}
}
diff --git a/Content.Client/StationEvents/IStationEventManager.cs b/Content.Client/StationEvents/IStationEventManager.cs
new file mode 100644
index 0000000000..828b20e80d
--- /dev/null
+++ b/Content.Client/StationEvents/IStationEventManager.cs
@@ -0,0 +1,13 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+
+namespace Content.Client.StationEvents
+{
+ public interface IStationEventManager
+ {
+ public List? StationEvents { get; }
+ public void Initialize();
+ public event Action OnStationEventsReceived;
+ }
+}
diff --git a/Content.Client/StationEvents/StationEventManager.cs b/Content.Client/StationEvents/StationEventManager.cs
new file mode 100644
index 0000000000..eb1ce55416
--- /dev/null
+++ b/Content.Client/StationEvents/StationEventManager.cs
@@ -0,0 +1,42 @@
+#nullable enable
+using Content.Shared.StationEvents;
+using Robust.Shared.Interfaces.Network;
+using Robust.Shared.IoC;
+using System;
+using System.Collections.Generic;
+
+namespace Content.Client.StationEvents
+{
+ class StationEventManager : SharedStationEvent, IStationEventManager
+ {
+ private List? _events;
+ public List? StationEvents
+ {
+ get
+ {
+ if (_events == null)
+ RequestEvents();
+ return _events;
+ }
+ }
+ public event Action? OnStationEventsReceived;
+
+ public void Initialize()
+ {
+ var netManager = IoCManager.Resolve();
+ netManager.RegisterNetMessage(nameof(MsgGetStationEvents), EventHandler);
+ netManager.Disconnect += (sender, msg) => _events = null;
+ }
+
+ private void EventHandler(MsgGetStationEvents msg)
+ {
+ _events = msg.Events;
+ OnStationEventsReceived?.Invoke();
+ }
+ public void RequestEvents()
+ {
+ var netManager = IoCManager.Resolve();
+ netManager.ClientSendMessage(netManager.CreateNetMessage());
+ }
+ }
+}
diff --git a/Content.Client/UserInterface/CooldownGraphic.cs b/Content.Client/UserInterface/CooldownGraphic.cs
index a092cf425b..99fa4fe18b 100644
--- a/Content.Client/UserInterface/CooldownGraphic.cs
+++ b/Content.Client/UserInterface/CooldownGraphic.cs
@@ -30,6 +30,7 @@ namespace Content.Client.UserInterface
protected override void Draw(DrawingHandleScreen handle)
{
+ Span x = stackalloc float[10];
Color color;
var lerp = 1f - MathF.Abs(Progress); // for future bikeshedding purposes
diff --git a/Content.Client/UserInterface/LobbyGui.cs b/Content.Client/UserInterface/LobbyGui.cs
index 2b077446f1..2f8caf3574 100644
--- a/Content.Client/UserInterface/LobbyGui.cs
+++ b/Content.Client/UserInterface/LobbyGui.cs
@@ -1,4 +1,4 @@
-using Content.Client.Chat;
+using Content.Client.Chat;
using Content.Client.Interfaces;
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
@@ -22,6 +22,7 @@ namespace Content.Client.UserInterface
public Button LeaveButton { get; }
public ChatBox Chat { get; }
public ItemList OnlinePlayerItemList { get; }
+ public ItemList PlayerReadyList { get; }
public ServerInfo ServerInfo { get; }
public LobbyCharacterPreviewPanel CharacterPreview { get; }
@@ -219,7 +220,25 @@ namespace Content.Client.UserInterface
MarginBottomOverride = 3,
Children =
{
- (OnlinePlayerItemList = new ItemList())
+ new HBoxContainer
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ CustomMinimumSize = (50,50),
+ Children =
+ {
+ (OnlinePlayerItemList = new ItemList
+ {
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ }),
+ (PlayerReadyList = new ItemList
+ {
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 0.2f
+ }),
+ }
+ }
}
},
new NanoHeading
diff --git a/Content.Client/UserInterface/RoundEndSummaryWindow.cs b/Content.Client/UserInterface/RoundEndSummaryWindow.cs
index 5f2d736d9c..8d40c0b8ba 100644
--- a/Content.Client/UserInterface/RoundEndSummaryWindow.cs
+++ b/Content.Client/UserInterface/RoundEndSummaryWindow.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using Content.Client.Utility;
+using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
@@ -17,7 +18,7 @@ namespace Content.Client.UserInterface
private TabContainer RoundEndWindowTabs { get; }
protected override Vector2? CustomSize => (520, 580);
- public RoundEndSummaryWindow(string gm, TimeSpan roundTimeSpan, List info )
+ public RoundEndSummaryWindow(string gm, string roundEnd, TimeSpan roundTimeSpan, List info)
{
Title = Loc.GetString("Round End Summary");
@@ -49,6 +50,14 @@ namespace Content.Client.UserInterface
gamemodeLabel.SetMarkup(Loc.GetString("Round of [color=white]{0}[/color] has ended.", gm));
RoundEndSummaryTab.AddChild(gamemodeLabel);
+ //Round end text
+ if (!string.IsNullOrEmpty(roundEnd))
+ {
+ var roundendLabel = new RichTextLabel();
+ roundendLabel.SetMarkup(Loc.GetString(roundEnd));
+ RoundEndSummaryTab.AddChild(roundendLabel);
+ }
+
//Duration
var roundTimeLabel = new RichTextLabel();
roundTimeLabel.SetMarkup(Loc.GetString("It lasted for [color=yellow]{0} hours, {1} minutes, and {2} seconds.",
@@ -65,30 +74,40 @@ namespace Content.Client.UserInterface
//Create labels for each player info.
foreach (var plyinfo in manifestSortedList)
{
-
var playerInfoText = new RichTextLabel()
{
- SizeFlagsVertical = SizeFlags.Fill
+ SizeFlagsVertical = SizeFlags.Fill,
};
//TODO: On Hover display a popup detailing more play info.
//For example: their antag goals and if they completed them sucessfully.
var icNameColor = plyinfo.Antag ? "red" : "white";
playerInfoText.SetMarkup(
- Loc.GetString($"[color=gray]{plyinfo.PlayerOOCName}[/color] was [color={icNameColor}]{plyinfo.PlayerICName}[/color] playing role of [color=orange]{plyinfo.Role}[/color]."));
+ Loc.GetString("[color=gray]{0}[/color] was [color={1}]{2}[/color] playing role of [color=orange]{3}[/color].",
+ plyinfo.PlayerOOCName, icNameColor, plyinfo.PlayerICName, Loc.GetString(plyinfo.Role)));
innerScrollContainer.AddChild(playerInfoText);
}
scrollContainer.AddChild(innerScrollContainer);
//Attach the entire ScrollContainer that holds all the playerinfo.
PlayerManifestoTab.AddChild(scrollContainer);
+ // TODO: 1240 Overlap, remove once it's fixed. Temp Hack to make the lines not overlap
+ PlayerManifestoTab.OnVisibilityChanged += PlayerManifestoTab_OnVisibilityChanged;
//Finally, display the window.
OpenCentered();
MoveToFront();
-
}
+ private void PlayerManifestoTab_OnVisibilityChanged(Control obj)
+ {
+ if (obj.Visible)
+ {
+ // For some reason the lines get not properly drawn with the right height
+ // so we just force a update
+ ForceRunLayoutUpdate();
+ }
+ }
}
}
diff --git a/Content.IntegrationTests/ContentIntegrationTest.cs b/Content.IntegrationTests/ContentIntegrationTest.cs
index 1970ba9ee7..023cf5fe0f 100644
--- a/Content.IntegrationTests/ContentIntegrationTest.cs
+++ b/Content.IntegrationTests/ContentIntegrationTest.cs
@@ -4,6 +4,7 @@ using Content.Client;
using Content.Client.Interfaces.Parallax;
using Content.Server;
using Content.Server.Interfaces.GameTicking;
+using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
@@ -12,6 +13,7 @@ using EntryPoint = Content.Client.EntryPoint;
namespace Content.IntegrationTests
{
+ [Parallelizable(ParallelScope.All)]
public abstract class ContentIntegrationTest : RobustIntegrationTest
{
protected sealed override ClientIntegrationInstance StartClient(ClientIntegrationOptions options = null)
diff --git a/Content.IntegrationTests/DummyGameTicker.cs b/Content.IntegrationTests/DummyGameTicker.cs
index f135339c75..afe44ed8ff 100644
--- a/Content.IntegrationTests/DummyGameTicker.cs
+++ b/Content.IntegrationTests/DummyGameTicker.cs
@@ -41,7 +41,7 @@ namespace Content.IntegrationTests
{
}
- public void EndRound()
+ public void EndRound(string roundEnd)
{
}
diff --git a/Content.IntegrationTests/Tests/Atmos/AtmosHelpersTest.cs b/Content.IntegrationTests/Tests/Atmos/AtmosHelpersTest.cs
new file mode 100644
index 0000000000..844198ce66
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/AtmosHelpersTest.cs
@@ -0,0 +1,160 @@
+using System.Threading.Tasks;
+using Content.Server.Atmos;
+using NUnit.Framework;
+using Robust.Shared.Map;
+
+namespace Content.IntegrationTests.Tests.Atmos
+{
+ [TestFixture]
+ [TestOf(typeof(AtmosHelpersTest))]
+ public class AtmosHelpersTest : ContentIntegrationTest
+ {
+ [Test]
+ public async Task GetTileAtmosphereGridCoordinatesNullTest()
+ {
+ var server = StartServerDummyTicker();
+
+ server.Assert(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ var atmosphere = default(GridCoordinates).GetTileAtmosphere();
+
+ Assert.Null(atmosphere);
+ });
+ });
+
+ await server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task GetTileAirGridCoordinatesNullTest()
+ {
+ var server = StartServerDummyTicker();
+
+ server.Assert(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ var air = default(GridCoordinates).GetTileAir();
+
+ Assert.Null(air);
+ });
+ });
+
+ await server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task TryGetTileAtmosphereGridCoordinatesNullTest()
+ {
+ var server = StartServerDummyTicker();
+
+ server.Assert(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ var hasAtmosphere = default(GridCoordinates).TryGetTileAtmosphere(out var atmosphere);
+
+ Assert.False(hasAtmosphere);
+ Assert.Null(atmosphere);
+ });
+ });
+
+ await server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task TryGetTileTileAirGridCoordinatesNullTest()
+ {
+ var server = StartServerDummyTicker();
+
+ server.Assert(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ var hasAir = default(GridCoordinates).TryGetTileAir(out var air);
+
+ Assert.False(hasAir);
+ Assert.Null(air);
+ });
+ });
+
+ await server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task GetTileAtmosphereMapIndicesNullTest()
+ {
+ var server = StartServerDummyTicker();
+
+ server.Assert(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ var atmosphere = default(MapIndices).GetTileAtmosphere(default);
+
+ Assert.Null(atmosphere);
+ });
+ });
+
+ await server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task GetTileAirMapIndicesNullTest()
+ {
+ var server = StartServerDummyTicker();
+
+ server.Assert(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ var air = default(MapIndices).GetTileAir(default);
+
+ Assert.Null(air);
+ });
+ });
+
+ await server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task TryGetTileAtmosphereMapIndicesNullTest()
+ {
+ var server = StartServerDummyTicker();
+
+ server.Assert(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ var hasAtmosphere = default(MapIndices).TryGetTileAtmosphere(default, out var atmosphere);
+
+ Assert.False(hasAtmosphere);
+ Assert.Null(atmosphere);
+ });
+ });
+
+ await server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task TryGetTileAirMapIndicesNullTest()
+ {
+ var server = StartServerDummyTicker();
+
+ server.Assert(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ var hasAir = default(MapIndices).TryGetTileAir(default, out var air);
+
+ Assert.False(hasAir);
+ Assert.Null(air);
+ });
+ });
+
+ await server.WaitIdleAsync();
+ }
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs
index 15b2167631..3eb3821634 100644
--- a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs
+++ b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs
@@ -66,7 +66,7 @@ namespace Content.IntegrationTests.Tests.Disposal
DisposalUnitComponent unit;
DisposalEntryComponent entry;
- server.Assert(() =>
+ server.Assert(async () =>
{
var mapManager = IoCManager.Resolve();
@@ -81,19 +81,19 @@ namespace Content.IntegrationTests.Tests.Disposal
var disposalTrunk = entityManager.SpawnEntity("DisposalTrunk", disposalUnit.Transform.MapPosition);
// Test for components existing
- Assert.True(disposalUnit.TryGetComponent(out unit));
- Assert.True(disposalTrunk.TryGetComponent(out entry));
+ Assert.True(disposalUnit.TryGetComponent(out unit!));
+ Assert.True(disposalTrunk.TryGetComponent(out entry!));
// Can't insert, unanchored and unpowered
var disposalUnitAnchorable = disposalUnit.GetComponent();
- disposalUnitAnchorable.TryUnAnchor(human, null, true);
+ await disposalUnitAnchorable.TryUnAnchor(human, null, true);
Assert.False(unit.Anchored);
UnitInsertContains(unit, false, human, wrench, disposalUnit, disposalTrunk);
// Anchor the disposal unit
- disposalUnitAnchorable.TryAnchor(human, null, true);
- Assert.True(disposalUnit.TryGetComponent(out AnchorableComponent anchorableUnit));
- Assert.True(anchorableUnit.TryAnchor(human, wrench));
+ await disposalUnitAnchorable.TryAnchor(human, null, true);
+ Assert.True(disposalUnit.TryGetComponent(out AnchorableComponent? anchorableUnit));
+ Assert.True(await anchorableUnit!.TryAnchor(human, wrench));
Assert.True(unit.Anchored);
// No power
@@ -118,8 +118,8 @@ namespace Content.IntegrationTests.Tests.Disposal
Flush(unit, false, entry, human, wrench);
// Remove power need
- Assert.True(disposalUnit.TryGetComponent(out PowerReceiverComponent power));
- power.NeedsPower = false;
+ Assert.True(disposalUnit.TryGetComponent(out PowerReceiverComponent? power));
+ power!.NeedsPower = false;
Assert.True(unit.Powered);
// Flush with a mob and an item
diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs
index 4df604e4ad..639a2f78b6 100644
--- a/Content.IntegrationTests/Tests/EntityTest.cs
+++ b/Content.IntegrationTests/Tests/EntityTest.cs
@@ -19,7 +19,7 @@ namespace Content.IntegrationTests.Tests
public class EntityTest : ContentIntegrationTest
{
[Test]
- public async Task Test()
+ public async Task SpawnTest()
{
var server = StartServerDummyTicker();
await server.WaitIdleAsync();
@@ -41,43 +41,65 @@ namespace Content.IntegrationTests.Tests
});
server.Assert(() =>
+ {
+ var testLocation = new GridCoordinates(new Vector2(0, 0), grid);
+
+ //Generate list of non-abstract prototypes to test
+ foreach (var prototype in prototypeMan.EnumeratePrototypes())
{
- var testLocation = new GridCoordinates(new Vector2(0, 0), grid);
-
- //Generate list of non-abstract prototypes to test
- foreach (var prototype in prototypeMan.EnumeratePrototypes())
+ if (prototype.Abstract)
{
- if (prototype.Abstract)
- {
- continue;
- }
- prototypes.Add(prototype);
+ continue;
+ }
+ prototypes.Add(prototype);
+ }
+
+ //Iterate list of prototypes to spawn
+ foreach (var prototype in prototypes)
+ {
+ try
+ {
+ Logger.LogS(LogLevel.Debug, "EntityTest", "Testing: " + prototype.ID);
+ testEntity = entityMan.SpawnEntity(prototype.ID, testLocation);
+ server.RunTicks(2);
+ Assert.That(testEntity.Initialized);
+ entityMan.DeleteEntity(testEntity.Uid);
}
- //Iterate list of prototypes to spawn
- foreach (var prototype in prototypes)
+ //Fail any exceptions thrown on spawn
+ catch (Exception e)
{
- try
- {
- Logger.LogS(LogLevel.Debug, "EntityTest", "Testing: " + prototype.ID);
- testEntity = entityMan.SpawnEntity(prototype.ID, testLocation);
- server.RunTicks(2);
- Assert.That(testEntity.Initialized);
- entityMan.DeleteEntity(testEntity.Uid);
- }
-
- //Fail any exceptions thrown on spawn
- catch (Exception e)
- {
- Logger.LogS(LogLevel.Error, "EntityTest", "Entity '" + prototype.ID + "' threw: " + e.Message);
- //Assert.Fail();
- throw;
- }
+ Logger.LogS(LogLevel.Error, "EntityTest", "Entity '" + prototype.ID + "' threw: " + e.Message);
+ //Assert.Fail();
+ throw;
}
- });
+ }
+ });
await server.WaitIdleAsync();
}
+ [Test]
+ public async Task NotAbstractIconTest()
+ {
+ var client = StartClient();
+ await client.WaitIdleAsync();
+ var prototypeMan = client.ResolveDependency();
+
+ client.Assert(() =>
+ {
+ foreach (var prototype in prototypeMan.EnumeratePrototypes())
+ {
+ if (prototype.Abstract)
+ {
+ continue;
+ }
+
+ Assert.That(prototype.Components.ContainsKey("Icon"), $"Entity {prototype.ID} does not have an Icon component, but is not abstract");
+ }
+ });
+
+ await client.WaitIdleAsync();
+ }
}
}
diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Movement/ClimbUnitTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Movement/ClimbUnitTest.cs
new file mode 100644
index 0000000000..e55218ef09
--- /dev/null
+++ b/Content.IntegrationTests/Tests/GameObjects/Components/Movement/ClimbUnitTest.cs
@@ -0,0 +1,64 @@
+#nullable enable
+
+using System.Threading.Tasks;
+using NUnit.Framework;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Map;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Content.Server.GameObjects.Components.Movement;
+using Content.Shared.Physics;
+using Robust.Shared.GameObjects.Components;
+
+namespace Content.IntegrationTests.Tests.GameObjects.Components.Movement
+{
+ [TestFixture]
+ [TestOf(typeof(ClimbableComponent))]
+ [TestOf(typeof(ClimbingComponent))]
+ public class ClimbUnitTest : ContentIntegrationTest
+ {
+ [Test]
+ public async Task Test()
+ {
+ var server = StartServerDummyTicker();
+
+ IEntity human;
+ IEntity table;
+ IEntity carpet;
+ ClimbableComponent climbable;
+ ClimbingComponent climbing;
+
+ server.Assert(() =>
+ {
+ var mapManager = IoCManager.Resolve();
+ mapManager.CreateNewMapEntity(MapId.Nullspace);
+
+ var entityManager = IoCManager.Resolve();
+
+ // Spawn the entities
+ human = entityManager.SpawnEntity("HumanMob_Content", MapCoordinates.Nullspace);
+ table = entityManager.SpawnEntity("Table", MapCoordinates.Nullspace);
+
+ // Test for climb components existing
+ // Players and tables should have these in their prototypes.
+ Assert.True(human.TryGetComponent(out climbing!), "Human has no climbing");
+ Assert.True(table.TryGetComponent(out climbable!), "Table has no climbable");
+
+ // Now let's make the player enter a climbing transitioning state.
+ climbing.IsClimbing = true;
+ climbing.TryMoveTo(human.Transform.WorldPosition, table.Transform.WorldPosition);
+ var body = human.GetComponent();
+
+ Assert.True(body.HasController(), "Player has no ClimbController");
+
+ // Force the player out of climb state. It should immediately remove the ClimbController.
+ climbing.IsClimbing = false;
+
+ Assert.True(!body.HasController(), "Player wrongly has a ClimbController");
+
+ });
+
+ await server.WaitIdleAsync();
+ }
+ }
+}
diff --git a/Content.IntegrationTests/Tests/PowerTest.cs b/Content.IntegrationTests/Tests/PowerTest.cs
new file mode 100644
index 0000000000..b53fa69734
--- /dev/null
+++ b/Content.IntegrationTests/Tests/PowerTest.cs
@@ -0,0 +1,150 @@
+using Content.Server.GameObjects.Components.Power;
+using Content.Server.GameObjects.Components.Power.ApcNetComponents;
+using Content.Server.GameObjects.Components.Power.PowerNetComponents;
+using NUnit.Framework;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Map;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using System.Threading.Tasks;
+
+namespace Content.IntegrationTests.Tests
+{
+ [TestFixture]
+ public class PowerTest : ContentIntegrationTest
+ {
+ [Test]
+ public async Task PowerNetTest()
+ {
+ var server = StartServerDummyTicker();
+
+ PowerSupplierComponent supplier = null;
+ PowerConsumerComponent consumer1 = null;
+ PowerConsumerComponent consumer2 = null;
+
+ server.Assert(() =>
+ {
+ var mapMan = IoCManager.Resolve();
+ var entityMan = IoCManager.Resolve();
+ mapMan.CreateMap(new MapId(1));
+ var grid = mapMan.CreateGrid(new MapId(1));
+
+ var generatorEnt = entityMan.SpawnEntity("DebugGenerator", new GridCoordinates(new Vector2(0, 0), grid.Index));
+ var consumerEnt1 = entityMan.SpawnEntity("DebugConsumer", new GridCoordinates(new Vector2(0, 1), grid.Index));
+ var consumerEnt2 = entityMan.SpawnEntity("DebugConsumer", new GridCoordinates(new Vector2(0, 2), grid.Index));
+
+ Assert.That(generatorEnt.TryGetComponent(out supplier));
+ Assert.That(consumerEnt1.TryGetComponent(out consumer1));
+ Assert.That(consumerEnt2.TryGetComponent(out consumer2));
+
+ var supplyRate = 1000; //arbitrary amount of power supply
+
+ supplier.SupplyRate = supplyRate;
+ consumer1.DrawRate = supplyRate / 2; //arbitrary draw less than supply
+ consumer2.DrawRate = supplyRate * 2; //arbitrary draw greater than supply
+
+ consumer1.Priority = Priority.First; //power goes to this consumer first
+ consumer2.Priority = Priority.Last; //any excess power should go to low priority consumer
+ });
+
+ server.RunTicks(1); //let run a tick for PowerNet to process power
+
+ server.Assert(() =>
+ {
+ Assert.That(consumer1.DrawRate, Is.EqualTo(consumer1.ReceivedPower)); //first should be fully powered
+ Assert.That(consumer2.ReceivedPower, Is.EqualTo(supplier.SupplyRate - consumer1.ReceivedPower)); //second should get remaining power
+ });
+
+ await server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task ApcChargingTest()
+ {
+ var server = StartServerDummyTicker();
+
+ BatteryComponent apcBattery = null;
+ PowerSupplierComponent substationSupplier = null;
+
+ server.Assert(() =>
+ {
+ var mapMan = IoCManager.Resolve();
+ var entityMan = IoCManager.Resolve();
+ mapMan.CreateMap(new MapId(1));
+ var grid = mapMan.CreateGrid(new MapId(1));
+
+ var generatorEnt = entityMan.SpawnEntity("DebugGenerator", new GridCoordinates(new Vector2(0, 0), grid.Index));
+ var substationEnt = entityMan.SpawnEntity("DebugSubstation", new GridCoordinates(new Vector2(0, 1), grid.Index));
+ var apcEnt = entityMan.SpawnEntity("DebugApc", new GridCoordinates(new Vector2(0, 2), grid.Index));
+
+ Assert.That(generatorEnt.TryGetComponent(out var generatorSupplier));
+
+ Assert.That(substationEnt.TryGetComponent(out substationSupplier));
+ Assert.That(substationEnt.TryGetComponent(out var substationStorage));
+ Assert.That(substationEnt.TryGetComponent(out var substationDischarger));
+
+ Assert.That(apcEnt.TryGetComponent(out apcBattery));
+ Assert.That(apcEnt.TryGetComponent(out var apcStorage));
+
+ generatorSupplier.SupplyRate = 1000; //arbitrary nonzero amount of power
+ substationStorage.ActiveDrawRate = 1000; //arbitrary nonzero power draw
+ substationDischarger.ActiveSupplyRate = 500; //arbitirary nonzero power supply less than substation storage draw
+ apcStorage.ActiveDrawRate = 500; //arbitrary nonzero power draw
+ apcBattery.MaxCharge = 100; //abbitrary nonzero amount of charge
+ apcBattery.CurrentCharge = 0; //no charge
+ });
+
+ server.RunTicks(5); //let run a few ticks for PowerNets to reevaluate and start charging apc
+
+ server.Assert(() =>
+ {
+ Assert.That(substationSupplier.SupplyRate, Is.Not.EqualTo(0)); //substation should be providing power
+ Assert.That(apcBattery.CurrentCharge, Is.Not.EqualTo(0)); //apc battery should have gained charge
+ });
+
+ await server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task ApcNetTest()
+ {
+ var server = StartServerDummyTicker();
+
+ PowerReceiverComponent receiver = null;
+
+ server.Assert(() =>
+ {
+ var mapMan = IoCManager.Resolve();
+ var entityMan = IoCManager.Resolve();
+ mapMan.CreateMap(new MapId(1));
+ var grid = mapMan.CreateGrid(new MapId(1));
+
+ var apcEnt = entityMan.SpawnEntity("DebugApc", new GridCoordinates(new Vector2(0, 0), grid.Index));
+ var apcExtensionEnt = entityMan.SpawnEntity("ApcExtensionCable", new GridCoordinates(new Vector2(0, 1), grid.Index));
+ var powerReceiverEnt = entityMan.SpawnEntity("DebugPowerReceiver", new GridCoordinates(new Vector2(0, 2), grid.Index));
+
+ Assert.That(apcEnt.TryGetComponent(out var apc));
+ Assert.That(apcExtensionEnt.TryGetComponent(out var provider));
+ Assert.That(powerReceiverEnt.TryGetComponent(out receiver));
+
+ provider.PowerTransferRange = 5; //arbitrary range to reach receiver
+ receiver.PowerReceptionRange = 5; //arbitrary range to reach provider
+
+ apc.Battery.MaxCharge = 10000; //arbitrary nonzero amount of charge
+ apc.Battery.CurrentCharge = apc.Battery.MaxCharge; //fill battery
+
+ receiver.Load = 1; //arbitrary small amount of power
+ });
+
+ server.RunTicks(1); //let run a tick for ApcNet to process power
+
+ server.Assert(() =>
+ {
+ Assert.That(receiver.Powered);
+ });
+
+ await server.WaitIdleAsync();
+ }
+ }
+}
diff --git a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs
index 05c3a6f243..e6c685285c 100644
--- a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs
+++ b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs
@@ -3,8 +3,11 @@ using NUnit.Framework;
using Robust.Server.Interfaces.Maps;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
+using Robust.Shared.Interfaces.Resources;
+using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
+using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests
{
@@ -14,7 +17,7 @@ namespace Content.IntegrationTests.Tests
[Test]
public async Task SaveLoadMultiGridMap()
{
- const string mapPath = @"Maps/Test/TestMap.yml";
+ const string mapPath = @"/Maps/Test/TestMap.yml";
var server = StartServer();
await server.WaitIdleAsync();
@@ -24,6 +27,10 @@ namespace Content.IntegrationTests.Tests
server.Post(() =>
{
+ var dir = new ResourcePath(mapPath).Directory;
+ IoCManager.Resolve()
+ .UserData.CreateDir(dir);
+
var mapId = mapManager.CreateMap(new MapId(5));
{
diff --git a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs
index d462beb279..cb4a8d3ea4 100644
--- a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs
+++ b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs
@@ -38,14 +38,14 @@ namespace Content.IntegrationTests.Tests
string one;
string two;
- var rp1 = new ResourcePath("save load save 1.yml");
+ var rp1 = new ResourcePath("/save load save 1.yml");
using (var stream = userData.Open(rp1, FileMode.Open))
using (var reader = new StreamReader(stream))
{
one = reader.ReadToEnd();
}
- var rp2 = new ResourcePath("save load save 2.yml");
+ var rp2 = new ResourcePath("/save load save 2.yml");
using (var stream = userData.Open(rp2, FileMode.Open))
using (var reader = new StreamReader(stream))
{
@@ -96,7 +96,7 @@ namespace Content.IntegrationTests.Tests
server.Post(() =>
{
- mapLoader.SaveBlueprint(grid.Index, "load save ticks save 2.yml");
+ mapLoader.SaveBlueprint(grid.Index, "/load save ticks save 2.yml");
});
await server.WaitIdleAsync();
@@ -105,13 +105,13 @@ namespace Content.IntegrationTests.Tests
string one;
string two;
- using (var stream = userData.Open(new ResourcePath("load save ticks save 1.yml"), FileMode.Open))
+ using (var stream = userData.Open(new ResourcePath("/load save ticks save 1.yml"), FileMode.Open))
using (var reader = new StreamReader(stream))
{
one = reader.ReadToEnd();
}
- using (var stream = userData.Open(new ResourcePath("load save ticks save 2.yml"), FileMode.Open))
+ using (var stream = userData.Open(new ResourcePath("/load save ticks save 2.yml"), FileMode.Open))
using (var reader = new StreamReader(stream))
{
two = reader.ReadToEnd();
diff --git a/Content.Server.Database/Configuration.cs b/Content.Server.Database/Configuration.cs
index 1541b42ed8..6db9a594d3 100644
--- a/Content.Server.Database/Configuration.cs
+++ b/Content.Server.Database/Configuration.cs
@@ -1,4 +1,5 @@
-using Microsoft.EntityFrameworkCore;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
using Npgsql;
namespace Content.Server.Database
@@ -50,9 +51,10 @@ namespace Content.Server.Database
public class SqliteConfiguration : IDatabaseConfiguration
{
- private readonly string _databaseFilePath;
+ private readonly string? _databaseFilePath;
- public SqliteConfiguration(string databaseFilePath)
+ /// If null, an in-memory database is used.
+ public SqliteConfiguration(string? databaseFilePath)
{
_databaseFilePath = databaseFilePath;
}
@@ -62,7 +64,20 @@ namespace Content.Server.Database
get
{
var optionsBuilder = new DbContextOptionsBuilder();
- optionsBuilder.UseSqlite($"Data Source={_databaseFilePath}");
+ SqliteConnection connection;
+ if (_databaseFilePath != null)
+ {
+ connection = new SqliteConnection($"Data Source={_databaseFilePath}");
+ }
+ else
+ {
+ connection = new SqliteConnection("Data Source=:memory:");
+ // When using an in-memory DB we have to open it manually
+ // so EFCore doesn't open, close and wipe it.
+ connection.Open();
+ }
+
+ optionsBuilder.UseSqlite(connection);
return optionsBuilder.Options;
}
}
diff --git a/Content.Server/AI/Utility/Actions/UtilityAction.cs b/Content.Server/AI/Utility/Actions/UtilityAction.cs
index f8be97abaa..b79fa0955a 100644
--- a/Content.Server/AI/Utility/Actions/UtilityAction.cs
+++ b/Content.Server/AI/Utility/Actions/UtilityAction.cs
@@ -112,19 +112,14 @@ namespace Content.Server.AI.Utility.Actions
UpdateBlackboard(context);
var considerations = GetConsiderations(context);
DebugTools.Assert(considerations.Count > 0);
- // I used the IAUS video although I did have some confusion on how to structure it overall
- // as some of the slides seemed contradictory
- // Ideally we should early-out each action as cheaply as possible if it's not valid
-
- // We also need some way to tell if the action isn't going to
- // have a better score than the current action (if applicable) and early-out that way as well.
-
- // 23:00 Building a better centaur
+ // Overall structure is based on Building a better centaur
+ // Ideally we should early-out each action as cheaply as possible if it's not valid, thus
+ // the finalScore can only go down over time.
+
var finalScore = 1.0f;
var minThreshold = min / Bonus;
context.GetState().SetValue(considerations.Count);
- // See 10:09 for this and the adjustments
foreach (var consideration in considerations)
{
diff --git a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs
index 005a30a9bc..b92d55ce1f 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,11 +6,13 @@ 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;
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.GameObjects;
+using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -63,6 +65,13 @@ namespace Content.Server.AI.Utility.AiLogic
{
SortActions();
}
+
+ if (BehaviorSets.Count == 1 && !EntitySystem.Get().IsAwake(this))
+ {
+ IoCManager.Resolve()
+ .EventBus
+ .RaiseEvent(EventSource.Local, new SleepAiMessage(this, false));
+ }
}
public void RemoveBehaviorSet(Type behaviorSet)
@@ -74,6 +83,13 @@ namespace Content.Server.AI.Utility.AiLogic
BehaviorSets.Remove(behaviorSet);
SortActions();
}
+
+ if (BehaviorSets.Count == 0)
+ {
+ IoCManager.Resolve()
+ .EventBus
+ .RaiseEvent(EventSource.Local, new SleepAiMessage(this, true));
+ }
}
///
@@ -114,38 +130,42 @@ 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;
- }
+ var oldDeadState = _isDead;
+ _isDead = eventArgs.Damageable.CurrentDamageState == DamageState.Dead || eventArgs.Damageable.CurrentDamageState == DamageState.Critical;
- if (speciesComponent.CurrentDamageState is DeadState)
+ if (oldDeadState != _isDead)
{
- _isDead = true;
- }
- else
- {
- _isDead = false;
+ var entityManager = IoCManager.Resolve();
+
+ switch (_isDead)
+ {
+ case true:
+ entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(this, true));
+ break;
+ case false:
+ entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(this, false));
+ break;
+ }
}
}
@@ -180,16 +200,6 @@ namespace Content.Server.AI.Utility.AiLogic
public override void Update(float frameTime)
{
- // If we can't do anything then there's no point thinking
- if (_isDead || BehaviorSets.Count == 0)
- {
- _actionCancellation?.Cancel();
- _blackboard.GetState().SetValue(0.0f);
- CurrentAction?.Shutdown();
- CurrentAction = null;
- return;
- }
-
// If we asked for a new action we don't want to dump the existing one.
if (_actionRequest != null)
{
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/Considerations/Consideration.cs b/Content.Server/AI/Utility/Considerations/Consideration.cs
index 6fc9966607..80860b07c1 100644
--- a/Content.Server/AI/Utility/Considerations/Consideration.cs
+++ b/Content.Server/AI/Utility/Considerations/Consideration.cs
@@ -13,18 +13,27 @@ namespace Content.Server.AI.Utility.Considerations
private float GetAdjustedScore(Blackboard context)
{
var score = GetScore(context);
+ /*
+ * Now using the geometric mean
+ * for n scores you take the n-th root of the scores multiplied
+ * e.g. a, b, c scores you take Math.Pow(a * b * c, 1/3)
+ * To get the ACTUAL geometric mean at any one stage you'd need to divide by the running consideration count
+ * however, the downside to this is it will fluctuate up and down over time.
+ * For our purposes if we go below the minimum threshold we want to cut it off, thus we take a
+ * "running geometric mean" which can only ever go down (and by the final value will equal the actual geometric mean).
+ */
+
+ // Previously we used a makeupvalue method although the geometric mean is less punishing for more considerations
var considerationsCount = context.GetState().GetValue();
- var modificationFactor = 1.0f - 1.0f / considerationsCount;
- var makeUpValue = (1.0f - score) * modificationFactor;
- var adjustedScore = score + makeUpValue * score;
- return MathHelper.Clamp(adjustedScore, 0.0f, 1.0f);
+ var adjustedScore = MathF.Pow(score, 1 / (float) considerationsCount);
+ return FloatMath.Clamp(adjustedScore, 0.0f, 1.0f);
}
[Pure]
private static float BoolCurve(float x)
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
- return x == 1.0f ? 1.0f : 0.0f;
+ return x > 0.0f ? 1.0f : 0.0f;
}
public Func BoolCurve(Blackboard context)
@@ -42,7 +51,7 @@ namespace Content.Server.AI.Utility.Considerations
private static float InverseBoolCurve(float x)
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
- return x == 1.0f ? 0.0f : 1.0f;
+ return x == 0.0f ? 1.0f : 0.0f;
}
public Func InverseBoolCurve(Blackboard context)
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/Inventory/InventoryState.cs b/Content.Server/AI/WorldState/States/Inventory/InventoryState.cs
index 3f570d58bd..8f9701a9ee 100644
--- a/Content.Server/AI/WorldState/States/Inventory/InventoryState.cs
+++ b/Content.Server/AI/WorldState/States/Inventory/InventoryState.cs
@@ -16,6 +16,9 @@ namespace Content.Server.AI.WorldState.States.Inventory
{
foreach (var item in handsComponent.GetAllHeldItems())
{
+ if (item.Owner.Deleted)
+ continue;
+
yield return item.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/Administration/ReadyAll.cs b/Content.Server/Administration/ReadyAll.cs
new file mode 100644
index 0000000000..05d0b48fc8
--- /dev/null
+++ b/Content.Server/Administration/ReadyAll.cs
@@ -0,0 +1,40 @@
+#nullable enable
+using Content.Server.GameTicking;
+using Content.Server.Interfaces.GameTicking;
+using Robust.Server.Interfaces.Console;
+using Robust.Server.Interfaces.Player;
+using Robust.Shared.IoC;
+
+namespace Content.Server.Administration
+{
+ public class ReadyAll : IClientCommand
+ {
+ public string Command => "readyall";
+ public string Description => "Readies up all players in the lobby.";
+ public string Help => $"{Command} | ̣{Command} ";
+ public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
+ {
+ var ready = true;
+
+ if (args.Length > 0)
+ {
+ ready = bool.Parse(args[0]);
+ }
+
+ var gameTicker = IoCManager.Resolve();
+ var playerManager = IoCManager.Resolve();
+
+
+ if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
+ {
+ shell.SendText(player, "This command can only be ran while in the lobby!");
+ return;
+ }
+
+ foreach (var p in playerManager.GetAllPlayers())
+ {
+ gameTicker.ToggleReady(p, ready);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Atmos/AtmosCommands.cs b/Content.Server/Atmos/AtmosCommands.cs
index 29525acaae..c53a0862d8 100644
--- a/Content.Server/Atmos/AtmosCommands.cs
+++ b/Content.Server/Atmos/AtmosCommands.cs
@@ -138,7 +138,7 @@ namespace Content.Server.Atmos
}
}
- public class FillGas : IClientCommand
+ public class FillGas : IClientCommand
{
public string Command => "fillgas";
public string Description => "Adds gas to all tiles in a grid.";
diff --git a/Content.Server/Atmos/AtmosHelpers.cs b/Content.Server/Atmos/AtmosHelpers.cs
index 5561b70639..28c8e165c5 100644
--- a/Content.Server/Atmos/AtmosHelpers.cs
+++ b/Content.Server/Atmos/AtmosHelpers.cs
@@ -20,14 +20,16 @@ namespace Content.Server.Atmos
return coordinates.GetTileAtmosphere()?.Air;
}
- public static bool TryGetTileAtmosphere(this GridCoordinates coordinates, [NotNullWhen(true)] out TileAtmosphere atmosphere)
+ public static bool TryGetTileAtmosphere(this GridCoordinates coordinates, [MaybeNullWhen(false)] out TileAtmosphere atmosphere)
{
- return (atmosphere = coordinates.GetTileAtmosphere()!) != default;
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalse
+ return !Equals(atmosphere = coordinates.GetTileAtmosphere()!, default);
}
- public static bool TryGetTileAir(this GridCoordinates coordinates, [NotNullWhen(true)] out GasMixture air)
+ public static bool TryGetTileAir(this GridCoordinates coordinates, [MaybeNullWhen(false)] out GasMixture air)
{
- return !(air = coordinates.GetTileAir()!).Equals(default);
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalse
+ return !Equals(air = coordinates.GetTileAir()!, default);
}
public static TileAtmosphere? GetTileAtmosphere(this MapIndices indices, GridId gridId)
@@ -43,14 +45,16 @@ namespace Content.Server.Atmos
}
public static bool TryGetTileAtmosphere(this MapIndices indices, GridId gridId,
- [NotNullWhen(true)] out TileAtmosphere atmosphere)
+ [MaybeNullWhen(false)] out TileAtmosphere atmosphere)
{
- return (atmosphere = indices.GetTileAtmosphere(gridId)!) != default;
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalse
+ return !Equals(atmosphere = indices.GetTileAtmosphere(gridId)!, default);
}
- public static bool TryGetTileAir(this MapIndices indices, GridId gridId, [NotNullWhen(true)] out GasMixture air)
+ public static bool TryGetTileAir(this MapIndices indices, GridId gridId, [MaybeNullWhen(false)] out GasMixture air)
{
- return !(air = indices.GetTileAir(gridId)!).Equals(default);
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalse
+ return !Equals(air = indices.GetTileAir(gridId)!, default);
}
}
}
diff --git a/Content.Server/Atmos/GasSprayerComponent.cs b/Content.Server/Atmos/GasSprayerComponent.cs
new file mode 100644
index 0000000000..4b78d413a9
--- /dev/null
+++ b/Content.Server/Atmos/GasSprayerComponent.cs
@@ -0,0 +1,74 @@
+using Content.Server.GameObjects.Components.Chemistry;
+using Content.Server.Interfaces;
+using Content.Shared.Chemistry;
+using Content.Shared.GameObjects.Components;
+using Content.Shared.GameObjects.Components.Pointing;
+using Content.Shared.Interfaces.GameObjects.Components;
+using Robust.Server.GameObjects;
+using Robust.Server.GameObjects.EntitySystems;
+using Robust.Server.Interfaces.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Systems;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
+using Robust.Shared.Serialization;
+
+
+namespace Content.Server.Atmos
+{
+ [RegisterComponent]
+ public class GasSprayerComponent : Component, IAfterInteract
+ {
+#pragma warning disable 649
+ [Dependency] private readonly IServerNotifyManager _notifyManager = default!;
+ [Dependency] private readonly IServerEntityManager _serverEntityManager = default!;
+#pragma warning restore 649
+
+ //TODO: create a function that can create a gas based on a solution mix
+ public override string Name => "GasSprayer";
+
+ private string _spraySound;
+ private string _sprayType;
+ private string _fuelType;
+ private string _fuelName;
+ private int _fuelCost;
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+ serializer.DataField(ref _spraySound, "spraySound", string.Empty);
+ serializer.DataField(ref _sprayType, "sprayType", string.Empty);
+ serializer.DataField(ref _fuelType, "fuelType", string.Empty);
+ serializer.DataField(ref _fuelName, "fuelName", "fuel");
+ serializer.DataField(ref _fuelCost, "fuelCost", 50);
+ }
+
+ public void AfterInteract(AfterInteractEventArgs eventArgs)
+ {
+ if (!Owner.TryGetComponent(out SolutionComponent tank))
+ return;
+
+ if (tank.Solution.GetReagentQuantity(_fuelType) == 0)
+ {
+ _notifyManager.PopupMessage(Owner, eventArgs.User,
+ Loc.GetString("{0:theName} is out of {1}!", Owner, _fuelName));
+ }
+ else
+ {
+ tank.TryRemoveReagent(_fuelType, ReagentUnit.New(_fuelCost));
+
+ var playerPos = eventArgs.User.Transform.GridPosition;
+ var direction = (eventArgs.ClickLocation.Position - playerPos.Position).Normalized;
+ playerPos.Offset(direction/2);
+
+ var spray = _serverEntityManager.SpawnEntity(_sprayType, playerPos);
+ spray.GetComponent()
+ .SetData(ExtinguisherVisuals.Rotation, direction.ToAngle().Degrees);
+ spray.GetComponent().StartMove(direction, 5);
+
+ EntitySystem.Get().PlayFromEntity(_spraySound, Owner);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Atmos/GasVaporComponent.cs b/Content.Server/Atmos/GasVaporComponent.cs
new file mode 100644
index 0000000000..b28d2396a6
--- /dev/null
+++ b/Content.Server/Atmos/GasVaporComponent.cs
@@ -0,0 +1,120 @@
+using Content.Shared.Physics;
+using Content.Server.Atmos.Reactions;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Components;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Map;
+using Robust.Shared.IoC;
+using Robust.Shared.Maths;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+using Content.Server.GameObjects.Components.Atmos;
+using Content.Server.Interfaces;
+using Content.Shared.Atmos;
+
+namespace Content.Server.Atmos
+{
+ [RegisterComponent]
+ class GasVaporComponent : Component, ICollideBehavior, IGasMixtureHolder
+ {
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ public override string Name => "GasVapor";
+
+ [ViewVariables] public GasMixture Air { get; set; }
+
+ private bool _running;
+ private Vector2 _direction;
+ private float _velocity;
+ private float _disspateTimer = 0;
+ private float _dissipationInterval;
+ private Gas _gas;
+ private float _gasVolume;
+ private float _gasTemperature;
+ private float _gasAmount;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ Air = new GasMixture(_gasVolume){Temperature = _gasTemperature};
+ Air.SetMoles(_gas,_gasAmount);
+ }
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+ serializer.DataField(ref _dissipationInterval, "dissipationInterval", 1);
+ serializer.DataField(ref _gas, "gas", Gas.WaterVapor);
+ serializer.DataField(ref _gasVolume, "gasVolume", 200);
+ serializer.DataField(ref _gasTemperature, "gasTemperature", Atmospherics.T20C);
+ serializer.DataField(ref _gasAmount, "gasAmount", 20);
+ }
+
+ public void StartMove(Vector2 dir, float velocity)
+ {
+ _running = true;
+ _direction = dir;
+ _velocity = velocity;
+
+ if (Owner.TryGetComponent(out ICollidableComponent collidable))
+ {
+ var controller = collidable.EnsureController();
+ controller.Move(_direction, _velocity);
+ }
+ }
+
+ public void Update(float frameTime)
+ {
+ if (!_running)
+ return;
+
+ if (Owner.TryGetComponent(out ICollidableComponent collidable))
+ {
+ var worldBounds = collidable.WorldAABB;
+ var mapGrid = _mapManager.GetGrid(Owner.Transform.GridID);
+
+ var tiles = mapGrid.GetTilesIntersecting(worldBounds);
+
+ foreach (var tile in tiles)
+ {
+ var pos = tile.GridIndices.ToGridCoordinates(_mapManager, tile.GridIndex);
+ var atmos = AtmosHelpers.GetTileAtmosphere(pos);
+
+ if (atmos.Air == null)
+ {
+ return;
+ }
+
+ if (atmos.Air.React(this) != ReactionResult.NoReaction)
+ {
+ Owner.Delete();
+ }
+ }
+ }
+
+ _disspateTimer += frameTime;
+ if (_disspateTimer > _dissipationInterval)
+ {
+ Air.SetMoles(_gas, Air.TotalMoles/2 );
+ }
+
+ if (Air.TotalMoles < 1)
+ {
+ Owner.Delete();
+ }
+ }
+
+ void ICollideBehavior.CollideWith(IEntity collidedWith)
+ {
+ // Check for collision with a impassable object (e.g. wall) and stop
+ if (collidedWith.TryGetComponent(out ICollidableComponent collidable) &&
+ (collidable.CollisionLayer & (int) CollisionGroup.Impassable) != 0 &&
+ collidable.Hard &&
+ Owner.TryGetComponent(out ICollidableComponent coll))
+ {
+ var controller = coll.EnsureController();
+ controller.Stop();
+ Owner.Delete();
+ }
+ }
+ }
+}
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 9cf67015ac..6f93d0c9a0 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;
@@ -332,32 +333,40 @@ namespace Content.Server.Atmos
var tile = tiles[i];
tile._tileAtmosInfo.FastDone = true;
if (!(tile._tileAtmosInfo.MoleDelta > 0)) continue;
- var eligibleDirections = new List();
- var amtEligibleAdj = 0;
+ var eligibleDirections = ArrayPool.Shared.Rent(4);
+ var eligibleDirectionCount = 0;
foreach (var direction in Cardinal)
{
if (!tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue;
- // skip anything that isn't part of our current processing block. Original one didn't do this unfortunately, which probably cause some massive lag.
+ // skip anything that isn't part of our current processing block.
if (tile2._tileAtmosInfo.FastDone || tile2._tileAtmosInfo.LastQueueCycle != queueCycle)
continue;
- eligibleDirections.Add(direction);
- amtEligibleAdj++;
+ eligibleDirections[eligibleDirectionCount++] = direction;
}
- if (amtEligibleAdj <= 0)
+ if (eligibleDirectionCount <= 0)
continue; // Oof we've painted ourselves into a corner. Bad luck. Next part will handle this.
- var molesToMove = tile._tileAtmosInfo.MoleDelta / amtEligibleAdj;
+ var molesToMove = tile._tileAtmosInfo.MoleDelta / eligibleDirectionCount;
foreach (var direction in Cardinal)
{
- if (eligibleDirections.Contains(direction) ||
- !tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue;
+ var hasDirection = false;
+ for (var j = 0; j < eligibleDirectionCount; j++)
+ {
+ if (eligibleDirections[j] != direction) continue;
+ hasDirection = true;
+ break;
+ }
+
+ if (hasDirection || !tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue;
tile.AdjustEqMovement(direction, molesToMove);
tile._tileAtmosInfo.MoleDelta -= molesToMove;
tile2._tileAtmosInfo.MoleDelta += molesToMove;
}
+
+ ArrayPool.Shared.Return(eligibleDirections);
}
giverTilesLength = 0;
@@ -446,7 +455,7 @@ namespace Content.Server.Atmos
}
}
- ArrayPool.Shared.Return(queue, true);
+ ArrayPool.Shared.Return(queue);
}
else
{
@@ -516,7 +525,7 @@ namespace Content.Server.Atmos
}
}
- ArrayPool.Shared.Return(queue, true);
+ ArrayPool.Shared.Return(queue);
}
for (var i = 0; i < tileCount; i++)
@@ -537,9 +546,9 @@ namespace Content.Server.Atmos
}
}
- ArrayPool.Shared.Return(tiles, true);
- ArrayPool.Shared.Return(giverTiles, true);
- ArrayPool.Shared.Return(takerTiles, true);
+ ArrayPool.Shared.Return(tiles);
+ ArrayPool.Shared.Return(giverTiles);
+ ArrayPool.Shared.Return(takerTiles);
}
}
@@ -737,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)
@@ -925,16 +934,22 @@ namespace Content.Server.Atmos
public void ExplosivelyDepressurize(int cycleNum)
{
if (Air == null) return;
+
+ const int limit = Atmospherics.ZumosTileLimit;
+
var totalGasesRemoved = 0f;
var queueCycle = ++_gridAtmosphereComponent.EqualizationQueueCycleControl;
- var tiles = new List();
- var spaceTiles = new List();
- tiles.Add(this);
+ var tiles = ArrayPool.Shared.Rent(limit);
+ var spaceTiles = ArrayPool.Shared.Rent(limit);
+
+ var tileCount = 0;
+ var spaceTileCount = 0;
+
+ tiles[tileCount++] = this;
ResetTileAtmosInfo();
_tileAtmosInfo.LastQueueCycle = queueCycle;
- var tileCount = 1;
for (var i = 0; i < tileCount; i++)
{
var tile = tiles[i];
@@ -942,40 +957,44 @@ namespace Content.Server.Atmos
tile._tileAtmosInfo.CurrentTransferDirection = Direction.Invalid;
if (tile.Air.Immutable)
{
- spaceTiles.Add(tile);
+ spaceTiles[spaceTileCount++] = tile;
tile.PressureSpecificTarget = tile;
}
else
{
- if (i > Atmospherics.ZumosTileLimit) continue;
foreach (var direction in Cardinal)
{
if (!tile._adjacentTiles.TryGetValue(direction, out var tile2)) continue;
- if (tile2?.Air == null) continue;
+ if (tile2.Air == null) continue;
if (tile2._tileAtmosInfo.LastQueueCycle == queueCycle) continue;
+
tile.ConsiderFirelocks(tile2);
- if (tile._adjacentTiles[direction]?.Air != null)
- {
- tile2.ResetTileAtmosInfo();
- tile2._tileAtmosInfo.LastQueueCycle = queueCycle;
- tiles.Add(tile2);
- tileCount++;
- }
+
+ // The firelocks might have closed on us.
+ if (tile._adjacentTiles[direction]?.Air == null) continue;
+ tile2.ResetTileAtmosInfo();
+ tile2._tileAtmosInfo.LastQueueCycle = queueCycle;
+ tiles[tileCount++] = tile2;
}
}
+
+ if (tileCount >= limit || spaceTileCount >= limit)
+ break;
}
var queueCycleSlow = ++_gridAtmosphereComponent.EqualizationQueueCycleControl;
- var progressionOrder = new List();
- foreach (var tile in spaceTiles)
+ var progressionOrder = ArrayPool.Shared.Rent(limit * 2);
+ var progressionCount = 0;
+
+ for (var i = 0; i < spaceTileCount; i++)
{
- progressionOrder.Add(tile);
+ var tile = spaceTiles[i];
+ progressionOrder[progressionCount++] = tile;
tile._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow;
tile._tileAtmosInfo.CurrentTransferDirection = Direction.Invalid;
}
- var progressionCount = progressionOrder.Count;
- for (int i = 0; i < progressionCount; i++)
+ for (var i = 0; i < progressionCount; i++)
{
var tile = progressionOrder[i];
foreach (var direction in Cardinal)
@@ -988,8 +1007,7 @@ namespace Content.Server.Atmos
tile2._tileAtmosInfo.CurrentTransferAmount = 0;
tile2.PressureSpecificTarget = tile.PressureSpecificTarget;
tile2._tileAtmosInfo.LastSlowQueueCycle = queueCycleSlow;
- progressionOrder.Add(tile2);
- progressionCount++;
+ progressionOrder[progressionCount++] = tile2;
}
}
@@ -1017,6 +1035,10 @@ namespace Content.Server.Atmos
tile.UpdateVisuals();
tile.HandleDecompressionFloorRip(sum);
}
+
+ ArrayPool.Shared.Return(tiles);
+ ArrayPool.Shared.Return(spaceTiles);
+ ArrayPool.Shared.Return(progressionOrder);
}
private void HandleDecompressionFloorRip(float sum)
@@ -1029,7 +1051,6 @@ namespace Content.Server.Atmos
private void ConsiderFirelocks(TileAtmosphere other)
{
// TODO ATMOS firelocks!
- //throw new NotImplementedException();
}
private void React()
diff --git a/Content.Server/Body/BodyCommands.cs b/Content.Server/Body/BodyCommands.cs
new file mode 100644
index 0000000000..9809a5845f
--- /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..2229357bfc
--- /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..bc2591c387
--- /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..ce6a2bcf43
--- /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 9d3ff7c995..c5f194d429 100644
--- a/Content.Server/Chat/ChatCommands.cs
+++ b/Content.Server/Chat/ChatCommands.cs
@@ -1,10 +1,11 @@
-using System.Linq;
-using Content.Server.GameObjects.Components.Damage;
+using System;
+using System.Linq;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Observer;
using Content.Server.Interfaces.Chat;
using Content.Server.Interfaces.GameObjects;
+using Content.Server.Observer;
using Content.Server.Players;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.Interfaces.Console;
@@ -13,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
{
@@ -30,9 +32,11 @@ namespace Content.Server.Chat
if (args.Length < 1)
return;
- var chat = IoCManager.Resolve();
+ var message = string.Join(" ", args).Trim();
+ if (string.IsNullOrEmpty(message))
+ return;
- var message = string.Join(" ", args);
+ var chat = IoCManager.Resolve();
if (player.AttachedEntity.HasComponent())
chat.SendDeadChat(player, message);
@@ -59,9 +63,11 @@ namespace Content.Server.Chat
if (args.Length < 1)
return;
- var chat = IoCManager.Resolve();
+ var action = string.Join(" ", args).Trim();
+ if (string.IsNullOrEmpty(action))
+ return;
- var action = string.Join(" ", args);
+ var chat = IoCManager.Resolve();
var mindComponent = player.ContentData().Mind;
chat.EntityMe(mindComponent.OwnedEntity, action);
@@ -76,8 +82,15 @@ namespace Content.Server.Chat
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
+ if (args.Length < 1)
+ return;
+
+ var message = string.Join(" ", args).Trim();
+ if (string.IsNullOrEmpty(message))
+ return;
+
var chat = IoCManager.Resolve();
- chat.SendOOC(player, string.Join(" ", args));
+ chat.SendOOC(player, message);
}
}
@@ -89,8 +102,15 @@ namespace Content.Server.Chat
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
+ if (args.Length < 1)
+ return;
+
+ var message = string.Join(" ", args).Trim();
+ if (string.IsNullOrEmpty(message))
+ return;
+
var chat = IoCManager.Resolve();
- chat.SendAdminChat(player, string.Join(" ", args));
+ chat.SendAdminChat(player, message);
}
}
@@ -105,24 +125,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);
}
}
@@ -133,7 +153,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?
@@ -167,7 +187,11 @@ 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};
+ ghost.Execute(shell, player, Array.Empty());
}
}
}
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/Content.Server.csproj b/Content.Server/Content.Server.csproj
index 0c9be50c54..9eae91acb0 100644
--- a/Content.Server/Content.Server.csproj
+++ b/Content.Server/Content.Server.csproj
@@ -9,6 +9,7 @@
..\bin\Content.Server\
true
Exe
+ 1998
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/Access/AccessReaderComponent.cs b/Content.Server/GameObjects/Components/Access/AccessReaderComponent.cs
index 4ebfcd20bd..c861074d34 100644
--- a/Content.Server/GameObjects/Components/Access/AccessReaderComponent.cs
+++ b/Content.Server/GameObjects/Components/Access/AccessReaderComponent.cs
@@ -71,16 +71,16 @@ namespace Content.Server.GameObjects.Components.Access
public static ICollection FindAccessTags(IEntity entity)
{
- if (entity.TryGetComponent(out IAccess accessComponent))
+ if (entity.TryGetComponent(out IAccess? accessComponent))
{
return accessComponent.Tags;
}
- if (entity.TryGetComponent(out IHandsComponent handsComponent))
+ if (entity.TryGetComponent(out IHandsComponent? handsComponent))
{
var activeHandEntity = handsComponent.GetActiveHand?.Owner;
if (activeHandEntity != null &&
- activeHandEntity.TryGetComponent(out IAccess handAccessComponent))
+ activeHandEntity.TryGetComponent(out IAccess? handAccessComponent))
{
return handAccessComponent.Tags;
}
@@ -90,11 +90,11 @@ namespace Content.Server.GameObjects.Components.Access
return Array.Empty();
}
- if (entity.TryGetComponent(out InventoryComponent inventoryComponent))
+ if (entity.TryGetComponent(out InventoryComponent? inventoryComponent))
{
if (inventoryComponent.HasSlot(EquipmentSlotDefines.Slots.IDCARD) &&
inventoryComponent.TryGetSlotItem(EquipmentSlotDefines.Slots.IDCARD, out ItemComponent item) &&
- item.Owner.TryGetComponent(out IAccess idAccessComponent)
+ item.Owner.TryGetComponent(out IAccess? idAccessComponent)
)
{
return idAccessComponent.Tags;
diff --git a/Content.Server/GameObjects/Components/AnchorableComponent.cs b/Content.Server/GameObjects/Components/AnchorableComponent.cs
index 6ad088bdbe..224ce59886 100644
--- a/Content.Server/GameObjects/Components/AnchorableComponent.cs
+++ b/Content.Server/GameObjects/Components/AnchorableComponent.cs
@@ -1,5 +1,6 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.Interfaces.GameObjects.Components;
@@ -14,17 +15,18 @@ namespace Content.Server.GameObjects.Components
{
public override string Name => "Anchorable";
+ int IInteractUsing.Priority => 1;
+
///
/// Checks if a tool can change the anchored status.
///
/// The user doing the action
/// The tool being used, can be null if forcing it
- /// The physics component of the owning entity
/// Whether or not to check if the tool is valid
/// true if it is valid, false otherwise
- private bool Valid(IEntity user, IEntity? utilizing, [MaybeNullWhen(false)] out ICollidableComponent collidable, bool force = false)
+ private async Task Valid(IEntity user, IEntity? utilizing, [MaybeNullWhen(false)] bool force = false)
{
- if (!Owner.TryGetComponent(out collidable))
+ if (!Owner.HasComponent())
{
return false;
}
@@ -32,8 +34,8 @@ namespace Content.Server.GameObjects.Components
if (!force)
{
if (utilizing == null ||
- !utilizing.TryGetComponent(out ToolComponent tool) ||
- !tool.UseTool(user, Owner, ToolQuality.Anchoring))
+ !utilizing.TryGetComponent(out ToolComponent? tool) ||
+ !(await tool.UseTool(user, Owner, 0.5f, ToolQuality.Anchoring)))
{
return false;
}
@@ -49,13 +51,14 @@ namespace Content.Server.GameObjects.Components
/// The tool being used, if any
/// Whether or not to ignore valid tool checks
/// true if anchored, false otherwise
- public bool TryAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
+ public async Task TryAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
{
- if (!Valid(user, utilizing, out var physics, force))
+ if (!(await Valid(user, utilizing, force)))
{
return false;
}
+ var physics = Owner.GetComponent();
physics.Anchored = true;
return true;
@@ -68,13 +71,14 @@ namespace Content.Server.GameObjects.Components
/// The tool being used, if any
/// Whether or not to ignore valid tool checks
/// true if unanchored, false otherwise
- public bool TryUnAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
+ public async Task TryUnAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
{
- if (!Valid(user, utilizing, out var physics, force))
+ if (!(await Valid(user, utilizing, force)))
{
return false;
}
+ var physics = Owner.GetComponent();
physics.Anchored = false;
return true;
@@ -87,16 +91,16 @@ namespace Content.Server.GameObjects.Components
/// The tool being used, if any
/// Whether or not to ignore valid tool checks
/// true if toggled, false otherwise
- private bool TryToggleAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
+ private async Task TryToggleAnchor(IEntity user, IEntity? utilizing = null, bool force = false)
{
- if (!Owner.TryGetComponent(out ICollidableComponent collidable))
+ if (!Owner.TryGetComponent(out ICollidableComponent? collidable))
{
return false;
}
return collidable.Anchored ?
- TryUnAnchor(user, utilizing, force) :
- TryAnchor(user, utilizing, force);
+ await TryUnAnchor(user, utilizing, force) :
+ await TryAnchor(user, utilizing, force);
}
public override void Initialize()
@@ -105,9 +109,9 @@ namespace Content.Server.GameObjects.Components
Owner.EnsureComponent();
}
- bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
+ async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
- return TryToggleAnchor(eventArgs.User, eventArgs.Using);
+ return await TryToggleAnchor(eventArgs.User, eventArgs.Using);
}
}
}
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/GasAnalyzerComponent.cs b/Content.Server/GameObjects/Components/Atmos/GasAnalyzerComponent.cs
index 066bc0940a..2a6a2be135 100644
--- a/Content.Server/GameObjects/Components/Atmos/GasAnalyzerComponent.cs
+++ b/Content.Server/GameObjects/Components/Atmos/GasAnalyzerComponent.cs
@@ -116,7 +116,7 @@ namespace Content.Server.GameObjects.Components.Atmos
{
_pressureDanger = GasAnalyzerDanger.Nominal;
}
-
+
Dirty();
_timeSinceSync = 0f;
}
@@ -131,11 +131,11 @@ namespace Content.Server.GameObjects.Components.Atmos
if (session.AttachedEntity == null)
return;
- if (!session.AttachedEntity.TryGetComponent(out IHandsComponent handsComponent))
+ if (!session.AttachedEntity.TryGetComponent(out IHandsComponent? handsComponent))
return;
var activeHandEntity = handsComponent?.GetActiveHand?.Owner;
- if (activeHandEntity == null || !activeHandEntity.TryGetComponent(out GasAnalyzerComponent gasAnalyzer))
+ if (activeHandEntity == null || !activeHandEntity.TryGetComponent(out GasAnalyzerComponent? gasAnalyzer))
{
return;
}
@@ -147,7 +147,7 @@ namespace Content.Server.GameObjects.Components.Atmos
// Check if position is out of range => don't update
if (!_position.Value.InRange(_mapManager, pos, SharedInteractionSystem.InteractionRange))
return;
-
+
pos = _position.Value;
}
@@ -195,7 +195,7 @@ namespace Content.Server.GameObjects.Components.Atmos
return;
}
- if (!player.TryGetComponent(out IHandsComponent handsComponent))
+ if (!player.TryGetComponent(out IHandsComponent? handsComponent))
{
_notifyManager.PopupMessage(Owner.Transform.GridPosition, player,
Loc.GetString("You have no hands."));
@@ -203,7 +203,7 @@ namespace Content.Server.GameObjects.Components.Atmos
}
var activeHandEntity = handsComponent.GetActiveHand?.Owner;
- if (activeHandEntity == null || !activeHandEntity.TryGetComponent(out GasAnalyzerComponent gasAnalyzer))
+ if (activeHandEntity == null || !activeHandEntity.TryGetComponent(out GasAnalyzerComponent? gasAnalyzer))
{
_notifyManager.PopupMessage(serverMsg.Session.AttachedEntity,
serverMsg.Session.AttachedEntity,
@@ -225,7 +225,7 @@ namespace Content.Server.GameObjects.Components.Atmos
return;
}
- if (eventArgs.User.TryGetComponent(out IActorComponent actor))
+ if (eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
OpenInterface(actor.playerSession, eventArgs.ClickLocation);
//TODO: show other sprite when ui open?
@@ -236,7 +236,7 @@ namespace Content.Server.GameObjects.Components.Atmos
void IDropped.Dropped(DroppedEventArgs eventArgs)
{
- if (eventArgs.User.TryGetComponent(out IActorComponent actor))
+ if (eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
CloseInterface(actor.playerSession);
//TODO: if other sprite is shown, change again
@@ -245,7 +245,7 @@ namespace Content.Server.GameObjects.Components.Atmos
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
- if (eventArgs.User.TryGetComponent(out IActorComponent actor))
+ if (eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
OpenInterface(actor.playerSession);
//TODO: show other sprite when ui open?
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..5c14e4da0c
--- /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
///