diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index cce2333942..fd0b2a6d01 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -63,9 +63,6 @@ namespace Content.Client factory.Register(); factory.Register(); factory.Register(); - - factory.Register(); - factory.Register(); factory.Register(); factory.Register(); diff --git a/Content.Client/GameObjects/Components/Body/BodyComponent.cs b/Content.Client/GameObjects/Components/Body/BodyComponent.cs new file mode 100644 index 0000000000..96d30e18bf --- /dev/null +++ b/Content.Client/GameObjects/Components/Body/BodyComponent.cs @@ -0,0 +1,30 @@ +#nullable enable +using Content.Client.GameObjects.Components.Disposal; +using Content.Client.GameObjects.Components.MedicalScanner; +using Content.Client.Interfaces.GameObjects.Components.Interaction; +using Content.Shared.GameObjects.Components.Body; +using Robust.Shared.GameObjects; + +namespace Content.Client.GameObjects.Components.Body +{ + [RegisterComponent] + [ComponentReference(typeof(IBody))] + public class BodyComponent : SharedBodyComponent, IClientDraggable + { + public bool ClientCanDropOn(CanDropEventArgs eventArgs) + { + if (eventArgs.Target.HasComponent() || + eventArgs.Target.HasComponent()) + { + return true; + } + + return false; + } + + public bool ClientCanDrag(CanDragEventArgs eventArgs) + { + return true; + } + } +} diff --git a/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs b/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs deleted file mode 100644 index 8466a5e3d3..0000000000 --- a/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs +++ /dev/null @@ -1,76 +0,0 @@ -#nullable enable -using Content.Client.GameObjects.Components.Disposal; -using Content.Client.GameObjects.Components.MedicalScanner; -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(ISharedBodyManagerComponent))] - public class BodyManagerComponent : SharedBodyManagerComponent, IClientDraggable - { - [Dependency] private readonly IEntityManager _entityManager = default!; - - public bool ClientCanDropOn(CanDropEventArgs eventArgs) - { - if ( - eventArgs.Target.HasComponent()|| - eventArgs.Target.HasComponent()) - { - return true; - } - return false; - } - - 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/GameObjects/Components/Body/Mechanism/HeartBehaviorComponent.cs b/Content.Client/GameObjects/Components/Body/Mechanism/HeartBehaviorComponent.cs new file mode 100644 index 0000000000..0131117d81 --- /dev/null +++ b/Content.Client/GameObjects/Components/Body/Mechanism/HeartBehaviorComponent.cs @@ -0,0 +1,12 @@ +using Content.Shared.GameObjects.Components.Body.Behavior; +using Robust.Shared.GameObjects; + +namespace Content.Client.GameObjects.Components.Body.Mechanism +{ + [RegisterComponent] + [ComponentReference(typeof(SharedHeartBehaviorComponent))] + public class HeartBehaviorComponent : SharedHeartBehaviorComponent + { + public override void Update(float frameTime) { } + } +} diff --git a/Content.Client/GameObjects/Components/Body/Mechanism/MechanismComponent.cs b/Content.Client/GameObjects/Components/Body/Mechanism/MechanismComponent.cs new file mode 100644 index 0000000000..0c80dbccf1 --- /dev/null +++ b/Content.Client/GameObjects/Components/Body/Mechanism/MechanismComponent.cs @@ -0,0 +1,34 @@ +#nullable enable +using Content.Shared.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Part; +using Robust.Client.Interfaces.GameObjects.Components; +using Robust.Shared.GameObjects; + +namespace Content.Client.GameObjects.Components.Body.Mechanism +{ + [RegisterComponent] + [ComponentReference(typeof(SharedMechanismComponent))] + [ComponentReference(typeof(IMechanism))] + public class MechanismComponent : SharedMechanismComponent + { + protected override void OnPartAdd(IBodyPart? old, IBodyPart current) + { + base.OnPartAdd(old, current); + + if (Owner.TryGetComponent(out ISpriteComponent? sprite)) + { + sprite.Visible = false; + } + } + + protected override void OnPartRemove(IBodyPart old) + { + base.OnPartRemove(old); + + if (Owner.TryGetComponent(out ISpriteComponent? sprite)) + { + sprite.Visible = true; + } + } + } +} diff --git a/Content.Client/GameObjects/Components/Body/Part/BodyPartComponent.cs b/Content.Client/GameObjects/Components/Body/Part/BodyPartComponent.cs new file mode 100644 index 0000000000..4dc2156248 --- /dev/null +++ b/Content.Client/GameObjects/Components/Body/Part/BodyPartComponent.cs @@ -0,0 +1,13 @@ +#nullable enable +using Content.Shared.GameObjects.Components.Body.Part; +using Robust.Shared.GameObjects; + +namespace Content.Client.GameObjects.Components.Body.Part +{ + [RegisterComponent] + [ComponentReference(typeof(SharedBodyPartComponent))] + [ComponentReference(typeof(IBodyPart))] + public class BodyPartComponent : SharedBodyPartComponent + { + } +} diff --git a/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerBoundUserInterface.cs b/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerBoundUserInterface.cs index 9ebfaa881e..bee5094d28 100644 --- a/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerBoundUserInterface.cs +++ b/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerBoundUserInterface.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; -using Content.Shared.Body.Scanner; +using System; +using Content.Shared.GameObjects.Components.Body.Scanner; using JetBrains.Annotations; using Robust.Client.GameObjects.Components.UserInterface; using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.ViewVariables; namespace Content.Client.GameObjects.Components.Body.Scanner @@ -14,10 +15,7 @@ namespace Content.Client.GameObjects.Components.Body.Scanner private BodyScannerDisplay _display; [ViewVariables] - private BodyScannerTemplateData _template; - - [ViewVariables] - private Dictionary _parts; + private IEntity _entity; public BodyScannerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) { } @@ -33,15 +31,17 @@ namespace Content.Client.GameObjects.Components.Body.Scanner { base.UpdateState(state); - if (!(state is BodyScannerInterfaceState scannerState)) + if (!(state is BodyScannerUIState scannerState)) { return; } - _template = scannerState.Template; - _parts = scannerState.Parts; + if (!Owner.Owner.EntityManager.TryGetEntity(scannerState.Uid, out _entity)) + { + throw new ArgumentException($"Received an invalid entity with id {scannerState.Uid} for body scanner with id {Owner.Owner.Uid} at {Owner.Owner.Transform.MapPosition}"); + } - _display.UpdateDisplay(_template, _parts); + _display.UpdateDisplay(_entity); } protected override void Dispose(bool disposing) @@ -51,8 +51,6 @@ namespace Content.Client.GameObjects.Components.Body.Scanner 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 index ce75175add..10e496a623 100644 --- a/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerDisplay.cs +++ b/Content.Client/GameObjects/Components/Body/Scanner/BodyScannerDisplay.cs @@ -1,7 +1,12 @@ -using System.Collections.Generic; -using Content.Shared.Body.Scanner; +#nullable enable +using System.Linq; +using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.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; @@ -11,13 +16,10 @@ namespace Content.Client.GameObjects.Components.Body.Scanner { public sealed class BodyScannerDisplay : SS14Window { - private BodyScannerTemplateData _template; + private IEntity? _currentEntity; + private IBodyPart? _currentBodyPart; - private Dictionary _parts; - - private List _slots; - - private BodyScannerBodyPartData _currentBodyPart; + private IBody? CurrentBody => _currentEntity?.GetBody(); public BodyScannerDisplay(BodyScannerBoundUserInterface owner) { @@ -102,51 +104,70 @@ namespace Content.Client.GameObjects.Components.Body.Scanner private RichTextLabel MechanismInfoLabel { get; } - public void UpdateDisplay(BodyScannerTemplateData template, Dictionary parts) + public void UpdateDisplay(IEntity entity) { - _template = template; - _parts = parts; - _slots = new List(); + _currentEntity = entity; 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); + var body = CurrentBody; + if (body == null) + { + return; + } + + foreach (var slotName in body.Parts.Keys) + { BodyPartList.AddItem(Loc.GetString(slotName)); } } public void BodyPartOnItemSelected(ItemListSelectedEventArgs args) { - if (_parts.TryGetValue(_slots[args.ItemIndex], out _currentBodyPart)) { - UpdateBodyPartBox(_currentBodyPart, _slots[args.ItemIndex]); + var body = CurrentBody; + + if (body == null) + { + return; + } + + var slot = body.SlotAt(args.ItemIndex).Key; + _currentBodyPart = body.PartAt(args.ItemIndex).Value; + + if (body.Parts.TryGetValue(slot, out var part)) + { + UpdateBodyPartBox(part, slot); } } - private void UpdateBodyPartBox(BodyScannerBodyPartData part, string slotName) + private void UpdateBodyPartBox(IBodyPart part, string slotName) { - BodyPartLabel.Text = $"{Loc.GetString(slotName)}: {Loc.GetString(part.Name)}"; - BodyPartHealth.Text = $"{part.CurrentDurability}/{part.MaxDurability}"; + BodyPartLabel.Text = $"{Loc.GetString(slotName)}: {Loc.GetString(part.Owner.Name)}"; + + // TODO BODY Make dead not be the destroy threshold for a body part + if (part.Owner.TryGetComponent(out IDamageableComponent? damageable) && + damageable.TryHealth(DamageState.Critical, out var health)) + { + BodyPartHealth.Text = $"{health.current} / {health.max}"; + } MechanismList.Clear(); - foreach (var mechanism in part.Mechanisms) { + + foreach (var mechanism in part.Mechanisms) + { MechanismList.AddItem(mechanism.Name); } } + // TODO BODY Guaranteed this is going to crash when a part's mechanisms change. This part is left as an exercise for the reader. public void MechanismOnItemSelected(ItemListSelectedEventArgs args) { - UpdateMechanismBox(_currentBodyPart.Mechanisms[args.ItemIndex]); + UpdateMechanismBox(_currentBodyPart?.Mechanisms.ElementAt(args.ItemIndex)); } - private void UpdateMechanismBox(BodyScannerMechanismData mechanism) + private void UpdateMechanismBox(IMechanism? mechanism) { - // TODO: Improve UI + // TODO BODY Improve UI if (mechanism == null) { MechanismInfoLabel.SetMessage(""); diff --git a/Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryBoundUserInterface.cs b/Content.Client/GameObjects/Components/Body/Surgery/SurgeryBoundUserInterface.cs similarity index 85% rename from Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryBoundUserInterface.cs rename to Content.Client/GameObjects/Components/Body/Surgery/SurgeryBoundUserInterface.cs index e44f368221..7ff39ec0f7 100644 --- a/Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryBoundUserInterface.cs +++ b/Content.Client/GameObjects/Components/Body/Surgery/SurgeryBoundUserInterface.cs @@ -1,27 +1,27 @@ #nullable enable -using Content.Shared.Body.Surgery; +using Content.Shared.GameObjects.Components.Body.Surgery; using JetBrains.Annotations; using Robust.Client.GameObjects.Components.UserInterface; using Robust.Shared.GameObjects.Components.UserInterface; namespace Content.Client.GameObjects.Components.Body.Surgery { - // TODO : Make window close if target or surgery tool gets too far away from user. + // TODO BODY 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. /// [UsedImplicitly] - public class GenericSurgeryBoundUserInterface : BoundUserInterface + public class SurgeryBoundUserInterface : BoundUserInterface { - private GenericSurgeryWindow? _window; + private SurgeryWindow? _window; - public GenericSurgeryBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) { } + public SurgeryBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) { } protected override void Open() { - _window = new GenericSurgeryWindow(); + _window = new SurgeryWindow(); _window.OpenCentered(); _window.OnClose += Close; diff --git a/Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryWindow.cs b/Content.Client/GameObjects/Components/Body/Surgery/SurgeryWindow.cs similarity index 97% rename from Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryWindow.cs rename to Content.Client/GameObjects/Components/Body/Surgery/SurgeryWindow.cs index d432c3342a..ededa9338f 100644 --- a/Content.Client/GameObjects/Components/Body/Surgery/GenericSurgeryWindow.cs +++ b/Content.Client/GameObjects/Components/Body/Surgery/SurgeryWindow.cs @@ -8,7 +8,7 @@ using Robust.Shared.Maths; namespace Content.Client.GameObjects.Components.Body.Surgery { - public class GenericSurgeryWindow : SS14Window + public class SurgeryWindow : SS14Window { public delegate void OptionSelectedCallback(int selectedOptionData); @@ -17,7 +17,7 @@ namespace Content.Client.GameObjects.Components.Body.Surgery protected override Vector2? CustomSize => (300, 400); - public GenericSurgeryWindow() + public SurgeryWindow() { Title = Loc.GetString("Select surgery target..."); RectClipContent = true; diff --git a/Content.Client/GameObjects/Components/Chemistry/SolutionContainerComponent.cs b/Content.Client/GameObjects/Components/Chemistry/SolutionContainerComponent.cs new file mode 100644 index 0000000000..e52a03c747 --- /dev/null +++ b/Content.Client/GameObjects/Components/Chemistry/SolutionContainerComponent.cs @@ -0,0 +1,29 @@ +using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Chemistry; +using Robust.Shared.GameObjects; + +namespace Content.Client.GameObjects.Components.Chemistry +{ + [RegisterComponent] + [ComponentReference(typeof(SharedSolutionContainerComponent))] + public class SolutionContainerComponent : SharedSolutionContainerComponent + { + public override bool CanAddSolution(Solution solution) + { + // TODO CLIENT + return false; + } + + public override bool TryAddSolution(Solution solution, bool skipReactionCheck = false, bool skipColor = false) + { + // TODO CLIENT + return false; + } + + public override bool TryRemoveReagent(string reagentId, ReagentUnit quantity) + { + // TODO CLIENT + return false; + } + } +} diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs index 62bae7d387..e48f4cc14b 100644 --- a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs +++ b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs @@ -3,11 +3,9 @@ using Robust.Shared.GameObjects; namespace Content.Client.GameObjects.Components.MedicalScanner { - [RegisterComponent] [ComponentReference(typeof(SharedMedicalScannerComponent))] public class MedicalScannerComponent : SharedMedicalScannerComponent { - } } diff --git a/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs b/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs index 7eed8f3064..46b7d66e53 100644 --- a/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs +++ b/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs @@ -1,4 +1,6 @@ using Content.Client.GameObjects.Components.ActionBlocking; +using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.Preferences; using Content.Shared.Preferences.Appearance; @@ -8,7 +10,7 @@ using Robust.Shared.GameObjects; namespace Content.Client.GameObjects.Components.Mobs { [RegisterComponent] - public sealed class HumanoidAppearanceComponent : SharedHumanoidAppearanceComponent + public sealed class HumanoidAppearanceComponent : SharedHumanoidAppearanceComponent, IBodyPartAdded, IBodyPartRemoved { public override HumanoidCharacterAppearance Appearance { @@ -39,8 +41,24 @@ namespace Content.Client.GameObjects.Components.Mobs private void UpdateLooks() { - if (Appearance is null) return; - var sprite = Owner.GetComponent(); + if (Appearance is null || + !Owner.TryGetComponent(out SpriteComponent sprite)) + { + return; + } + + if (Owner.TryGetBody(out var body)) + { + foreach (var part in body.Parts.Values) + { + if (!part.Owner.TryGetComponent(out SpriteComponent partSprite)) + { + continue; + } + + partSprite.Color = Appearance.SkinColor; + } + } sprite.LayerSetColor(HumanoidVisualLayers.Hair, Appearance.HairColor); sprite.LayerSetColor(HumanoidVisualLayers.FacialHair, Appearance.FacialHairColor); @@ -71,5 +89,51 @@ namespace Content.Client.GameObjects.Components.Mobs sprite.LayerSetState(HumanoidVisualLayers.FacialHair, HairStyles.FacialHairStylesMap[facialHairStyle]); } + + public void BodyPartAdded(BodyPartAddedEventArgs args) + { + if (!Owner.TryGetComponent(out SpriteComponent sprite)) + { + return; + } + + if (!args.Part.Owner.TryGetComponent(out SpriteComponent partSprite)) + { + return; + } + + var layer = args.Part.ToHumanoidLayer(); + + if (layer == null) + { + return; + } + + // TODO BODY Layer color, sprite and state + sprite.LayerSetVisible(layer, true); + } + + public void BodyPartRemoved(BodyPartRemovedEventArgs args) + { + if (!Owner.TryGetComponent(out SpriteComponent sprite)) + { + return; + } + + if (!args.Part.Owner.TryGetComponent(out SpriteComponent partSprite)) + { + return; + } + + var layer = args.Part.ToHumanoidLayer(); + + if (layer == null) + { + return; + } + + // TODO BODY Layer color, sprite and state + sprite.LayerSetVisible(layer, false); + } } } diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index 4a02dd15fb..24a624d66a 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -182,6 +182,7 @@ "BreakableConstruction", "GasCanister", "GasCanisterPort", + "Lung", }; } } diff --git a/Content.IntegrationTests/Tests/Body/LegTest.cs b/Content.IntegrationTests/Tests/Body/LegTest.cs new file mode 100644 index 0000000000..323e97507d --- /dev/null +++ b/Content.IntegrationTests/Tests/Body/LegTest.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using Content.Server.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.Rotation; +using Content.Shared.GameObjects.EntitySystems; +using NUnit.Framework; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Map; + +namespace Content.IntegrationTests.Tests.Body +{ + [TestFixture] + [TestOf(typeof(SharedBodyComponent))] + [TestOf(typeof(BodyComponent))] + public class LegTest : ContentIntegrationTest + { + [Test] + public async Task RemoveLegsFallTest() + { + var server = StartServerDummyTicker(); + + AppearanceComponent appearance = null; + + await server.WaitAssertion(() => + { + var mapManager = IoCManager.Resolve(); + + var mapId = new MapId(0); + mapManager.CreateNewMapEntity(mapId); + + var entityManager = IoCManager.Resolve(); + var human = entityManager.SpawnEntity("HumanMob_Content", MapCoordinates.Nullspace); + + Assert.That(human.TryGetBody(out var body)); + Assert.That(human.TryGetComponent(out appearance)); + + Assert.That(!appearance.TryGetData(RotationVisuals.RotationState, out RotationState _)); + + var legs = body.GetPartsOfType(BodyPartType.Leg); + + foreach (var leg in legs) + { + body.RemovePart(leg, false); + } + }); + + await server.WaitAssertion(() => + { + Assert.That(appearance.TryGetData(RotationVisuals.RotationState, out RotationState state)); + Assert.That(state, Is.EqualTo(RotationState.Horizontal)); + }); + } + } +} diff --git a/Content.IntegrationTests/Tests/LungTest.cs b/Content.IntegrationTests/Tests/Body/LungTest.cs similarity index 90% rename from Content.IntegrationTests/Tests/LungTest.cs rename to Content.IntegrationTests/Tests/Body/LungTest.cs index 58c9b78079..e1af0314c7 100644 --- a/Content.IntegrationTests/Tests/LungTest.cs +++ b/Content.IntegrationTests/Tests/Body/LungTest.cs @@ -1,10 +1,12 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Content.Server.Atmos; +using Content.Server.GameObjects.Components.Body.Behavior; using Content.Server.GameObjects.Components.Body.Circulatory; -using Content.Server.GameObjects.Components.Body.Respiratory; using Content.Server.GameObjects.Components.Metabolism; using Content.Shared.Atmos; +using Content.Shared.GameObjects.Components.Body.Mechanism; using NUnit.Framework; using Robust.Server.Interfaces.Maps; using Robust.Shared.Interfaces.GameObjects; @@ -13,10 +15,10 @@ using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; -namespace Content.IntegrationTests.Tests +namespace Content.IntegrationTests.Tests.Body { [TestFixture] - [TestOf(typeof(LungComponent))] + [TestOf(typeof(LungBehaviorComponent))] public class LungTest : ContentIntegrationTest { [Test] @@ -34,7 +36,8 @@ namespace Content.IntegrationTests.Tests var human = entityManager.SpawnEntity("HumanMob_Content", MapCoordinates.Nullspace); - Assert.True(human.TryGetComponent(out LungComponent lung)); + Assert.True(human.TryGetMechanismBehaviors(out List lungs)); + Assert.That(lungs.Count, Is.EqualTo(1)); Assert.True(human.TryGetComponent(out BloodstreamComponent bloodstream)); var gas = new GasMixture(1); @@ -46,6 +49,7 @@ namespace Content.IntegrationTests.Tests gas.AdjustMoles(Gas.Oxygen, originalOxygen); gas.AdjustMoles(Gas.Nitrogen, originalNitrogen); + var lung = lungs[0]; lung.Inhale(1, gas); var lungOxygen = originalOxygen * breathedPercentage; @@ -114,7 +118,6 @@ namespace Content.IntegrationTests.Tests MapId mapId; IMapGrid grid = null; - LungComponent lung = null; MetabolismComponent metabolism = null; IEntity human = null; @@ -134,7 +137,7 @@ namespace Content.IntegrationTests.Tests var coordinates = new EntityCoordinates(grid.GridEntityId, center); human = entityManager.SpawnEntity("HumanMob_Content", coordinates); - Assert.True(human.TryGetComponent(out lung)); + Assert.True(human.HasMechanismBehavior()); Assert.True(human.TryGetComponent(out metabolism)); Assert.False(metabolism.Suffocating); }); diff --git a/Content.IntegrationTests/Tests/BuckleTest.cs b/Content.IntegrationTests/Tests/BuckleTest.cs index d1fd3f7746..58c36183f4 100644 --- a/Content.IntegrationTests/Tests/BuckleTest.cs +++ b/Content.IntegrationTests/Tests/BuckleTest.cs @@ -4,6 +4,8 @@ using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Strap; using Content.Shared.Damage; +using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Buckle; using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.EntitySystems; @@ -182,7 +184,7 @@ namespace Content.IntegrationTests.Tests BuckleComponent buckle = null; StrapComponent strap = null; HandsComponent hands = null; - IDamageableComponent humanDamageable = null; + IBody body = null; server.Assert(() => { @@ -208,7 +210,7 @@ namespace Content.IntegrationTests.Tests Assert.True(human.TryGetComponent(out buckle)); Assert.True(chair.TryGetComponent(out strap)); Assert.True(human.TryGetComponent(out hands)); - Assert.True(human.TryGetComponent(out humanDamageable)); + Assert.True(human.TryGetBody(out body)); // Buckle Assert.True(buckle.TryBuckle(human, chair)); @@ -239,8 +241,13 @@ namespace Content.IntegrationTests.Tests Assert.NotNull(hands.GetItem(slot)); } - // Banish our guy into the shadow realm - humanDamageable.ChangeDamage(DamageClass.Brute, 1000000, true); + var legs = body.GetPartsOfType(BodyPartType.Leg); + + // Break our guy's kneecaps + foreach (var leg in legs) + { + body.RemovePart(leg, false); + } }); server.RunTicks(10); diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/CuffUnitTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/CuffUnitTest.cs index 182e3079b0..02e4006b13 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/CuffUnitTest.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/CuffUnitTest.cs @@ -1,20 +1,17 @@ #nullable enable - using System.Linq; using System.Threading.Tasks; using Content.Client.GameObjects.Components.Items; -using Content.Server.Body; using Content.Server.GameObjects.Components.ActionBlocking; using Content.Server.GameObjects.Components.Body; using Content.Server.Interfaces.GameObjects.Components.Items; -using Content.Shared.Body.Part; using Content.Shared.GameObjects.Components.Body; using NUnit.Framework; +using Robust.Server.Interfaces.Console; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Map; using Robust.Shared.IoC; using Robust.Shared.Map; -using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking { @@ -36,7 +33,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking HandcuffComponent handcuff; CuffableComponent cuffed; IHandsComponent hands; - BodyManagerComponent body; + IBody body; server.Assert(() => { @@ -56,7 +53,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking // Test for components existing Assert.True(human.TryGetComponent(out cuffed!), $"Human has no {nameof(CuffableComponent)}"); Assert.True(human.TryGetComponent(out hands!), $"Human has no {nameof(HandsComponent)}"); - Assert.True(human.TryGetComponent(out body!), $"Human has no {nameof(BodyManagerComponent)}"); + Assert.True(human.TryGetBody(out body!), $"Human has no {nameof(IBody)}"); Assert.True(cuffs.TryGetComponent(out handcuff!), $"Handcuff has no {nameof(HandcuffComponent)}"); Assert.True(cables.TryGetComponent(out cableHandcuff!), $"Cablecuff has no {nameof(HandcuffComponent)}"); @@ -65,8 +62,8 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking Assert.True(cuffed.CuffedHandCount > 0, "Handcuffing a player did not result in their hands being cuffed"); // Test to ensure a player with 4 hands will still only have 2 hands cuffed - AddHand(body); - AddHand(body); + AddHand(cuffed.Owner); + AddHand(cuffed.Owner); Assert.True(cuffed.CuffedHandCount == 2 && hands.Hands.Count() == 4, "Player doesn't have correct amount of hands cuffed"); // Test to give a player with 4 hands 2 sets of cuffs @@ -78,16 +75,10 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking await server.WaitIdleAsync(); } - private void AddHand(BodyManagerComponent body) + private void AddHand(IEntity to) { - var prototypeManager = IoCManager.Resolve(); - prototypeManager.TryIndex("bodyPart.LHand.BasicHuman", out BodyPartPrototype prototype); - - var part = new BodyPart(prototype); - var slot = part.GetHashCode().ToString(); - - body.Template.Slots.Add(slot, BodyPartType.Hand); - body.TryAddPart(slot, part, true); + var shell = IoCManager.Resolve(); + shell.ExecuteCommand($"addhand {to.Uid}"); } } } diff --git a/Content.IntegrationTests/Tests/GridTileLookupTest.cs b/Content.IntegrationTests/Tests/GridTileLookupTest.cs index 651c2c9482..a854626a0f 100644 --- a/Content.IntegrationTests/Tests/GridTileLookupTest.cs +++ b/Content.IntegrationTests/Tests/GridTileLookupTest.cs @@ -43,8 +43,8 @@ namespace Content.IntegrationTests.Tests entities = tileLookup.GetEntitiesIntersecting(gridOne.Index, new MapIndices(1000, 1000)).ToList(); Assert.That(entities.Count, Is.EqualTo(0)); - var entityOne = entityManager.SpawnEntity("HumanMob_Content", new EntityCoordinates(gridOne.GridEntityId, Vector2.Zero)); - entityManager.SpawnEntity("HumanMob_Content", new EntityCoordinates(gridOne.GridEntityId, Vector2.One)); + var entityOne = entityManager.SpawnEntity("Food4NoRaisins", new EntityCoordinates(gridOne.GridEntityId, Vector2.Zero)); + entityManager.SpawnEntity("Food4NoRaisins", new EntityCoordinates(gridOne.GridEntityId, Vector2.One)); var entityTiles = tileLookup.GetIndices(entityOne); Assert.That(entityTiles.Count, Is.EqualTo(2)); diff --git a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs index b92d55ce1f..f4b8107218 100644 --- a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs +++ b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs @@ -151,12 +151,12 @@ namespace Content.Server.AI.Utility.AiLogic private void DeathHandle(HealthChangedEventArgs eventArgs) { var oldDeadState = _isDead; - _isDead = eventArgs.Damageable.CurrentDamageState == DamageState.Dead || eventArgs.Damageable.CurrentDamageState == DamageState.Critical; + _isDead = eventArgs.Damageable.CurrentState == DamageState.Dead || eventArgs.Damageable.CurrentState == DamageState.Critical; if (oldDeadState != _isDead) { var entityManager = IoCManager.Resolve(); - + switch (_isDead) { case true: diff --git a/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs b/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs index 856a53958d..c26b590dac 100644 --- a/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs +++ b/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs @@ -15,7 +15,7 @@ namespace Content.Server.AI.Utility.Considerations.Combat return 0.0f; } - if (damageableComponent.CurrentDamageState == DamageState.Critical) + if (damageableComponent.CurrentState == 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 d4d582e6b1..dc9d3edcc7 100644 --- a/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs +++ b/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs @@ -15,7 +15,7 @@ namespace Content.Server.AI.Utility.Considerations.Combat return 0.0f; } - if (damageableComponent.CurrentDamageState == DamageState.Dead) + if (damageableComponent.CurrentState == DamageState.Dead) { return 1.0f; } diff --git a/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs b/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs index 0c0693b51d..54dc776808 100644 --- a/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs +++ b/Content.Server/AI/WorldState/States/Mobs/NearbyBodiesState.cs @@ -21,7 +21,7 @@ namespace Content.Server.AI.WorldState.States.Mobs return result; } - foreach (var entity in Visibility.GetEntitiesInRange(Owner.Transform.Coordinates, typeof(ISharedBodyManagerComponent), controller.VisionRadius)) + foreach (var entity in Visibility.GetEntitiesInRange(Owner.Transform.Coordinates, typeof(IBody), controller.VisionRadius)) { if (entity == Owner) continue; result.Add(entity); diff --git a/Content.Server/Body/BodyPart.cs b/Content.Server/Body/BodyPart.cs deleted file mode 100644 index 51e59aee20..0000000000 --- a/Content.Server/Body/BodyPart.cs +++ /dev/null @@ -1,534 +0,0 @@ -#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.Shared.Interfaces.GameObjects; -using Robust.Shared.Interfaces.Reflection; -using Robust.Shared.Interfaces.Serialization; -using Robust.Shared.IoC; -using Robust.Shared.Log; -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 : IBodyPart - { - private IBodyManagerComponent? _body; - - 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); - } - - [ViewVariables] - public IBodyManagerComponent? 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; } - - [ViewVariables] public string Name { get; private set; } - - [ViewVariables] public string Plural { get; private set; } - - [ViewVariables] public string RSIPath { get; private set; } - - [ViewVariables] public string RSIState { get; private set; } - - [ViewVariables] public Enum? RSIMap { get; set; } - - // TODO: SpriteComponent rework - [ViewVariables] public Color? RSIColor { get; set; } - - [ViewVariables] public BodyPartType PartType { get; private set; } - - [ViewVariables] public int Size { get; private set; } - - [ViewVariables] public int MaxDurability { get; private set; } - - [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; - - /// - /// Represents if body part is vital for creature. - /// If the last vital body part is removed creature dies - /// - [ViewVariables] - public bool IsVital { get; private set; } - - /// - /// 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([NotNullWhen(true)] out T? property) where T : BodyPartProperty - { - property = (T?) Properties.FirstOrDefault(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, [NotNullWhen(true)] 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; - } - - public bool CanAttachPart(IBodyPart part) - { - return SurgeryData.CanAttachBodyPart(part); - } - - public bool CanInstallMechanism(IMechanism 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(IMechanism 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; - } - - public bool TryDropMechanism(IEntity dropLocation, IMechanism 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.Coordinates; - 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(IMechanism mechanismTarget) - { - if (!RemoveMechanism(mechanismTarget)) - { - return false; - } - - return true; - } - - public bool SurgeryCheck(SurgeryType surgery) - { - return SurgeryData.CheckSurgery(surgery); - } - - /// - /// 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(IMechanism 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(IMechanism 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; - IsVital = data.IsVital; - - 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.Coordinates); - - dropped.GetComponent().TransferBodyPartData(this); - - return true; - } - } -} diff --git a/Content.Server/Body/BodyPreset.cs b/Content.Server/Body/BodyPreset.cs deleted file mode 100644 index 03572fcbb8..0000000000 --- a/Content.Server/Body/BodyPreset.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using Content.Shared.Body.Part; -using Content.Shared.Body.Preset; -using Robust.Shared.Utility; -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 - { - [ViewVariables] public bool Initialized { get; private set; } - - [ViewVariables] public string Name { get; protected 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; protected set; } - - public virtual void Initialize(BodyPresetPrototype prototype) - { - DebugTools.Assert(!Initialized, $"{nameof(BodyPreset)} {Name} has already been initialized!"); - - Name = prototype.Name; - PartIDs = prototype.PartIDs; - - Initialized = true; - } - } -} diff --git a/Content.Server/Body/BodyTemplate.cs b/Content.Server/Body/BodyTemplate.cs deleted file mode 100644 index 88af6ee085..0000000000 --- a/Content.Server/Body/BodyTemplate.cs +++ /dev/null @@ -1,137 +0,0 @@ -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.Utility; -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 - { - [ViewVariables] public bool Initialized { get; private set; } - - [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; } = new Dictionary(); - - /// - /// 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; } = new Dictionary>(); - - [ViewVariables] - public Dictionary Layers { get; private set; } = new Dictionary(); - - [ViewVariables] - public Dictionary MechanismLayers { get; private set; } = new Dictionary(); - - 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 HasSlot(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; - } - - public virtual void Initialize(BodyTemplatePrototype prototype) - { - DebugTools.Assert(!Initialized, $"{nameof(BodyTemplate)} {Name} has already been initialized!"); - - Name = prototype.Name; - CenterSlot = prototype.CenterSlot; - Slots = new Dictionary(prototype.Slots); - Connections = new Dictionary>(prototype.Connections); - Layers = new Dictionary(prototype.Layers); - MechanismLayers = new Dictionary(prototype.MechanismLayers); - - Initialized = true; - } - } -} diff --git a/Content.Server/Body/IBodyPart.cs b/Content.Server/Body/IBodyPart.cs deleted file mode 100644 index eaa49d0e06..0000000000 --- a/Content.Server/Body/IBodyPart.cs +++ /dev/null @@ -1,136 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Content.Server.Body.Mechanisms; -using Content.Server.GameObjects.Components.Body; -using Content.Shared.Body.Part.Properties; -using Content.Shared.GameObjects.Components.Body; -using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.Maths; - -namespace Content.Server.Body -{ - public interface IBodyPart - { - /// - /// The body that this body part is currently in, if any. - /// - IBodyManagerComponent? Body { get; set; } - - /// - /// that this is considered - /// to be. - /// For example, . - /// - BodyPartType PartType { get; } - - /// - /// The name of this , often displayed to the user. - /// For example, it could be named "advanced robotic arm". - /// - public string Name { get; } - - /// - /// Plural version of this name. - /// - public string Plural { get; } - - /// - /// Determines many things: how many mechanisms can be fit inside this - /// , whether a body can fit through tiny crevices, - /// etc. - /// - int Size { get; } - - /// - /// Max HP of this . - /// - int MaxDurability { get; } - - /// - /// Current HP of this based on sum of all damage types. - /// - int CurrentDurability { get; } - - /// - /// Collection of all s currently inside this - /// . - /// To add and remove from this list see and - /// - /// - IReadOnlyCollection Mechanisms { get; } - - /// - /// Path to the RSI that represents this . - /// - public string RSIPath { get; } - - /// - /// RSI state that represents this . - /// - public string RSIState { get; } - - /// - /// RSI map keys that this body part changes on the sprite. - /// - public Enum? RSIMap { get; set; } - - /// - /// RSI color of this body part. - /// - // TODO: SpriteComponent rework - public Color? RSIColor { get; set; } - - /// - /// If body part is vital - /// - public bool IsVital { get; } - - bool HasProperty() where T : BodyPartProperty; - - bool HasProperty(Type type); - - bool TryGetProperty([NotNullWhen(true)] out T? property) where T : BodyPartProperty; - - void PreMetabolism(float frameTime); - - void PostMetabolism(float frameTime); - - bool SpawnDropped([NotNullWhen(true)] out IEntity? dropped); - - /// - /// Checks if the given can be used on - /// the current state of this . - /// - /// True if it can be used, false otherwise. - bool SurgeryCheck(SurgeryType surgery); - - /// - /// Checks if another can be connected to this one. - /// - /// The part to connect. - /// True if it can be connected, false otherwise. - bool CanAttachPart(IBodyPart part); - - /// - /// Checks if a can be installed on this - /// . - /// - /// True if it can be installed, false otherwise. - bool CanInstallMechanism(IMechanism mechanism); - - /// - /// 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. - /// - bool TryDropMechanism(IEntity dropLocation, IMechanism mechanismTarget, - [NotNullWhen(true)] out DroppedMechanismComponent dropped); - - bool DestroyMechanism(IMechanism mechanism); - } -} diff --git a/Content.Server/Body/Mechanisms/Behaviors/BrainBehavior.cs b/Content.Server/Body/Mechanisms/Behaviors/BrainBehavior.cs deleted file mode 100644 index 869534803e..0000000000 --- a/Content.Server/Body/Mechanisms/Behaviors/BrainBehavior.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 2229357bfc..0000000000 --- a/Content.Server/Body/Mechanisms/Behaviors/HeartBehavior.cs +++ /dev/null @@ -1,38 +0,0 @@ -#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 deleted file mode 100644 index bc2591c387..0000000000 --- a/Content.Server/Body/Mechanisms/Behaviors/LungBehavior.cs +++ /dev/null @@ -1,27 +0,0 @@ -#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 deleted file mode 100644 index 54f1e97384..0000000000 --- a/Content.Server/Body/Mechanisms/Behaviors/MechanismBehavior.cs +++ /dev/null @@ -1,185 +0,0 @@ -#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(IBodyManagerComponent 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(IBodyPart old) - { - OnRemovedFromPart(old); - TryRemoveNetwork(old.Body); - } - - private void TryAddNetwork() - { - if (Network != null) - { - Mechanism.Body?.EnsureNetwork(Network); - } - } - - private void TryRemoveNetwork(IBodyManagerComponent? 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(IBodyManagerComponent old) { } - - /// - /// Called when the parent is - /// removed from a . - /// For instance, taking a brain out of ones head. - /// - protected virtual void OnRemovedFromPart(IBodyPart 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 deleted file mode 100644 index ce6a2bcf43..0000000000 --- a/Content.Server/Body/Mechanisms/Behaviors/StomachBehavior.cs +++ /dev/null @@ -1,36 +0,0 @@ -#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/IMechanism.cs b/Content.Server/Body/Mechanisms/IMechanism.cs deleted file mode 100644 index 9037c0f4e9..0000000000 --- a/Content.Server/Body/Mechanisms/IMechanism.cs +++ /dev/null @@ -1,103 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using Content.Server.Body.Mechanisms.Behaviors; -using Content.Server.GameObjects.Components.Body; -using Content.Shared.GameObjects.Components.Body; - -namespace Content.Server.Body.Mechanisms -{ - public interface IMechanism - { - string Id { get; } - - string Name { get; set; } - - /// - /// Professional description of the . - /// - 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. - /// - string ExamineMessage { get; set; } - - // TODO: Make RSI properties sane - /// - /// Path to the RSI that represents this . - /// - string RSIPath { get; set; } - - /// - /// RSI state that represents this . - /// - string RSIState { get; set; } - - /// - /// Max HP of this . - /// - int MaxDurability { get; set; } - - /// - /// Current HP of this . - /// - int CurrentDurability { get; set; } - - /// - /// At what HP this is completely destroyed. - /// - int DestroyThreshold { get; set; } - - /// - /// Armor of this against attacks. - /// - int Resistance { get; set; } - - /// - /// Determines a handful of things - mostly whether this - /// can fit into a . - /// - // TODO: OnSizeChanged - int Size { get; set; } - - /// - /// What kind of this can be - /// easily installed into. - /// - BodyPartCompatibility Compatibility { get; set; } - - IReadOnlyList Behaviors { get; } - - IBodyManagerComponent? Body { get; } - - IBodyPart? Part { get; set; } - - void EnsureInitialize(); - - void InstalledIntoBody(); - - void RemovedFromBody(IBodyManagerComponent old); - - /// - /// This method is called by before - /// is called. - /// - void PreMetabolism(float frameTime); - - /// - /// This method is called by after - /// is called. - /// - void PostMetabolism(float frameTime); - - void AddBehavior(MechanismBehavior behavior); - - /// - /// Removes a behavior from this mechanism. - /// - /// The behavior to remove. - /// True if it was removed, false otherwise. - bool RemoveBehavior(MechanismBehavior behavior); - } -} diff --git a/Content.Server/Body/Mechanisms/Mechanism.cs b/Content.Server/Body/Mechanisms/Mechanism.cs deleted file mode 100644 index e44170de2a..0000000000 --- a/Content.Server/Body/Mechanisms/Mechanism.cs +++ /dev/null @@ -1,200 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using Content.Server.Body.Mechanisms.Behaviors; -using Content.Server.GameObjects.Components.Body; -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 : IMechanism - { - private IBodyPart? _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; } - - [ViewVariables] public string Description { get; set; } - - [ViewVariables] public string ExamineMessage { get; set; } - - [ViewVariables] public string RSIPath { get; set; } - - [ViewVariables] public string RSIState { get; set; } - - [ViewVariables] public int MaxDurability { get; set; } - - [ViewVariables] public int CurrentDurability { get; set; } - - [ViewVariables] public int DestroyThreshold { get; set; } - - [ViewVariables] public int Resistance { get; set; } - - [ViewVariables] public int Size { get; set; } - - [ViewVariables] public BodyPartCompatibility Compatibility { get; set; } - - private readonly List _behaviors; - - [ViewVariables] public IReadOnlyList Behaviors => _behaviors; - - public IBodyManagerComponent? Body => Part?.Body; - - public IBodyPart? 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(IBodyManagerComponent old) - { - foreach (var behavior in Behaviors) - { - behavior.RemovedFromBody(old); - } - } - - public void PreMetabolism(float frameTime) - { - foreach (var behavior in Behaviors) - { - behavior.PreMetabolism(frameTime); - } - } - - public void PostMetabolism(float frameTime) - { - foreach (var behavior in Behaviors) - { - behavior.PostMetabolism(frameTime); - } - } - - public void AddBehavior(MechanismBehavior behavior) - { - _behaviors.Add(behavior); - behavior.Initialize(this); - } - - public bool RemoveBehavior(MechanismBehavior behavior) - { - if (_behaviors.Remove(behavior)) - { - behavior.Remove(); - return true; - } - - return false; - } - } -} diff --git a/Content.Server/Body/Network/BodyNetwork.cs b/Content.Server/Body/Network/BodyNetwork.cs deleted file mode 100644 index b9ba20c272..0000000000 --- a/Content.Server/Body/Network/BodyNetwork.cs +++ /dev/null @@ -1,83 +0,0 @@ -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 PreMetabolism(float frameTime) { } - - /// - /// Called every update by - /// . - /// - public virtual void PostMetabolism(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 deleted file mode 100644 index 8b5ec0cc7f..0000000000 --- a/Content.Server/Body/Network/BodyNetworkFactory.cs +++ /dev/null @@ -1,88 +0,0 @@ -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 deleted file mode 100644 index c67bc6e201..0000000000 --- a/Content.Server/Body/Network/CirculatoryNetwork.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 6362b55072..0000000000 --- a/Content.Server/Body/Network/DigestiveNetwork.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 29d883f21b..0000000000 --- a/Content.Server/Body/Network/IBodyNetworkFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 92c974f2e3..0000000000 --- a/Content.Server/Body/Network/RespiratoryNetwork.cs +++ /dev/null @@ -1,25 +0,0 @@ -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/EntryPoint.cs b/Content.Server/EntryPoint.cs index 64eb99018c..9542e2b36a 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -1,6 +1,5 @@ using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; -using Content.Server.Body.Network; using Content.Server.Database; using Content.Server.GameObjects.Components.Mobs.Speech; using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; @@ -49,8 +48,6 @@ namespace Content.Server IoCManager.BuildGraph(); - IoCManager.Resolve().DoAutoRegistrations(); - _gameTicker = IoCManager.Resolve(); IoCManager.Resolve().Initialize(); diff --git a/Content.Server/GameObjects/Components/Body/Behavior/HeartBehaviorComponent.cs b/Content.Server/GameObjects/Components/Body/Behavior/HeartBehaviorComponent.cs new file mode 100644 index 0000000000..9b0c9bc5fb --- /dev/null +++ b/Content.Server/GameObjects/Components/Body/Behavior/HeartBehaviorComponent.cs @@ -0,0 +1,33 @@ +using Content.Shared.GameObjects.Components.Body.Behavior; +using Content.Shared.GameObjects.Components.Body.Networks; +using Robust.Shared.GameObjects; + +namespace Content.Server.GameObjects.Components.Body.Behavior +{ + [RegisterComponent] + [ComponentReference(typeof(SharedHeartBehaviorComponent))] + public class HeartBehaviorComponent : SharedHeartBehaviorComponent + { + private float _accumulatedFrameTime; + + public override void Update(float frameTime) + { + // TODO BODY do between pre and metabolism + if (Mechanism?.Body == null || + !Mechanism.Body.Owner.HasComponent()) + { + 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/GameObjects/Components/Body/Respiratory/LungComponent.cs b/Content.Server/GameObjects/Components/Body/Behavior/LungBehaviorComponent.cs similarity index 80% rename from Content.Server/GameObjects/Components/Body/Respiratory/LungComponent.cs rename to Content.Server/GameObjects/Components/Body/Behavior/LungBehaviorComponent.cs index af1e520c07..7d1beb0663 100644 --- a/Content.Server/GameObjects/Components/Body/Respiratory/LungComponent.cs +++ b/Content.Server/GameObjects/Components/Body/Behavior/LungBehaviorComponent.cs @@ -1,29 +1,28 @@ +#nullable enable using System; using System.Linq; using Content.Server.Atmos; using Content.Server.GameObjects.Components.Body.Circulatory; -using Content.Server.Interfaces; using Content.Server.Utility; using Content.Shared.Atmos; -using Content.Shared.Interfaces; +using Content.Shared.GameObjects.Components.Body.Behavior; using Robust.Shared.GameObjects; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; -namespace Content.Server.GameObjects.Components.Body.Respiratory +namespace Content.Server.GameObjects.Components.Body.Behavior { [RegisterComponent] - public class LungComponent : Component, IGasMixtureHolder + [ComponentReference(typeof(SharedLungBehaviorComponent))] + public class LungBehaviorComponent : SharedLungBehaviorComponent { - public override string Name => "Lung"; - private float _accumulatedFrameTime; - [ViewVariables] public GasMixture Air { get; set; } + [ViewVariables] public GasMixture Air { get; set; } = default!; - [ViewVariables] public LungStatus Status { get; set; } + [ViewVariables] public override float Temperature => Air.Temperature; - [ViewVariables] public float CycleDelay { get; set; } + [ViewVariables] public override float Volume => Air.Volume; public override void ExposeData(ObjectSerializer serializer) { @@ -42,11 +41,52 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory Atmospherics.NormalBodyTemperature, temp => Air.Temperature = temp, () => Air.Temperature); - - serializer.DataField(this, l => l.CycleDelay, "cycleDelay", 2); } - public void Update(float frameTime) + public override void Gasp() + { + Owner.PopupMessageEveryone("Gasp"); + Inhale(CycleDelay); + } + + public void Transfer(GasMixture from, GasMixture to, float ratio) + { + var removed = from.RemoveRatio(ratio); + var toOld = to.Gases.ToArray(); + + to.Merge(removed); + + for (var gas = 0; gas < Atmospherics.TotalNumberOfGases; gas++) + { + var newAmount = to.GetMoles(gas); + var oldAmount = toOld[gas]; + var delta = newAmount - oldAmount; + + removed.AdjustMoles(gas, -delta); + } + + from.Merge(removed); + } + + public void ToBloodstream(GasMixture mixture) + { + if (Body == null) + { + return; + } + + if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream)) + { + return; + } + + var to = bloodstream.Air; + + to.Merge(mixture); + mixture.Clear(); + } + + public override void Update(float frameTime) { if (Status == LungStatus.None) { @@ -85,39 +125,7 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory _accumulatedFrameTime = absoluteTime - delay; } - public void Transfer(GasMixture from, GasMixture to, float ratio) - { - var removed = from.RemoveRatio(ratio); - var toOld = to.Gases.ToArray(); - - to.Merge(removed); - - for (var gas = 0; gas < Atmospherics.TotalNumberOfGases; gas++) - { - var newAmount = to.GetMoles(gas); - var oldAmount = toOld[gas]; - var delta = newAmount - oldAmount; - - removed.AdjustMoles(gas, -delta); - } - - from.Merge(removed); - } - - public void ToBloodstream(GasMixture mixture) - { - if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream)) - { - return; - } - - var to = bloodstream.Air; - - to.Merge(mixture); - mixture.Clear(); - } - - public void Inhale(float frameTime) + public override void Inhale(float frameTime) { if (!Owner.Transform.Coordinates.TryGetTileAir(out var tileAir)) { @@ -135,7 +143,7 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory ToBloodstream(Air); } - public void Exhale(float frameTime) + public override void Exhale(float frameTime) { if (!Owner.Transform.Coordinates.TryGetTileAir(out var tileAir)) { @@ -148,7 +156,12 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory public void Exhale(float frameTime, GasMixture to) { // TODO: Make the bloodstream separately pump toxins into the lungs, making the lungs' only job to empty. - if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream)) + if (Body == null) + { + return; + } + + if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream)) { return; } @@ -171,18 +184,5 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory Air.Merge(lungRemoved); } - - public void Gasp() - { - Owner.PopupMessageEveryone("Gasp"); - Inhale(CycleDelay); - } - } - - public enum LungStatus - { - None = 0, - Inhaling, - Exhaling } } diff --git a/Content.Server/GameObjects/Components/Body/Behavior/StomachBehaviorComponent.cs b/Content.Server/GameObjects/Components/Body/Behavior/StomachBehaviorComponent.cs new file mode 100644 index 0000000000..10476d09c6 --- /dev/null +++ b/Content.Server/GameObjects/Components/Body/Behavior/StomachBehaviorComponent.cs @@ -0,0 +1,24 @@ +using Content.Server.GameObjects.Components.Chemistry; +using Content.Shared.GameObjects.Components.Body.Behavior; +using Robust.Shared.GameObjects; +using Robust.Shared.Log; + +namespace Content.Server.GameObjects.Components.Body.Behavior +{ + [RegisterComponent] + [ComponentReference(typeof(SharedStomachBehaviorComponent))] + public class StomachBehaviorComponent : SharedStomachBehaviorComponent + { + protected override void Startup() + { + base.Startup(); + + if (!Owner.EnsureComponent(out SolutionContainerComponent solution)) + { + Logger.Warning($"Entity {Owner} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionContainerComponent)}"); + } + + solution.MaxVolume = InitialMaxVolume; + } + } +} diff --git a/Content.Server/Body/BodyCommands.cs b/Content.Server/GameObjects/Components/Body/BodyCommands.cs similarity index 61% rename from Content.Server/Body/BodyCommands.cs rename to Content.Server/GameObjects/Components/Body/BodyCommands.cs index b7616933e6..a9be39d6dc 100644 --- a/Content.Server/Body/BodyCommands.cs +++ b/Content.Server/GameObjects/Components/Body/BodyCommands.cs @@ -1,10 +1,9 @@ #nullable enable using System; using System.Linq; -using Content.Server.GameObjects.Components.Body; -using Content.Shared.Body.Part; using Content.Shared.Damage; using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Damage; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; @@ -15,29 +14,118 @@ using Robust.Shared.IoC; using Robust.Shared.Prototypes; using Robust.Shared.Random; -namespace Content.Server.Body +namespace Content.Server.GameObjects.Components.Body { class AddHandCommand : IClientCommand { + public const string DefaultHandPrototype = "LeftHandHuman"; + public string Command => "addhand"; public string Description => "Adds a hand to your entity."; - public string Help => $"Usage: {Command}"; + public string Help => $"Usage: {Command} / {Command} / {Command} / {Command}"; public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) { - if (player == null) + if (args.Length > 1) { - shell.SendText(player, "Only a player can run this command."); + shell.SendText(player, Help); return; } - if (player.AttachedEntity == null) + var entityManager = IoCManager.Resolve(); + var prototypeManager = IoCManager.Resolve(); + + IEntity entity; + IEntity hand; + + switch (args.Length) { - shell.SendText(player, "You have no entity."); - return; + case 0: + { + if (player == null) + { + shell.SendText(player, "Only a player can run this command without arguments."); + return; + } + + if (player.AttachedEntity == null) + { + shell.SendText(player, "You don't have an entity to add a hand to."); + return; + } + + entity = player.AttachedEntity; + hand = entityManager.SpawnEntity(DefaultHandPrototype, entity.Transform.Coordinates); + break; + } + case 1: + { + if (EntityUid.TryParse(args[0], out var uid)) + { + if (!entityManager.TryGetEntity(uid, out var parsedEntity)) + { + shell.SendText(player, $"No entity found with uid {uid}"); + return; + } + + entity = parsedEntity; + hand = entityManager.SpawnEntity(DefaultHandPrototype, entity.Transform.Coordinates); + } + else + { + if (player == null) + { + shell.SendText(player, + "You must specify an entity to add a hand to when using this command from the server terminal."); + return; + } + + if (player.AttachedEntity == null) + { + shell.SendText(player, "You don't have an entity to add a hand to."); + return; + } + + entity = player.AttachedEntity; + hand = entityManager.SpawnEntity(args[0], entity.Transform.Coordinates); + } + + break; + } + case 2: + { + if (!EntityUid.TryParse(args[0], out var uid)) + { + shell.SendText(player, $"{args[0]} is not a valid entity uid."); + return; + } + + if (!entityManager.TryGetEntity(uid, out var parsedEntity)) + { + shell.SendText(player, $"No entity exists with uid {uid}."); + return; + } + + entity = parsedEntity; + + if (!prototypeManager.HasIndex(args[1])) + { + shell.SendText(player, $"No hand entity exists with id {args[1]}."); + return; + } + + hand = entityManager.SpawnEntity(args[1], entity.Transform.Coordinates); + + break; + } + default: + { + shell.SendText(player, Help); + return; + } } - if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent? body)) + if (!entity.TryGetComponent(out IBody? body)) { var random = IoCManager.Resolve(); var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}"; @@ -46,14 +134,18 @@ namespace Content.Server.Body return; } - var prototypeManager = IoCManager.Resolve(); - prototypeManager.TryIndex("bodyPart.LHand.BasicHuman", out BodyPartPrototype prototype); + if (!hand.TryGetComponent(out IBodyPart? part)) + { + shell.SendText(player, $"Hand entity {hand} does not have a {nameof(IBodyPart)} component."); + return; + } - var part = new BodyPart(prototype); var slot = part.GetHashCode().ToString(); + var response = body.TryAddPart(slot, part, true) + ? $"Added hand to entity {entity.Name}" + : $"Error occurred trying to add a hand to entity {entity.Name}"; - body.Template.Slots.Add(slot, BodyPartType.Hand); - body.TryAddPart(slot, part, true); + shell.SendText(player, response); } } @@ -77,7 +169,7 @@ namespace Content.Server.Body return; } - if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent? body)) + if (!player.AttachedEntity.TryGetBody(out var body)) { var random = IoCManager.Resolve(); var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}"; @@ -87,7 +179,7 @@ namespace Content.Server.Body } var hand = body.Parts.FirstOrDefault(x => x.Value.PartType == BodyPartType.Hand); - if (hand.Value == null) + if (hand.Value.Equals(default)) { shell.SendText(player, "You have no hands."); } @@ -124,7 +216,7 @@ namespace Content.Server.Body return; } - if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent? body)) + if (!player.AttachedEntity.TryGetBody(out var body)) { var random = IoCManager.Resolve(); var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}"; @@ -140,7 +232,7 @@ namespace Content.Server.Body { if (mechanism.Name.ToLowerInvariant() == mechanismName) { - part.DestroyMechanism(mechanism); + part.DeleteMechanism(mechanism); shell.SendText(player, $"Mechanism with name {mechanismName} has been destroyed."); return; } @@ -190,7 +282,7 @@ namespace Content.Server.Body var ignoreResistance = false; var entityUid = player != null && player.AttachedEntityUid.HasValue ? player.AttachedEntityUid.Value : EntityUid.Invalid; if (!int.TryParse(args[1], out var amount) || - args.Length >= 3 && args[2] != "_" && !EntityUid.TryParse(args[2], out entityUid) || + args.Length >= 3 && args[2] != "_" && !EntityUid.TryParse(args[2], out entityUid) || args.Length >= 4 && !bool.TryParse(args[3], out ignoreResistance)) { shell.SendText(player, Help); diff --git a/Content.Server/GameObjects/Components/Body/BodyComponent.cs b/Content.Server/GameObjects/Components/Body/BodyComponent.cs new file mode 100644 index 0000000000..a440612a85 --- /dev/null +++ b/Content.Server/GameObjects/Components/Body/BodyComponent.cs @@ -0,0 +1,85 @@ +#nullable enable +using Content.Server.Observer; +using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Movement; +using Robust.Server.GameObjects.Components.Container; +using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.Log; +using Robust.Shared.Players; + +namespace Content.Server.GameObjects.Components.Body +{ + [RegisterComponent] + [ComponentReference(typeof(SharedBodyComponent))] + [ComponentReference(typeof(IBody))] + public class BodyComponent : SharedBodyComponent, IRelayMoveInput + { + private Container _container = default!; + + protected override bool CanAddPart(string slot, IBodyPart part) + { + return base.CanAddPart(slot, part) && _container.CanInsert(part.Owner); + } + + protected override void OnAddPart(string slot, IBodyPart part) + { + base.OnAddPart(slot, part); + + _container.Insert(part.Owner); + } + + protected override void OnRemovePart(string slot, IBodyPart part) + { + base.OnRemovePart(slot, part); + + _container.ForceRemove(part.Owner); + } + + public override void Initialize() + { + base.Initialize(); + + _container = ContainerManagerComponent.Ensure($"{Name}-{nameof(BodyComponent)}", Owner); + + foreach (var (slot, partId) in PartIds) + { + // Using MapPosition instead of Coordinates here prevents + // a crash within the character preview menu in the lobby + var entity = Owner.EntityManager.SpawnEntity(partId, Owner.Transform.MapPosition); + + if (!entity.TryGetComponent(out IBodyPart? part)) + { + Logger.Error($"Entity {partId} does not have a {nameof(IBodyPart)} component."); + continue; + } + + TryAddPart(slot, part, true); + } + } + + protected override void Startup() + { + base.Startup(); + + // This is ran in Startup as entities spawned in Initialize + // are not synced to the client since they are assumed to be + // identical on it + foreach (var part in Parts.Values) + { + part.Dirty(); + } + } + + void IRelayMoveInput.MoveInputPressed(ICommonSession session) + { + if (Owner.TryGetComponent(out IDamageableComponent? damageable) && + damageable.CurrentState == DamageState.Dead) + { + new Ghost().Execute(null, (IPlayerSession) session, null); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Body/BodyManagerComponent.Parts.cs b/Content.Server/GameObjects/Components/Body/BodyManagerComponent.Parts.cs deleted file mode 100644 index 614cb5eb0e..0000000000 --- a/Content.Server/GameObjects/Components/Body/BodyManagerComponent.Parts.cs +++ /dev/null @@ -1,543 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Server.Body; -using Content.Server.GameObjects.EntitySystems; -using Content.Server.Interfaces.GameObjects.Components.Interaction; -using Content.Shared.Body.Part.Properties.Movement; -using Content.Shared.Body.Part.Properties.Other; -using Content.Shared.GameObjects.Components.Body; -using Content.Shared.GameObjects.Components.Damage; -using Content.Shared.GameObjects.Components.Movement; -using Robust.Shared.GameObjects.Systems; -using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.Log; -using Robust.Shared.Utility; -using Robust.Shared.ViewVariables; - -namespace Content.Server.GameObjects.Components.Body -{ - public partial class BodyManagerComponent - { - private readonly Dictionary _parts = new Dictionary(); - - [ViewVariables] public BodyPreset Preset { get; private set; } = default!; - - /// - /// 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(); - - /// - /// Maps slot name to the - /// object filling it (if there is one). - /// - [ViewVariables] - public IReadOnlyDictionary Parts => _parts; - - /// - /// List of all occupied slots in this body, taken from the values of - /// . - /// - public IEnumerable OccupiedSlots => Parts.Keys; - - /// - /// List of all slots in this body, taken from the keys of - /// slots. - /// - public IEnumerable AllSlots => Template.Slots.Keys; - - public bool TryAddPart(string slot, DroppedBodyPartComponent part, bool force = false) - { - DebugTools.AssertNotNull(part); - - if (!TryAddPart(slot, part.ContainedBodyPart, force)) - { - return false; - } - - part.Owner.Delete(); - return true; - } - - public bool TryAddPart(string slot, IBodyPart part, bool force = false) - { - DebugTools.AssertNotNull(part); - DebugTools.AssertNotNull(slot); - - // Make sure the given slot exists - if (!force) - { - if (!HasSlot(slot)) - { - return false; - } - - // And that nothing is in it - if (!_parts.TryAdd(slot, part)) - { - return false; - } - } - else - { - _parts[slot] = part; - } - - part.Body = this; - - var argsAdded = new BodyPartAddedEventArgs(part, slot); - - foreach (var component in Owner.GetAllComponents().ToArray()) - { - component.BodyPartAdded(argsAdded); - } - - // TODO: Sort this duplicate out - OnBodyChanged(); - - if (!Template.Layers.TryGetValue(slot, 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 false; - } - - 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); - } - - return true; - } - - public bool HasPart(string slot) - { - return _parts.ContainsKey(slot); - } - - public void RemovePart(IBodyPart part, bool drop) - { - DebugTools.AssertNotNull(part); - - var slotName = _parts.FirstOrDefault(x => x.Value == part).Key; - - if (string.IsNullOrEmpty(slotName)) return; - - RemovePart(slotName, drop); - } - - public bool RemovePart(string slot, bool drop) - { - DebugTools.AssertNotNull(slot); - - if (!_parts.Remove(slot, out var part)) - { - return false; - } - - IEntity? dropped = null; - if (drop) - { - part.SpawnDropped(out dropped); - } - - part.Body = null; - - var args = new BodyPartRemovedEventArgs(part, slot); - - 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); - } - - if (CurrentDamageState == DamageState.Dead) return true; - - // creadth: fall down if no legs - if (part.PartType == BodyPartType.Leg && Parts.Count(x => x.Value.PartType == BodyPartType.Leg) == 0) - { - EntitySystem.Get().Down(Owner); - } - - // creadth: immediately kill entity if last vital part removed - if (part.IsVital && Parts.Count(x => x.Value.PartType == part.PartType) == 0) - { - CurrentDamageState = DamageState.Dead; - ForceHealthChangedEvent(); - } - - if (TryGetSlotConnections(slot, out var connections)) - { - foreach (var connectionName in connections) - { - if (TryGetPart(connectionName, out var result) && !ConnectedToCenter(result)) - { - RemovePart(connectionName, drop); - } - } - } - - OnBodyChanged(); - return true; - } - - public bool RemovePart(IBodyPart part, [NotNullWhen(true)] out string? slot) - { - DebugTools.AssertNotNull(part); - - var pair = _parts.FirstOrDefault(kvPair => kvPair.Value == part); - - if (pair.Equals(default)) - { - slot = null; - return false; - } - - slot = pair.Key; - - return RemovePart(slot, false); - } - - public IEntity? DropPart(IBodyPart part) - { - DebugTools.AssertNotNull(part); - - if (!_parts.ContainsValue(part)) - { - return null; - } - - if (!RemovePart(part, out var slotName)) - { - return null; - } - - // Call disconnect on all limbs that were hanging off this limb. - if (TryGetSlotConnections(slotName, out var connections)) - { - // This loop is an unoptimized travesty. TODO: optimize to be less shit - foreach (var connectionName in connections) - { - if (TryGetPart(connectionName, out var result) && !ConnectedToCenter(result)) - { - RemovePart(connectionName, true); - } - } - } - - part.SpawnDropped(out var dropped); - - OnBodyChanged(); - return dropped; - } - - public bool ConnectedToCenter(IBodyPart part) - { - var searchedSlots = new List(); - - return TryGetSlot(part, out var result) && - ConnectedToCenterPartRecursion(searchedSlots, result); - } - - private bool ConnectedToCenterPartRecursion(ICollection searchedSlots, string slotName) - { - if (!TryGetPart(slotName, out var part)) - { - return false; - } - - if (part == CenterPart()) - { - return true; - } - - searchedSlots.Add(slotName); - - if (!TryGetSlotConnections(slotName, out var connections)) - { - return false; - } - - foreach (var connection in connections) - { - if (!searchedSlots.Contains(connection) && - ConnectedToCenterPartRecursion(searchedSlots, connection)) - { - return true; - } - } - - return false; - } - - public IBodyPart? CenterPart() - { - Parts.TryGetValue(Template.CenterSlot, out var center); - return center; - } - - public bool HasSlot(string slot) - { - return Template.HasSlot(slot); - } - - public bool TryGetPart(string slot, [NotNullWhen(true)] out IBodyPart? result) - { - return Parts.TryGetValue(slot, out result); - } - - public bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out string? slot) - { - // 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. - var pair = Parts.FirstOrDefault(x => x.Value == part); - slot = pair.Key; - - return !pair.Equals(default); - } - - public bool TryGetSlotType(string slot, out BodyPartType result) - { - return Template.Slots.TryGetValue(slot, out result); - } - - public bool TryGetSlotConnections(string slot, [NotNullWhen(true)] out List? connections) - { - return Template.Connections.TryGetValue(slot, out connections); - } - - public bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List? result) - { - result = null; - - if (!Template.Connections.TryGetValue(slot, out var connections)) - { - return false; - } - - var toReturn = new List(); - foreach (var connection in connections) - { - if (TryGetPart(connection, out var partResult)) - { - toReturn.Add(partResult); - } - } - - if (toReturn.Count <= 0) - { - return false; - } - - result = toReturn; - return true; - } - - public bool TryGetPartConnections(IBodyPart part, [NotNullWhen(true)] out List? connections) - { - connections = null; - - return TryGetSlot(part, out var slotName) && - TryGetPartConnections(slotName, out connections); - } - - public List GetPartsOfType(BodyPartType type) - { - var toReturn = new List(); - - foreach (var part in Parts.Values) - { - if (part.PartType == type) - { - toReturn.Add(part); - } - } - - return toReturn; - } - - 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? leg)) - { - // Speed of a leg = base speed * (1+log1024(leg length)) - speedSum += leg.Speed * (1 + (float) Math.Log(value, 1024.0)); - } - } - - if (speedSum <= 0.001f || _activeLegs.Count <= 0) - { - playerMover.BaseWalkSpeed = 0.8f; - playerMover.BaseSprintSpeed = 2.0f; - } - else - { - // 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; - } - } - - /// - /// 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(part); - - if (Math.Abs(footDistance - float.MinValue) > 0.001f) - { - _activeLegs.Add(part, footDistance); - } - } - - CalculateSpeed(); - } - } - - /// - /// 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 . - /// - public float DistanceToNearestFoot(IBodyPart source) - { - if (source.HasProperty() && source.TryGetProperty(out var property)) - { - return property.ReachDistance; - } - - return LookForFootRecursion(source, new List()); - } - - private float LookForFootRecursion(IBodyPart 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 (!TryGetPartConnections(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(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. - } - } -} diff --git a/Content.Server/GameObjects/Components/Body/BodyManagerComponent.cs b/Content.Server/GameObjects/Components/Body/BodyManagerComponent.cs deleted file mode 100644 index 13add1a697..0000000000 --- a/Content.Server/GameObjects/Components/Body/BodyManagerComponent.cs +++ /dev/null @@ -1,296 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Content.Server.Body; -using Content.Server.Body.Network; -using Content.Server.GameObjects.Components.Metabolism; -using Content.Server.GameObjects.EntitySystems; -using Content.Server.Observer; -using Content.Shared.Body.Part; -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.Interfaces.Player; -using Robust.Shared.GameObjects; -using Robust.Shared.Interfaces.Reflection; -using Robust.Shared.IoC; -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(ISharedBodyManagerComponent))] - [ComponentReference(typeof(IBodyPartManager))] - [ComponentReference(typeof(IBodyManagerComponent))] - public partial class BodyManagerComponent : SharedBodyManagerComponent, IBodyPartContainer, IRelayMoveInput, IBodyManagerComponent - { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IBodyNetworkFactory _bodyNetworkFactory = default!; - [Dependency] private readonly IReflectionManager _reflectionManager = default!; - - [ViewVariables] private string _presetName = default!; - - [ViewVariables] private readonly Dictionary _networks = new Dictionary(); - - [ViewVariables] public BodyTemplate Template { get; private set; } = default!; - - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataReadWriteFunction( - "baseTemplate", - "bodyTemplate.Humanoid", - template => - { - if (!_prototypeManager.TryIndex(template, out BodyTemplatePrototype prototype)) - { - // Invalid prototype - throw new InvalidOperationException( - $"No {nameof(BodyTemplatePrototype)} found with name {template}"); - } - - Template = new BodyTemplate(); - Template.Initialize(prototype); - }, - () => Template.Name); - - serializer.DataReadWriteFunction( - "basePreset", - "bodyPreset.BasicHuman", - preset => - { - if (!_prototypeManager.TryIndex(preset, out BodyPresetPrototype prototype)) - { - // Invalid prototype - throw new InvalidOperationException( - $"No {nameof(BodyPresetPrototype)} found with name {preset}"); - } - - Preset = new BodyPreset(); - Preset.Initialize(prototype); - }, - () => _presetName); - } - - public override void Initialize() - { - base.Initialize(); - - LoadBodyPreset(Preset); - } - - 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. - RemovePart(slotName, false); - - // Add a new BodyPart with the BodyPartPrototype as a baseline to our - // BodyComponent. - var addedPart = new BodyPart(newPartData); - TryAddPart(slotName, addedPart); - } - - 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.PreMetabolism(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.PostMetabolism(frameTime); - } - } - - void IRelayMoveInput.MoveInputPressed(ICommonSession session) - { - if (CurrentDamageState == DamageState.Dead) - { - new Ghost().Execute(null, (IPlayerSession) session, null); - } - } - - #region BodyNetwork Functions - - private bool EnsureNetwork(BodyNetwork network) - { - DebugTools.AssertNotNull(network); - - if (_networks.ContainsKey(network.GetType())) - { - return true; - } - - _networks.Add(network.GetType(), network); - network.OnAdd(Owner); - - return false; - } - - /// - /// 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)); - } - - public void RemoveNetwork(Type networkType) - { - DebugTools.AssertNotNull(networkType); - - if (_networks.Remove(networkType, out var network)) - { - network.OnRemove(); - } - } - - public void RemoveNetwork() where T : BodyNetwork - { - RemoveNetwork(typeof(T)); - } - - /// - /// 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 - } - - 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/BodyManagerHealthChangeParams.cs b/Content.Server/GameObjects/Components/Body/BodyManagerHealthChangeParams.cs new file mode 100644 index 0000000000..06edc4a351 --- /dev/null +++ b/Content.Server/GameObjects/Components/Body/BodyManagerHealthChangeParams.cs @@ -0,0 +1,20 @@ +using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.Damage; + +namespace Content.Server.GameObjects.Components.Body +{ + public interface IBodyHealthChangeParams + { + BodyPartType Part { get; } + } + + public class BodyHealthChangeParams : HealthChangeParams, IBodyHealthChangeParams + { + public BodyHealthChangeParams(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 index 343d4cff5e..a21d842ac6 100644 --- a/Content.Server/GameObjects/Components/Body/BodyScannerComponent.cs +++ b/Content.Server/GameObjects/Components/Body/BodyScannerComponent.cs @@ -1,8 +1,7 @@ #nullable enable -using System.Collections.Generic; -using Content.Server.Body; using Content.Server.Utility; -using Content.Shared.Body.Scanner; +using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Scanner; using Content.Shared.Interfaces.GameObjects.Components; using Robust.Server.GameObjects.Components.UserInterface; using Robust.Server.Interfaces.GameObjects; @@ -14,27 +13,32 @@ namespace Content.Server.GameObjects.Components.Body { [RegisterComponent] [ComponentReference(typeof(IActivate))] - public class BodyScannerComponent : Component, IActivate + [ComponentReference(typeof(SharedBodyScannerComponent))] + public class BodyScannerComponent : SharedBodyScannerComponent, IActivate { - public sealed override string Name => "BodyScanner"; - [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BodyScannerUiKey.Key); void IActivate.Activate(ActivateEventArgs eventArgs) { - if (!eventArgs.User.TryGetComponent(out IActorComponent? actor) || - actor.playerSession.AttachedEntity == null) + if (!eventArgs.User.TryGetComponent(out IActorComponent? actor)) { return; } - if (actor.playerSession.AttachedEntity.TryGetComponent(out BodyManagerComponent? attempt)) + var session = actor.playerSession; + + if (session.AttachedEntity == null) { - var state = InterfaceState(attempt.Template, attempt.Parts); + return; + } + + if (session.AttachedEntity.TryGetComponent(out IBody? body)) + { + var state = InterfaceState(body); UserInterface?.SetState(state); } - UserInterface?.Open(actor.playerSession); + UserInterface?.Open(session); } public override void Initialize() @@ -56,29 +60,9 @@ namespace Content.Server.GameObjects.Components.Body /// /// Copy BodyTemplate and BodyPart data into a common data class that the client can read. /// - private BodyScannerInterfaceState InterfaceState(BodyTemplate template, IReadOnlyDictionary bodyParts) + private BodyScannerUIState InterfaceState(IBody body) { - 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); + return new BodyScannerUIState(body.Owner.Uid); } } } diff --git a/Content.Server/GameObjects/Components/Body/Circulatory/BloodstreamComponent.cs b/Content.Server/GameObjects/Components/Body/Circulatory/BloodstreamComponent.cs index 7f1ff43101..b38b2db3a7 100644 --- a/Content.Server/GameObjects/Components/Body/Circulatory/BloodstreamComponent.cs +++ b/Content.Server/GameObjects/Components/Body/Circulatory/BloodstreamComponent.cs @@ -5,6 +5,7 @@ using Content.Server.GameObjects.Components.Metabolism; using Content.Server.Interfaces; using Content.Shared.Atmos; using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Body.Networks; using Robust.Shared.GameObjects; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; @@ -12,7 +13,8 @@ using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Body.Circulatory { [RegisterComponent] - public class BloodstreamComponent : Component, IGasMixtureHolder + [ComponentReference(typeof(SharedBloodstreamComponent))] + public class BloodstreamComponent : SharedBloodstreamComponent, IGasMixtureHolder { public override string Name => "Bloodstream"; @@ -58,7 +60,7 @@ namespace Content.Server.GameObjects.Components.Body.Circulatory /// /// Solution to be transferred /// Whether or not transfer was a success - public bool TryTransferSolution(Solution solution) + public override bool TryTransferSolution(Solution solution) { // For now doesn't support partial transfers if (solution.TotalVolume + _internalSolution.CurrentVolume > _internalSolution.MaxVolume) diff --git a/Content.Server/GameObjects/Components/Body/DroppedMechanismComponent.cs b/Content.Server/GameObjects/Components/Body/DroppedMechanismComponent.cs deleted file mode 100644 index 4c16cc9b7d..0000000000 --- a/Content.Server/GameObjects/Components/Body/DroppedMechanismComponent.cs +++ /dev/null @@ -1,213 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using Content.Server.Body; -using Content.Server.Body.Mechanisms; -using Content.Server.Utility; -using Content.Shared.Body.Mechanism; -using Content.Shared.Body.Surgery; -using Content.Shared.Interfaces; -using Content.Shared.Interfaces.GameObjects.Components; -using Robust.Server.GameObjects; -using Robust.Server.GameObjects.Components.UserInterface; -using Robust.Server.Interfaces.GameObjects; -using Robust.Server.Interfaces.Player; -using Robust.Shared.GameObjects; -using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -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 dropped, tangible entity. - /// - [RegisterComponent] - public class DroppedMechanismComponent : Component, IAfterInteract - { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - - public sealed override string Name => "DroppedMechanism"; - - private readonly Dictionary _optionsCache = new Dictionary(); - - private BodyManagerComponent? _bodyManagerComponentCache; - - private int _idHash; - - private IEntity? _performerCache; - - [ViewVariables] public IMechanism ContainedMechanism { get; private set; } = default!; - - [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(GenericSurgeryUiKey.Key); - - void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) - { - if (eventArgs.Target == null) - { - return; - } - - CloseAllSurgeryUIs(); - _optionsCache.Clear(); - _performerCache = null; - _bodyManagerComponentCache = null; - - if (eventArgs.Target.TryGetComponent(out var bodyManager)) - { - SendBodyPartListToUser(eventArgs, bodyManager); - } - else if (eventArgs.Target.TryGetComponent(out var droppedBodyPart)) - { - DebugTools.AssertNotNull(droppedBodyPart.ContainedBodyPart); - - if (!droppedBodyPart.ContainedBodyPart.TryInstallDroppedMechanism(this)) - { - eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("You can't fit it in!")); - } - } - } - - public override void Initialize() - { - base.Initialize(); - - if (UserInterface != null) - { - UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage; - } - } - - public void InitializeDroppedMechanism(IMechanism data) - { - ContainedMechanism = data; - Owner.Name = Loc.GetString(ContainedMechanism.Name); - - if (Owner.TryGetComponent(out SpriteComponent? component)) - { - component.LayerSetRSI(0, data.RSIPath); - component.LayerSetState(0, data.RSIState); - } - } - - public override void ExposeData(ObjectSerializer serializer) - { - // This is a temporary way to have spawnable hard-coded DroppedMechanismComponent prototypes - // In the future (when it becomes possible) DroppedMechanismComponent should be auto-generated from - // the Mechanism prototypes - var debugLoadMechanismData = ""; - base.ExposeData(serializer); - - serializer.DataField(ref debugLoadMechanismData, "debugLoadMechanismData", ""); - - if (serializer.Reading && debugLoadMechanismData != "") - { - _prototypeManager.TryIndex(debugLoadMechanismData!, out MechanismPrototype data); - - var mechanism = new Mechanism(data); - mechanism.EnsureInitialize(); - - InitializeDroppedMechanism(mechanism); - } - } - - private void SendBodyPartListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager) - { - // Create dictionary to send to client (text to be shown : data sent back if selected) - var toSend = new Dictionary(); - - foreach (var (key, value) in bodyManager.Parts) - { - // For each limb in the target, add it to our cache if it is a valid option. - if (value.CanInstallMechanism(ContainedMechanism)) - { - _optionsCache.Add(_idHash, value); - toSend.Add(key + ": " + value.Name, _idHash++); - } - } - - if (_optionsCache.Count > 0) - { - OpenSurgeryUI(eventArgs.User.GetComponent().playerSession); - UpdateSurgeryUIBodyPartRequest(eventArgs.User.GetComponent().playerSession, - toSend); - _performerCache = eventArgs.User; - _bodyManagerComponentCache = bodyManager; - } - else // If surgery cannot be performed, show message saying so. - { - eventArgs.Target.PopupMessage(eventArgs.User, - Loc.GetString("You see no way to install the {0}.", Owner.Name)); - } - } - - /// - /// Called after the client chooses from a list of possible BodyParts that can be operated on. - /// - private void HandleReceiveBodyPart(int key) - { - if (_performerCache == null || - !_performerCache.TryGetComponent(out IActorComponent? actor)) - { - return; - } - - CloseSurgeryUI(actor.playerSession); - - if (_bodyManagerComponentCache == null) - { - return; - } - - // TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc - if (!_optionsCache.TryGetValue(key, out var targetObject)) - { - _bodyManagerComponentCache.Owner.PopupMessage(_performerCache, - Loc.GetString("You see no useful way to use the {0} anymore.", Owner.Name)); - return; - } - - var target = (BodyPart) targetObject; - var message = target.TryInstallDroppedMechanism(this) - ? Loc.GetString("You jam the {0} inside {1:them}.", ContainedMechanism.Name, _performerCache) - : Loc.GetString("You can't fit it in!"); - - _bodyManagerComponentCache.Owner.PopupMessage(_performerCache, message); - - // TODO: {1:theName} - } - - private void OpenSurgeryUI(IPlayerSession session) - { - UserInterface?.Open(session); - } - - private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary options) - { - UserInterface?.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session); - } - - private void CloseSurgeryUI(IPlayerSession session) - { - UserInterface?.Close(session); - } - - private void CloseAllSurgeryUIs() - { - UserInterface?.CloseAll(); - } - - private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message) - { - switch (message.Message) - { - case ReceiveBodyPartSurgeryUIMessage msg: - HandleReceiveBodyPart(msg.SelectedOptionId); - break; - } - } - } -} diff --git a/Content.Server/GameObjects/Components/Body/IBodyManagerComponent.cs b/Content.Server/GameObjects/Components/Body/IBodyManagerComponent.cs deleted file mode 100644 index da0c41ab36..0000000000 --- a/Content.Server/GameObjects/Components/Body/IBodyManagerComponent.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using Content.Server.Body; -using Content.Server.Body.Network; -using Content.Shared.GameObjects.Components.Body; - -namespace Content.Server.GameObjects.Components.Body -{ - // TODO: Merge with ISharedBodyManagerComponent - public interface IBodyManagerComponent : ISharedBodyManagerComponent, IBodyPartManager - { - /// - /// The that this - /// is adhering to. - /// - public BodyTemplate Template { get; } - - /// - /// Installs the given into the given slot. - /// - /// True if successful, false otherwise. - bool TryAddPart(string slot, IBodyPart part, bool force = false); - - bool HasPart(string slot); - - /// - /// Ensures that this body has the specified network. - /// - /// The type of the network to ensure. - /// - /// True if the network already existed, false if it had to be created. - /// - bool EnsureNetwork() where T : BodyNetwork; - - /// - /// Ensures that this body has the specified network. - /// - /// The type of the network to ensure. - /// - /// True if the network already existed, false if it had to be created. - /// - bool EnsureNetwork(Type networkType); - - /// - /// Removes the of the given type in this body, - /// if one exists. - /// - /// The type of the network to remove. - void RemoveNetwork() where T : BodyNetwork; - - /// - /// Removes the of the given type in this body, - /// if there is one. - /// - /// The type of the network to remove. - void RemoveNetwork(Type networkType); - - void PreMetabolism(float frameTime); - - void PostMetabolism(float frameTime); - } -} diff --git a/Content.Server/GameObjects/Components/Body/MechanismComponent.cs b/Content.Server/GameObjects/Components/Body/MechanismComponent.cs new file mode 100644 index 0000000000..10629b48f5 --- /dev/null +++ b/Content.Server/GameObjects/Components/Body/MechanismComponent.cs @@ -0,0 +1,181 @@ +#nullable enable +using System.Collections.Generic; +using Content.Server.Utility; +using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.Body.Surgery; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Body +{ + [RegisterComponent] + [ComponentReference(typeof(SharedMechanismComponent))] + [ComponentReference(typeof(IMechanism))] + public class MechanismComponent : SharedMechanismComponent, IAfterInteract + { + [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key); + + public override void Initialize() + { + base.Initialize(); + + if (UserInterface != null) + { + UserInterface.OnReceiveMessage += OnUIMessage; + } + } + + void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + { + if (eventArgs.Target == null) + { + return; + } + + CloseAllSurgeryUIs(); + OptionsCache.Clear(); + PerformerCache = null; + BodyCache = null; + + if (eventArgs.Target.TryGetBody(out var body)) + { + SendBodyPartListToUser(eventArgs, body); + } + else if (eventArgs.Target.TryGetComponent(out var part)) + { + DebugTools.AssertNotNull(part); + + if (!part.TryAddMechanism(this)) + { + eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("You can't fit it in!")); + } + } + } + + private void SendBodyPartListToUser(AfterInteractEventArgs eventArgs, IBody body) + { + // Create dictionary to send to client (text to be shown : data sent back if selected) + var toSend = new Dictionary(); + + foreach (var (key, value) in body.Parts) + { + // For each limb in the target, add it to our cache if it is a valid option. + if (value.CanAddMechanism(this)) + { + OptionsCache.Add(IdHash, value); + toSend.Add(key + ": " + value.Name, IdHash++); + } + } + + if (OptionsCache.Count > 0 && + eventArgs.User.TryGetComponent(out IActorComponent? actor)) + { + OpenSurgeryUI(actor.playerSession); + UpdateSurgeryUIBodyPartRequest(actor.playerSession, toSend); + PerformerCache = eventArgs.User; + BodyCache = body; + } + else // If surgery cannot be performed, show message saying so. + { + eventArgs.Target.PopupMessage(eventArgs.User, + Loc.GetString("You see no way to install the {0}.", Owner.Name)); + } + } + + /// + /// Called after the client chooses from a list of possible BodyParts that can be operated on. + /// + private void HandleReceiveBodyPart(int key) + { + if (PerformerCache == null || + !PerformerCache.TryGetComponent(out IActorComponent? actor)) + { + return; + } + + CloseSurgeryUI(actor.playerSession); + + if (BodyCache == null) + { + return; + } + + // TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc + if (!OptionsCache.TryGetValue(key, out var targetObject)) + { + BodyCache.Owner.PopupMessage(PerformerCache, + Loc.GetString("You see no useful way to use the {0} anymore.", Owner.Name)); + return; + } + + var target = (IBodyPart) targetObject; + var message = target.TryAddMechanism(this) + ? Loc.GetString("You jam {0:theName} inside {1:them}.", Owner, PerformerCache) + : Loc.GetString("You can't fit it in!"); + + BodyCache.Owner.PopupMessage(PerformerCache, message); + + // TODO: {1:theName} + } + + private void OpenSurgeryUI(IPlayerSession session) + { + UserInterface?.Open(session); + } + + private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary options) + { + UserInterface?.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session); + } + + private void CloseSurgeryUI(IPlayerSession session) + { + UserInterface?.Close(session); + } + + private void CloseAllSurgeryUIs() + { + UserInterface?.CloseAll(); + } + + private void OnUIMessage(ServerBoundUserInterfaceMessage message) + { + switch (message.Message) + { + case ReceiveBodyPartSurgeryUIMessage msg: + HandleReceiveBodyPart(msg.SelectedOptionId); + break; + } + } + + protected override void OnPartAdd(IBodyPart? old, IBodyPart current) + { + base.OnPartAdd(old, current); + + if (Owner.TryGetComponent(out SpriteComponent? sprite)) + { + sprite.Visible = false; + } + } + + protected override void OnPartRemove(IBodyPart old) + { + base.OnPartRemove(old); + + if (Owner.TryGetComponent(out SpriteComponent? sprite)) + { + sprite.Visible = true; + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Body/DroppedBodyPartComponent.cs b/Content.Server/GameObjects/Components/Body/Part/BodyPartComponent.cs similarity index 51% rename from Content.Server/GameObjects/Components/Body/DroppedBodyPartComponent.cs rename to Content.Server/GameObjects/Components/Body/Part/BodyPartComponent.cs index db1ca227d4..9ad2418339 100644 --- a/Content.Server/GameObjects/Components/Body/DroppedBodyPartComponent.cs +++ b/Content.Server/GameObjects/Components/Body/Part/BodyPartComponent.cs @@ -1,9 +1,11 @@ #nullable enable using System.Collections.Generic; using System.Linq; -using Content.Server.Body; using Content.Server.Utility; -using Content.Shared.Body.Surgery; +using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.Body.Surgery; using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; using Robust.Server.GameObjects; @@ -13,42 +15,43 @@ using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Localization; +using Robust.Shared.Log; using Robust.Shared.ViewVariables; -namespace Content.Server.GameObjects.Components.Body +namespace Content.Server.GameObjects.Components.Body.Part { - /// - /// Component representing a dropped, tangible entity. - /// [RegisterComponent] - public class DroppedBodyPartComponent : Component, IAfterInteract, IBodyPartContainer + [ComponentReference(typeof(SharedBodyPartComponent))] + [ComponentReference(typeof(IBodyPart))] + public class BodyPartComponent : SharedBodyPartComponent, IAfterInteract { private readonly Dictionary _optionsCache = new Dictionary(); - private BodyManagerComponent? _bodyManagerComponentCache; + + private IBody? _owningBodyCache; + private int _idHash; - private IEntity? _performerCache; - public sealed override string Name => "DroppedBodyPart"; + private IEntity? _surgeonCache; - [ViewVariables] public BodyPart ContainedBodyPart { get; private set; } = default!; + [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key); - [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(GenericSurgeryUiKey.Key); - - void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + protected override void OnAddMechanism(IMechanism mechanism) { - if (eventArgs.Target == null) + base.OnAddMechanism(mechanism); + + if (mechanism.Owner.TryGetComponent(out SpriteComponent? sprite)) { - return; + sprite.Visible = false; } + } - CloseAllSurgeryUIs(); - _optionsCache.Clear(); - _performerCache = null; - _bodyManagerComponentCache = null; + protected override void OnRemoveMechanism(IMechanism mechanism) + { + base.OnRemoveMechanism(mechanism); - if (eventArgs.Target.TryGetComponent(out BodyManagerComponent? bodyManager)) + if (mechanism.Owner.TryGetComponent(out SpriteComponent? sprite)) { - SendBodySlotListToUser(eventArgs, bodyManager); + sprite.Visible = true; } } @@ -56,49 +59,77 @@ namespace Content.Server.GameObjects.Components.Body { base.Initialize(); + // This is ran in Startup as entities spawned in Initialize + // are not synced to the client since they are assumed to be + // identical on it + foreach (var mechanismId in MechanismIds) + { + var entity = Owner.EntityManager.SpawnEntity(mechanismId, Owner.Transform.MapPosition); + + if (!entity.TryGetComponent(out IMechanism? mechanism)) + { + Logger.Error($"Entity {mechanismId} does not have a {nameof(IMechanism)} component."); + continue; + } + + TryAddMechanism(mechanism, true); + } + } + + protected override void Startup() + { + base.Startup(); + if (UserInterface != null) { - UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage; + UserInterface.OnReceiveMessage += OnUIMessage; } - } - public void TransferBodyPartData(BodyPart data) - { - ContainedBodyPart = data; - Owner.Name = Loc.GetString(ContainedBodyPart.Name); - - if (Owner.TryGetComponent(out SpriteComponent? component)) + foreach (var mechanism in Mechanisms) { - component.LayerSetRSI(0, data.RSIPath); - component.LayerSetState(0, data.RSIState); - - if (data.RSIColor.HasValue) - { - component.LayerSetColor(0, data.RSIColor.Value); - } + mechanism.Dirty(); } } - private void SendBodySlotListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager) + public void AfterInteract(AfterInteractEventArgs eventArgs) + { + // TODO BODY + if (eventArgs.Target == null) + { + return; + } + + CloseAllSurgeryUIs(); + _optionsCache.Clear(); + _surgeonCache = null; + _owningBodyCache = null; + + if (eventArgs.Target.TryGetBody(out var body)) + { + SendSlots(eventArgs, body); + } + } + + private void SendSlots(AfterInteractEventArgs eventArgs, IBody body) { // Create dictionary to send to client (text to be shown : data sent back if selected) var toSend = new Dictionary(); // Here we are trying to grab a list of all empty BodySlots adjacent to an existing BodyPart that can be // attached to. i.e. an empty left hand slot, connected to an occupied left arm slot would be valid. - var unoccupiedSlots = bodyManager.AllSlots.ToList().Except(bodyManager.OccupiedSlots.ToList()).ToList(); + var unoccupiedSlots = body.Slots.Keys.ToList().Except(body.Parts.Keys.ToList()).ToList(); foreach (var slot in unoccupiedSlots) { - if (!bodyManager.TryGetSlotType(slot, out var typeResult) || - typeResult != ContainedBodyPart?.PartType || - !bodyManager.TryGetPartConnections(slot, out var parts)) + if (!body.TryGetSlotType(slot, out var typeResult) || + typeResult != PartType || + !body.TryGetPartConnections(slot, out var parts)) { continue; } foreach (var connectedPart in parts) { - if (!connectedPart.CanAttachPart(ContainedBodyPart)) + if (!connectedPart.CanAttachPart(this)) { continue; } @@ -111,10 +142,10 @@ namespace Content.Server.GameObjects.Components.Body if (_optionsCache.Count > 0) { OpenSurgeryUI(eventArgs.User.GetComponent().playerSession); - UpdateSurgeryUIBodyPartSlotRequest(eventArgs.User.GetComponent().playerSession, + BodyPartSlotRequest(eventArgs.User.GetComponent().playerSession, toSend); - _performerCache = eventArgs.User; - _bodyManagerComponentCache = bodyManager; + _surgeonCache = eventArgs.User; + _owningBodyCache = body; } else // If surgery cannot be performed, show message saying so. { @@ -124,19 +155,20 @@ namespace Content.Server.GameObjects.Components.Body } /// - /// Called after the client chooses from a list of possible BodyPartSlots to install the limb on. + /// Called after the client chooses from a list of possible + /// BodyPartSlots to install the limb on. /// - private void HandleReceiveBodyPartSlot(int key) + private void ReceiveBodyPartSlot(int key) { - if (_performerCache == null || - !_performerCache.TryGetComponent(out IActorComponent? actor)) + if (_surgeonCache == null || + !_surgeonCache.TryGetComponent(out IActorComponent? actor)) { return; } CloseSurgeryUI(actor.playerSession); - if (_bodyManagerComponentCache == null) + if (_owningBodyCache == null) { return; } @@ -144,23 +176,16 @@ namespace Content.Server.GameObjects.Components.Body // TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc if (!_optionsCache.TryGetValue(key, out var targetObject)) { - _bodyManagerComponentCache.Owner.PopupMessage(_performerCache, + _owningBodyCache.Owner.PopupMessage(_surgeonCache, Loc.GetString("You see no useful way to attach {0:theName} anymore.", Owner)); } var target = (string) targetObject!; - string message; + var message = _owningBodyCache.TryAddPart(target, this) + ? Loc.GetString("You attach {0:theName}.", Owner) + : Loc.GetString("You can't attach {0:theName}!", Owner); - if (_bodyManagerComponentCache.TryAddPart(target, this)) - { - message = Loc.GetString("You attach {0:theName}.", ContainedBodyPart); - } - else - { - message = Loc.GetString("You can't attach it!"); - } - - _bodyManagerComponentCache.Owner.PopupMessage(_performerCache, message); + _owningBodyCache.Owner.PopupMessage(_surgeonCache, message); } private void OpenSurgeryUI(IPlayerSession session) @@ -168,7 +193,7 @@ namespace Content.Server.GameObjects.Components.Body UserInterface?.Open(session); } - private void UpdateSurgeryUIBodyPartSlotRequest(IPlayerSession session, Dictionary options) + private void BodyPartSlotRequest(IPlayerSession session, Dictionary options) { UserInterface?.SendMessage(new RequestBodyPartSlotSurgeryUIMessage(options), session); } @@ -183,12 +208,12 @@ namespace Content.Server.GameObjects.Components.Body UserInterface?.CloseAll(); } - private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message) + private void OnUIMessage(ServerBoundUserInterfaceMessage message) { switch (message.Message) { case ReceiveBodyPartSlotSurgeryUIMessage msg: - HandleReceiveBodyPartSlot(msg.SelectedOptionId); + ReceiveBodyPartSlot(msg.SelectedOptionId); break; } } diff --git a/Content.Server/GameObjects/Components/Body/SurgeryToolComponent.cs b/Content.Server/GameObjects/Components/Body/SurgeryToolComponent.cs index 0a5d5136f7..b75729aa61 100644 --- a/Content.Server/GameObjects/Components/Body/SurgeryToolComponent.cs +++ b/Content.Server/GameObjects/Components/Body/SurgeryToolComponent.cs @@ -1,13 +1,12 @@ #nullable enable using System; using System.Collections.Generic; -using Content.Server.Body; -using Content.Server.Body.Mechanisms; -using Content.Server.Body.Surgery; using Content.Server.Utility; -using Content.Shared.Body.Surgery; using Content.Shared.GameObjects; using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.Body.Surgery; using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; using Robust.Server.GameObjects; @@ -19,13 +18,10 @@ using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Serialization; -using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Body { - // TODO: add checks to close UI if user walks too far away from tool or target. - /// /// Server-side component representing a generic tool capable of performing surgery. /// For instance, the scalpel. @@ -40,7 +36,7 @@ namespace Content.Server.GameObjects.Components.Body private float _baseOperateTime; - private BodyManagerComponent? _bodyManagerComponentCache; + private IBody? _bodyCache; private ISurgeon.MechanismRequestCallback? _callbackCache; @@ -50,7 +46,7 @@ namespace Content.Server.GameObjects.Components.Body private SurgeryType _surgeryType; - [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(GenericSurgeryUiKey.Key); + [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key); void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { @@ -68,11 +64,11 @@ namespace Content.Server.GameObjects.Components.Body _optionsCache.Clear(); _performerCache = null; - _bodyManagerComponentCache = null; + _bodyCache = null; _callbackCache = null; - // Attempt surgery on a BodyManagerComponent by sending a list of operable BodyParts to the client to choose from - if (eventArgs.Target.TryGetComponent(out BodyManagerComponent? body)) + // Attempt surgery on a body by sending a list of operable parts for the client to choose from + if (eventArgs.Target.TryGetBody(out var body)) { // Create dictionary to send to client (text to be shown : data sent back if selected) var toSend = new Dictionary(); @@ -92,36 +88,34 @@ namespace Content.Server.GameObjects.Components.Body OpenSurgeryUI(actor.playerSession); UpdateSurgeryUIBodyPartRequest(actor.playerSession, toSend); _performerCache = eventArgs.User; // Also, cache the data. - _bodyManagerComponentCache = body; + _bodyCache = body; } else // If surgery cannot be performed, show message saying so. { - SendNoUsefulWayToUsePopup(); + NotUsefulPopup(); } } - else if (eventArgs.Target.TryGetComponent(out var droppedBodyPart)) + else if (eventArgs.Target.TryGetComponent(out var part)) { // Attempt surgery on a DroppedBodyPart - there's only one possible target so no need for selection UI _performerCache = eventArgs.User; - DebugTools.AssertNotNull(droppedBodyPart.ContainedBodyPart); - // If surgery can be performed... - if (!droppedBodyPart.ContainedBodyPart.SurgeryCheck(_surgeryType)) + if (!part.SurgeryCheck(_surgeryType)) { - SendNoUsefulWayToUsePopup(); + NotUsefulPopup(); return; } - //...do the surgery. - if (droppedBodyPart.ContainedBodyPart.AttemptSurgery(_surgeryType, droppedBodyPart, this, + // ...do the surgery. + if (part.AttemptSurgery(_surgeryType, part, this, eventArgs.User)) { return; } // Log error if the surgery fails somehow. - Logger.Debug($"Error when trying to perform surgery on ${nameof(BodyPart)} {eventArgs.User.Name}"); + Logger.Debug($"Error when trying to perform surgery on ${nameof(IBodyPart)} {eventArgs.User.Name}"); throw new InvalidOperationException(); } } @@ -161,6 +155,7 @@ namespace Content.Server.GameObjects.Components.Body } } + // TODO BODY add checks to close UI if user walks too far away from tool or target. private void OpenSurgeryUI(IPlayerSession session) { UserInterface?.Open(session); @@ -201,7 +196,7 @@ namespace Content.Server.GameObjects.Components.Body /// /// Called after the client chooses from a list of possible - /// that can be operated on. + /// that can be operated on. /// private void HandleReceiveBodyPart(int key) { @@ -214,17 +209,18 @@ namespace Content.Server.GameObjects.Components.Body CloseSurgeryUI(actor.playerSession); // TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc if (!_optionsCache.TryGetValue(key, out var targetObject) || - _bodyManagerComponentCache == null) + _bodyCache == null) { - SendNoUsefulWayToUseAnymorePopup(); + NotUsefulAnymorePopup(); return; } - var target = (BodyPart) targetObject!; + var target = (IBodyPart) targetObject!; - if (!target.AttemptSurgery(_surgeryType, _bodyManagerComponentCache, this, _performerCache)) + // TODO BODY Reconsider + if (!target.AttemptSurgery(_surgeryType, _bodyCache, this, _performerCache)) { - SendNoUsefulWayToUseAnymorePopup(); + NotUsefulAnymorePopup(); } } @@ -239,25 +235,25 @@ namespace Content.Server.GameObjects.Components.Body _performerCache == null || !_performerCache.TryGetComponent(out IActorComponent? actor)) { - SendNoUsefulWayToUseAnymorePopup(); + NotUsefulAnymorePopup(); return; } - var target = targetObject as Mechanism; + var target = targetObject as MechanismComponent; CloseSurgeryUI(actor.playerSession); - _callbackCache?.Invoke(target, _bodyManagerComponentCache, this, _performerCache); + _callbackCache?.Invoke(target, _bodyCache, this, _performerCache); } - private void SendNoUsefulWayToUsePopup() + private void NotUsefulPopup() { - _bodyManagerComponentCache?.Owner.PopupMessage(_performerCache, + _bodyCache?.Owner.PopupMessage(_performerCache, Loc.GetString("You see no useful way to use {0:theName}.", Owner)); } - private void SendNoUsefulWayToUseAnymorePopup() + private void NotUsefulAnymorePopup() { - _bodyManagerComponentCache?.Owner.PopupMessage(_performerCache, + _bodyCache?.Owner.PopupMessage(_performerCache, Loc.GetString("You see no useful way to use {0:theName} anymore.", Owner)); } diff --git a/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs b/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs index fda8e43720..d27afd370c 100644 --- a/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs @@ -1,7 +1,9 @@ -using Content.Server.GameObjects.Components.Body.Digestive; +using System.Linq; +using Content.Server.GameObjects.Components.Body.Behavior; using Content.Server.GameObjects.Components.Nutrition; using Content.Server.GameObjects.Components.Utensil; using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Body.Mechanism; using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Utility; @@ -80,7 +82,7 @@ namespace Content.Server.GameObjects.Components.Chemistry var trueTarget = target ?? user; - if (!trueTarget.TryGetComponent(out StomachComponent stomach)) + if (!trueTarget.TryGetMechanismBehaviors(out var stomachs)) { return false; } @@ -93,7 +95,9 @@ namespace Content.Server.GameObjects.Components.Chemistry var transferAmount = ReagentUnit.Min(_transferAmount, _contents.CurrentVolume); var split = _contents.SplitSolution(transferAmount); - if (!stomach.CanTransferSolution(split)) + var firstStomach = stomachs.FirstOrDefault(stomach => stomach.CanTransferSolution(split)); + + if (firstStomach == null) { _contents.TryAddSolution(split); trueTarget.PopupMessage(user, Loc.GetString("You can't eat any more!")); @@ -108,7 +112,7 @@ namespace Content.Server.GameObjects.Components.Chemistry split.RemoveReagent(reagentId, reagent.ReactionEntity(trueTarget, ReactionMethod.Ingestion, quantity)); } - stomach.TryTransferSolution(split); + firstStomach.TryTransferSolution(split); if (_useSound != null) { diff --git a/Content.Server/GameObjects/Components/Chemistry/RehydratableComponent.cs b/Content.Server/GameObjects/Components/Chemistry/RehydratableComponent.cs index f727d00616..2dff3ccaf9 100644 --- a/Content.Server/GameObjects/Components/Chemistry/RehydratableComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/RehydratableComponent.cs @@ -1,29 +1,12 @@ #nullable enable -using System; -using System.Collections.Generic; -using Content.Server.GameObjects.Components.Body.Digestive; -using Content.Server.GameObjects.Components.Chemistry; -using Content.Server.GameObjects.Components.GUI; -using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.EntitySystems; using Content.Server.Utility; using Content.Shared.Chemistry; -using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; -using Content.Shared.Utility; -using Robust.Server.GameObjects.EntitySystems; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; using Robust.Shared.GameObjects; -using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.Interfaces.GameObjects.Components; -using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; -using Robust.Shared.Log; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; namespace Content.Server.GameObjects.Components.Chemistry { diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs index db41dc70a3..4df3f02a24 100644 --- a/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs @@ -27,6 +27,7 @@ namespace Content.Server.GameObjects.Components.Chemistry /// ECS component that manages a liquid solution of reagents. /// [RegisterComponent] + [ComponentReference(typeof(SharedSolutionContainerComponent))] public class SolutionContainerComponent : SharedSolutionContainerComponent, IExamine { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -42,42 +43,12 @@ namespace Content.Server.GameObjects.Components.Chemistry private ChemistrySystem _chemistrySystem; private SpriteComponent _spriteComponent; - /// - /// The total volume of all the of the reagents in the container. - /// - [ViewVariables] - public ReagentUnit CurrentVolume => Solution.TotalVolume; - /// /// The volume without reagents remaining in the container. /// [ViewVariables] public ReagentUnit EmptyVolume => MaxVolume - CurrentVolume; - /// - /// The current blended color of all the reagents in the container. - /// - [ViewVariables(VVAccess.ReadWrite)] - public Color SubstanceColor { get; private set; } - - /// - /// The current capabilities of this container (is the top open to pour? can I inject it into another object?). - /// - [ViewVariables(VVAccess.ReadWrite)] - public SolutionContainerCaps Capabilities { get; set; } - - /// - /// The contained solution. - /// - [ViewVariables] - public Solution Solution { get; set; } - - /// - /// The maximum volume of the container. - /// - [ViewVariables(VVAccess.ReadWrite)] - public ReagentUnit MaxVolume { get; set; } - public IReadOnlyList ReagentList => Solution.Contents; public bool CanExamineContents => (Capabilities & SolutionContainerCaps.NoExamine) == 0; public bool CanUseWithChemDispenser => (Capabilities & SolutionContainerCaps.FitsInDispenser) != 0; @@ -124,7 +95,7 @@ namespace Content.Server.GameObjects.Components.Chemistry OnSolutionChanged(false); } - public bool TryRemoveReagent(string reagentId, ReagentUnit quantity) + public override bool TryRemoveReagent(string reagentId, ReagentUnit quantity) { if (!ContainsReagent(reagentId, out var currentQuantity)) { @@ -393,12 +364,12 @@ namespace Content.Server.GameObjects.Components.Chemistry return true; } - public bool CanAddSolution(Solution solution) + public override bool CanAddSolution(Solution solution) { return solution.TotalVolume <= (MaxVolume - Solution.TotalVolume); } - public bool TryAddSolution(Solution solution, bool skipReactionCheck = false, bool skipColor = false) + public override bool TryAddSolution(Solution solution, bool skipReactionCheck = false, bool skipColor = false) { if (!CanAddSolution(solution)) return false; diff --git a/Content.Server/GameObjects/Components/Damage/BreakableComponent.cs b/Content.Server/GameObjects/Components/Damage/BreakableComponent.cs index 994373dde3..7be9a71b50 100644 --- a/Content.Server/GameObjects/Components/Damage/BreakableComponent.cs +++ b/Content.Server/GameObjects/Components/Damage/BreakableComponent.cs @@ -27,13 +27,10 @@ namespace Content.Server.GameObjects.Components.Damage public override string Name => "Breakable"; private ActSystem _actSystem; - private DamageState _currentDamageState; public override List SupportedDamageStates => new List {DamageState.Alive, DamageState.Dead}; - public override DamageState CurrentDamageState => _currentDamageState; - void IExAct.OnExplosion(ExplosionEventArgs eventArgs) { switch (eventArgs.Severity) @@ -62,7 +59,7 @@ namespace Content.Server.GameObjects.Components.Damage public void FixAllDamage() { Heal(); - _currentDamageState = DamageState.Alive; + CurrentState = DamageState.Alive; } protected override void DestructionBehavior() diff --git a/Content.Server/GameObjects/Components/Damage/RuinableComponent.cs b/Content.Server/GameObjects/Components/Damage/RuinableComponent.cs index 16dbb09af4..421574f8a5 100644 --- a/Content.Server/GameObjects/Components/Damage/RuinableComponent.cs +++ b/Content.Server/GameObjects/Components/Damage/RuinableComponent.cs @@ -5,6 +5,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Damage { @@ -15,18 +16,15 @@ namespace Content.Server.GameObjects.Components.Damage [ComponentReference(typeof(IDamageableComponent))] public abstract class RuinableComponent : DamageableComponent { - private DamageState _currentDamageState; - /// /// Sound played upon destruction. /// + [ViewVariables] protected string DestroySound { get; private set; } public override List SupportedDamageStates => new List {DamageState.Alive, DamageState.Dead}; - public override DamageState CurrentDamageState => _currentDamageState; - public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); @@ -34,8 +32,16 @@ namespace Content.Server.GameObjects.Components.Damage serializer.DataReadWriteFunction( "deadThreshold", 100, - t => DeadThreshold = t , - () => DeadThreshold ?? -1); + t => + { + if (t == null) + { + return; + } + + Thresholds[DamageState.Dead] = t.Value; + }, + () => Thresholds.TryGetValue(DamageState.Dead, out var value) ? value : (int?) null); serializer.DataField(this, ruinable => ruinable.DestroySound, "destroySound", string.Empty); } @@ -52,12 +58,12 @@ namespace Content.Server.GameObjects.Components.Damage /// /// Destroys the Owner , setting - /// to - /// + /// to + /// /// protected void PerformDestruction() { - _currentDamageState = DamageState.Dead; + CurrentState = DamageState.Dead; if (!Owner.Deleted && DestroySound != string.Empty) { diff --git a/Content.Server/GameObjects/Components/Disposal/DisposalHolderComponent.cs b/Content.Server/GameObjects/Components/Disposal/DisposalHolderComponent.cs index d9b890d53d..e188366d01 100644 --- a/Content.Server/GameObjects/Components/Disposal/DisposalHolderComponent.cs +++ b/Content.Server/GameObjects/Components/Disposal/DisposalHolderComponent.cs @@ -63,7 +63,7 @@ namespace Content.Server.GameObjects.Components.Disposal } return entity.HasComponent() || - entity.HasComponent(); + entity.HasComponent(); } public bool TryInsert(IEntity entity) diff --git a/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs b/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs index f1ecab7456..10e0573a0a 100644 --- a/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs +++ b/Content.Server/GameObjects/Components/Disposal/DisposalUnitComponent.cs @@ -145,7 +145,7 @@ namespace Content.Server.GameObjects.Components.Disposal } if (!entity.HasComponent() && - !entity.HasComponent()) + !entity.HasComponent()) { return false; } diff --git a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs index 846c27740e..6dbadb01f1 100644 --- a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs +++ b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs @@ -101,7 +101,7 @@ namespace Content.Server.GameObjects.Components.Doors /// Whether something is currently using a welder on this so DoAfter isn't spammed. /// private bool _beingWelded = false; - + [ViewVariables(VVAccess.ReadWrite)] private bool _canCrush = true; @@ -147,7 +147,7 @@ namespace Content.Server.GameObjects.Components.Doors // Disabled because it makes it suck hard to walk through double doors. - if (entity.HasComponent()) + if (entity.HasComponent()) { if (!entity.TryGetComponent(out var mover)) return; @@ -315,7 +315,7 @@ namespace Content.Server.GameObjects.Components.Doors damage.ChangeDamage(DamageType.Blunt, DoorCrushDamage, false, Owner); stun.Paralyze(DoorStunTime); - + // If we hit someone, open up after stun (opens right when stun ends) Timer.Spawn(TimeSpan.FromSeconds(DoorStunTime) - OpenTimeOne - OpenTimeTwo, Open); break; @@ -479,7 +479,7 @@ namespace Content.Server.GameObjects.Components.Doors if (_beingWelded) return false; - + _beingWelded = true; if (!await tool.UseTool(eventArgs.User, Owner, 3f, ToolQuality.Welding, 3f, () => _canWeldShut)) @@ -487,7 +487,7 @@ namespace Content.Server.GameObjects.Components.Doors _beingWelded = false; return false; } - + _beingWelded = false; IsWeldedShut ^= true; return true; diff --git a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs index c5694eb0b7..afe8a16664 100644 --- a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs +++ b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs @@ -7,9 +7,9 @@ using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Movement; using Content.Server.GameObjects.EntitySystems.Click; -using Content.Server.Interfaces.GameObjects.Components.Interaction; using Content.Server.Interfaces.GameObjects.Components.Items; using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Items; using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.EntitySystems; @@ -725,24 +725,24 @@ namespace Content.Server.GameObjects.Components.GUI } } - void IBodyPartAdded.BodyPartAdded(BodyPartAddedEventArgs eventArgs) + void IBodyPartAdded.BodyPartAdded(BodyPartAddedEventArgs args) { - if (eventArgs.Part.PartType != BodyPartType.Hand) + if (args.Part.PartType != BodyPartType.Hand) { return; } - AddHand(eventArgs.SlotName); + AddHand(args.Slot); } - void IBodyPartRemoved.BodyPartRemoved(BodyPartRemovedEventArgs eventArgs) + void IBodyPartRemoved.BodyPartRemoved(BodyPartRemovedEventArgs args) { - if (eventArgs.Part.PartType != BodyPartType.Hand) + if (args.Part.PartType != BodyPartType.Hand) { return; } - RemoveHand(eventArgs.SlotName); + RemoveHand(args.Slot); } } diff --git a/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs b/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs index 150044b6d3..175c174aec 100644 --- a/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs +++ b/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs @@ -54,7 +54,7 @@ namespace Content.Server.GameObjects.Components.Interactable public string? WeldSoundCollection { get; set; } [ViewVariables] - public float Fuel => _solutionComponent?.Solution.GetReagentQuantity("chem.WeldingFuel").Float() ?? 0f; + public float Fuel => _solutionComponent?.Solution?.GetReagentQuantity("chem.WeldingFuel").Float() ?? 0f; [ViewVariables] public float FuelCapacity => _solutionComponent?.MaxVolume.Float() ?? 0f; diff --git a/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs b/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs index 2e319fa31b..50dc1de6fa 100644 --- a/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs +++ b/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Content.Server.GameObjects.Components.Body; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Interactable; +using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Interactable; using Content.Shared.GameObjects.Components.Storage; using Content.Shared.GameObjects.EntitySystems; @@ -171,7 +172,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage // only items that can be stored in an inventory, or a mob, can be eaten by a locker if (!entity.HasComponent() && - !entity.HasComponent()) + !entity.HasComponent()) continue; if (!AddToContents(entity)) diff --git a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs index 5ea353c255..c8ef0f671d 100644 --- a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs +++ b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Content.Server.GameObjects.Components.Body; using Content.Server.GameObjects.Components.Chemistry; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Items.Storage; @@ -14,6 +13,7 @@ using Content.Server.Interfaces.GameObjects; using Content.Server.Utility; using Content.Shared.Chemistry; using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Power; using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; @@ -477,27 +477,37 @@ namespace Content.Server.GameObjects.Components.Kitchen public SuicideKind Suicide(IEntity victim, IChatManager chat) { var headCount = 0; - if (victim.TryGetComponent(out var bodyManagerComponent)) + + if (victim.TryGetComponent(out var body)) { - var heads = bodyManagerComponent.GetPartsOfType(BodyPartType.Head); + var heads = body.GetPartsOfType(BodyPartType.Head); foreach (var head in heads) { - var droppedHead = bodyManagerComponent.DropPart(head); - - if (droppedHead == null) + if (!body.TryDropPart(head, out var dropped)) { continue; } - _storage.Insert(droppedHead); - headCount++; + var droppedHeads = dropped.Where(p => p.PartType == BodyPartType.Head); + + foreach (var droppedHead in droppedHeads) + { + _storage.Insert(droppedHead.Owner); + headCount++; + } } } - var othersMessage = Loc.GetString("{0:theName} is trying to cook {0:their} head!", victim); + var othersMessage = headCount > 1 + ? Loc.GetString("{0:theName} is trying to cook {0:their} heads!", victim) + : Loc.GetString("{0:theName} is trying to cook {0:their} head!", victim); + victim.PopupMessageOtherClients(othersMessage); - var selfMessage = Loc.GetString("You cook your head!"); + var selfMessage = headCount > 1 + ? Loc.GetString("You cook your heads!") + : Loc.GetString("You cook your head!"); + victim.PopupMessage(selfMessage); _currentCookTimerTime = 10; diff --git a/Content.Server/GameObjects/Components/Medical/CloningPodComponent.cs b/Content.Server/GameObjects/Components/Medical/CloningPodComponent.cs index 496c9f37b2..b943f6cfc1 100644 --- a/Content.Server/GameObjects/Components/Medical/CloningPodComponent.cs +++ b/Content.Server/GameObjects/Components/Medical/CloningPodComponent.cs @@ -163,7 +163,7 @@ namespace Content.Server.GameObjects.Components.Medical var dead = mind.OwnedEntity.TryGetComponent(out var damageable) && - damageable.CurrentDamageState == DamageState.Dead; + damageable.CurrentState == DamageState.Dead; if (!dead) return; diff --git a/Content.Server/GameObjects/Components/Medical/HealingComponent.cs b/Content.Server/GameObjects/Components/Medical/HealingComponent.cs index 9a827c96d4..dd7d77698d 100644 --- a/Content.Server/GameObjects/Components/Medical/HealingComponent.cs +++ b/Content.Server/GameObjects/Components/Medical/HealingComponent.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Content.Server.GameObjects.Components.Stack; using Content.Shared.Damage; -using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Utility; @@ -31,7 +31,7 @@ namespace Content.Server.GameObjects.Components.Medical return; } - if (!eventArgs.Target.TryGetComponent(out ISharedBodyManagerComponent body)) + if (!eventArgs.Target.TryGetComponent(out IDamageableComponent damageable)) { return; } @@ -55,7 +55,7 @@ namespace Content.Server.GameObjects.Components.Medical foreach (var (type, amount) in Heal) { - body.ChangeDamage(type, -amount, true); + damageable.ChangeDamage(type, -amount, true); } } } diff --git a/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs b/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs index 5a297b71c2..1dd3fc6481 100644 --- a/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs +++ b/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs @@ -9,6 +9,7 @@ using Content.Server.GameObjects.EntitySystems; using Content.Server.Players; using Content.Server.Utility; using Content.Shared.Damage; +using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Medical; using Content.Shared.GameObjects.EntitySystems; @@ -128,7 +129,7 @@ namespace Content.Server.GameObjects.Components.Medical var body = _bodyContainer.ContainedEntity; return body == null ? MedicalScannerStatus.Open - : GetStatusFromDamageState(body.GetComponent().CurrentDamageState); + : GetStatusFromDamageState(body.GetComponent().CurrentState); } return MedicalScannerStatus.Off; @@ -249,7 +250,7 @@ namespace Content.Server.GameObjects.Components.Medical public bool CanDragDropOn(DragDropEventArgs eventArgs) { - return eventArgs.Dropped.HasComponent(); + return eventArgs.Dropped.HasComponent(); } public bool DragDropOn(DragDropEventArgs eventArgs) diff --git a/Content.Server/GameObjects/Components/Metabolism/MetabolismComponent.cs b/Content.Server/GameObjects/Components/Metabolism/MetabolismComponent.cs index 655847376a..3ed2e4b325 100644 --- a/Content.Server/GameObjects/Components/Metabolism/MetabolismComponent.cs +++ b/Content.Server/GameObjects/Components/Metabolism/MetabolismComponent.cs @@ -2,12 +2,13 @@ using System.Collections.Generic; using System.Linq; using Content.Server.Atmos; +using Content.Server.GameObjects.Components.Body.Behavior; using Content.Server.GameObjects.Components.Body.Circulatory; -using Content.Server.GameObjects.Components.Body.Respiratory; using Content.Server.GameObjects.Components.Temperature; using Content.Shared.Atmos; using Content.Shared.Chemistry; using Content.Shared.Damage; +using Content.Shared.GameObjects.Components.Body.Mechanism; using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces; @@ -190,9 +191,13 @@ namespace Content.Server.GameObjects.Components.Metabolism if (bloodstreamAmount < amountNeeded) { // Panic inhale - if (Owner.TryGetComponent(out LungComponent lung)) + if (Owner.TryGetMechanismBehaviors(out List lungs)) { - lung.Gasp(); + foreach (var lung in lungs) + { + lung.Gasp(); + } + bloodstreamAmount = bloodstream.Air.GetMoles(gas); } @@ -341,7 +346,7 @@ namespace Content.Server.GameObjects.Components.Metabolism public void Update(float frameTime) { if (!Owner.TryGetComponent(out var damageable) || - damageable.CurrentDamageState == DamageState.Dead) + damageable.CurrentState == DamageState.Dead) { return; } diff --git a/Content.Server/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs b/Content.Server/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs index 0eb9196a32..239bd1b4f8 100644 --- a/Content.Server/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs @@ -1,4 +1,7 @@ +using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Mobs; +using Content.Shared.Preferences; +using Robust.Server.GameObjects; using Robust.Shared.GameObjects; namespace Content.Server.GameObjects.Components.Mobs @@ -6,6 +9,44 @@ namespace Content.Server.GameObjects.Components.Mobs [RegisterComponent] public sealed class HumanoidAppearanceComponent : SharedHumanoidAppearanceComponent { + public override HumanoidCharacterAppearance Appearance + { + get => base.Appearance; + set + { + base.Appearance = value; + if (Owner.TryGetBody(out var body)) + { + foreach (var part in body.Parts.Values) + { + if (!part.Owner.TryGetComponent(out SpriteComponent sprite)) + { + continue; + } + + sprite.Color = value.SkinColor; + } + } + } + } + + protected override void Startup() + { + base.Startup(); + + if (Appearance != null && Owner.TryGetBody(out var body)) + { + foreach (var part in body.Parts.Values) + { + if (!part.Owner.TryGetComponent(out SpriteComponent sprite)) + { + continue; + } + + sprite.Color = Appearance.SkinColor; + } + } + } } } diff --git a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs index dd929b422e..df8b8b94e3 100644 --- a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs @@ -172,7 +172,7 @@ namespace Content.Server.GameObjects.Components.Mobs var dead = Owner.TryGetComponent(out var damageable) && - damageable.CurrentDamageState == DamageState.Dead; + damageable.CurrentState == DamageState.Dead; if (!HasMind) { diff --git a/Content.Server/GameObjects/Components/Mobs/State/NormalState.cs b/Content.Server/GameObjects/Components/Mobs/State/NormalState.cs index 507139e8bc..a2416447f7 100644 --- a/Content.Server/GameObjects/Components/Mobs/State/NormalState.cs +++ b/Content.Server/GameObjects/Components/Mobs/State/NormalState.cs @@ -1,5 +1,6 @@ using Content.Server.GameObjects.Components.Body; using Content.Server.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs.State; @@ -41,26 +42,12 @@ namespace Content.Server.GameObjects.Components.Mobs.State { case RuinableComponent ruinable: { - if (ruinable.DeadThreshold == null) - { - break; - } - - var modifier = (int) (ruinable.TotalDamage / (ruinable.DeadThreshold / 7f)); - - status.ChangeStatusEffectIcon(StatusEffect.Health, - "/Textures/Interface/StatusEffects/Human/human" + modifier + ".png"); - - break; - } - case BodyManagerComponent body: - { - if (body.CriticalThreshold == null) + if (!ruinable.Thresholds.TryGetValue(DamageState.Dead, out var threshold)) { return; } - var modifier = (int) (body.TotalDamage / (body.CriticalThreshold / 7f)); + var modifier = (int) (ruinable.TotalDamage / (threshold / 7f)); status.ChangeStatusEffectIcon(StatusEffect.Health, "/Textures/Interface/StatusEffects/Human/human" + modifier + ".png"); @@ -69,8 +56,15 @@ namespace Content.Server.GameObjects.Components.Mobs.State } default: { + if (!damageable.Thresholds.TryGetValue(DamageState.Critical, out var threshold)) + { + return; + } + + var modifier = (int) (damageable.TotalDamage / (threshold / 7f)); + status.ChangeStatusEffectIcon(StatusEffect.Health, - "/Textures/Interface/StatusEffects/Human/human0.png"); + "/Textures/Interface/StatusEffects/Human/human" + modifier + ".png"); break; } } diff --git a/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs b/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs index 41e64d79d5..7af5c21450 100644 --- a/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs +++ b/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs @@ -3,6 +3,7 @@ using Content.Server.GameObjects.Components.Body; using Content.Server.GameObjects.EntitySystems.DoAfter; using Content.Server.Utility; using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Movement; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.Verbs; @@ -92,16 +93,15 @@ namespace Content.Server.GameObjects.Components.Movement return false; } - if (!user.HasComponent()) + if (!user.HasComponent() || + !user.TryGetComponent(out IBody body)) { reason = Loc.GetString("You are incapable of climbing!"); return false; } - var bodyManager = user.GetComponent(); - - if (bodyManager.GetPartsOfType(BodyPartType.Leg).Count == 0 || - bodyManager.GetPartsOfType(BodyPartType.Foot).Count == 0) + if (body.GetPartsOfType(BodyPartType.Leg).Count == 0 || + body.GetPartsOfType(BodyPartType.Foot).Count == 0) { reason = Loc.GetString("You are unable to climb!"); return false; diff --git a/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs b/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs index 4aac528138..6a7826be70 100644 --- a/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs @@ -1,9 +1,11 @@ -using Content.Server.GameObjects.Components.Body.Digestive; +using System.Linq; +using Content.Server.GameObjects.Components.Body.Behavior; using Content.Server.GameObjects.Components.Chemistry; using Content.Server.GameObjects.Components.Fluids; using Content.Server.GameObjects.EntitySystems; using Content.Shared.Audio; using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Body.Mechanism; using Content.Shared.GameObjects.Components.Nutrition; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces; @@ -149,42 +151,43 @@ namespace Content.Server.GameObjects.Components.Nutrition return false; } - if (!target.TryGetComponent(out StomachComponent stomachComponent)) + if (!target.TryGetMechanismBehaviors(out var stomachs)) { return false; } var transferAmount = ReagentUnit.Min(TransferAmount, _contents.CurrentVolume); var split = _contents.SplitSolution(transferAmount); + var firstStomach = stomachs.FirstOrDefault(stomach => stomach.CanTransferSolution(split)); - if (stomachComponent.CanTransferSolution(split)) + + // All stomach are full or can't handle whatever solution we have. + if (firstStomach == null) { - if (_useSound == null) - { - return false; - } - - EntitySystem.Get().PlayFromEntity(_useSound, target, AudioParams.Default.WithVolume(-2f)); - target.PopupMessage(Loc.GetString("Slurp")); - UpdateAppearance(); - - // TODO: Account for partial transfer. - - foreach (var (reagentId, quantity) in split.Contents) - { - if (!_prototypeManager.TryIndex(reagentId, out ReagentPrototype reagent)) continue; - split.RemoveReagent(reagentId, reagent.ReactionEntity(target, ReactionMethod.Ingestion, quantity)); - } - - stomachComponent.TryTransferSolution(split); - - return true; + _contents.TryAddSolution(split); + target.PopupMessage(Loc.GetString("You've had enough {0:theName}!", Owner)); + return false; } - // Stomach was full or can't handle whatever solution we have. - _contents.TryAddSolution(split); - target.PopupMessage(Loc.GetString("You've had enough {0:theName}!", Owner)); - return false; + if (_useSound != null) + { + EntitySystem.Get().PlayFromEntity(_useSound, target, AudioParams.Default.WithVolume(-2f)); + } + + target.PopupMessage(Loc.GetString("Slurp")); + UpdateAppearance(); + + // TODO: Account for partial transfer. + + foreach (var (reagentId, quantity) in split.Contents) + { + if (!_prototypeManager.TryIndex(reagentId, out ReagentPrototype reagent)) continue; + split.RemoveReagent(reagentId, reagent.ReactionEntity(target, ReactionMethod.Ingestion, quantity)); + } + + firstStomach.TryTransferSolution(split); + + return true; } void ILand.Land(LandEventArgs eventArgs) diff --git a/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs b/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs index ab4a048975..49d569e60b 100644 --- a/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs @@ -1,12 +1,14 @@ #nullable enable using System; using System.Collections.Generic; -using Content.Server.GameObjects.Components.Body.Digestive; +using System.Linq; using Content.Server.GameObjects.Components.Chemistry; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Utensil; using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Body.Behavior; +using Content.Shared.GameObjects.Components.Body.Mechanism; using Content.Shared.GameObjects.Components.Utensil; using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; @@ -129,7 +131,7 @@ namespace Content.Server.GameObjects.Components.Nutrition var trueTarget = target ?? user; - if (!trueTarget.TryGetComponent(out StomachComponent? stomach)) + if (!trueTarget.TryGetMechanismBehaviors(out var stomachs)) { return false; } @@ -171,7 +173,9 @@ namespace Content.Server.GameObjects.Components.Nutrition var transferAmount = ReagentUnit.Min(_transferAmount, solution.CurrentVolume); var split = solution.SplitSolution(transferAmount); - if (!stomach.CanTransferSolution(split)) + var firstStomach = stomachs.FirstOrDefault(stomach => stomach.CanTransferSolution(split)); + + if (firstStomach == null) { trueTarget.PopupMessage(user, Loc.GetString("You can't eat any more!")); return false; @@ -185,7 +189,7 @@ namespace Content.Server.GameObjects.Components.Nutrition split.RemoveReagent(reagentId, reagent.ReactionEntity(target, ReactionMethod.Ingestion, quantity)); } - stomach.TryTransferSolution(split); + firstStomach.TryTransferSolution(split); _entitySystem.GetEntitySystem() .PlayFromEntity(_useSound, trueTarget, AudioParams.Default.WithVolume(-1f)); diff --git a/Content.Server/GameObjects/Components/Nutrition/HungerComponent.cs b/Content.Server/GameObjects/Components/Nutrition/HungerComponent.cs index 5e35835cc4..9ea95624cc 100644 --- a/Content.Server/GameObjects/Components/Nutrition/HungerComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/HungerComponent.cs @@ -189,7 +189,7 @@ namespace Content.Server.GameObjects.Components.Nutrition { if (Owner.TryGetComponent(out IDamageableComponent damageable)) { - if (damageable.CurrentDamageState != DamageState.Dead) + if (damageable.CurrentState != DamageState.Dead) { damageable.ChangeDamage(DamageType.Blunt, 2, true, null); } diff --git a/Content.Server/GameObjects/Components/Nutrition/ThirstComponent.cs b/Content.Server/GameObjects/Components/Nutrition/ThirstComponent.cs index 6dec3efe75..2d294e8db6 100644 --- a/Content.Server/GameObjects/Components/Nutrition/ThirstComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/ThirstComponent.cs @@ -186,7 +186,7 @@ namespace Content.Server.GameObjects.Components.Nutrition { if (Owner.TryGetComponent(out IDamageableComponent damageable)) { - if (damageable.CurrentDamageState != DamageState.Dead) + if (damageable.CurrentState != DamageState.Dead) { damageable.ChangeDamage(DamageType.Blunt, 2, true, null); } diff --git a/Content.Server/GameObjects/Components/Recycling/RecyclerComponent.cs b/Content.Server/GameObjects/Components/Recycling/RecyclerComponent.cs index 2854c59ab7..02e9585e4d 100644 --- a/Content.Server/GameObjects/Components/Recycling/RecyclerComponent.cs +++ b/Content.Server/GameObjects/Components/Recycling/RecyclerComponent.cs @@ -66,7 +66,7 @@ namespace Content.Server.GameObjects.Components.Recycling private bool CanGib(IEntity entity) { - return entity.HasComponent() && !_safe && Powered; + return entity.HasComponent() && !_safe && Powered; } private bool CanRecycle(IEntity entity, [MaybeNullWhen(false)] out ConstructionPrototype prototype) diff --git a/Content.Server/GameObjects/Components/Suspicion/SuspicionRoleComponent.cs b/Content.Server/GameObjects/Components/Suspicion/SuspicionRoleComponent.cs index f0e33db9ed..11e05c97b4 100644 --- a/Content.Server/GameObjects/Components/Suspicion/SuspicionRoleComponent.cs +++ b/Content.Server/GameObjects/Components/Suspicion/SuspicionRoleComponent.cs @@ -61,7 +61,7 @@ namespace Content.Server.GameObjects.Components.Suspicion public bool IsDead() { return Owner.TryGetComponent(out IDamageableComponent? damageable) && - damageable.CurrentDamageState == DamageState.Dead; + damageable.CurrentState == DamageState.Dead; } public bool IsInnocent() diff --git a/Content.Server/GameObjects/EntitySystems/BodySystem.cs b/Content.Server/GameObjects/EntitySystems/BodySystem.cs deleted file mode 100644 index 32467e1844..0000000000 --- a/Content.Server/GameObjects/EntitySystems/BodySystem.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Content.Server.GameObjects.Components.Body; -using Content.Server.GameObjects.Components.Metabolism; -using JetBrains.Annotations; -using Robust.Shared.GameObjects.Systems; - -namespace Content.Server.GameObjects.EntitySystems -{ - [UsedImplicitly] - public class BodySystem : EntitySystem - { - public override void Update(float frameTime) - { - foreach (var body in ComponentManager.EntityQuery()) - { - body.PreMetabolism(frameTime); - } - - foreach (var metabolism in ComponentManager.EntityQuery()) - { - metabolism.Update(frameTime); - } - - foreach (var body in ComponentManager.EntityQuery()) - { - body.PostMetabolism(frameTime); - } - } - } -} diff --git a/Content.Server/GameObjects/EntitySystems/HeartSystem.cs b/Content.Server/GameObjects/EntitySystems/HeartSystem.cs new file mode 100644 index 0000000000..c0ef5b225f --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/HeartSystem.cs @@ -0,0 +1,28 @@ +using Content.Shared.GameObjects.Components.Body.Behavior; +using Content.Shared.GameObjects.EntitySystems; +using JetBrains.Annotations; +using Robust.Shared.GameObjects.Systems; + +namespace Content.Server.GameObjects.EntitySystems +{ + [UsedImplicitly] + public class HeartSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + UpdatesBefore.Add(typeof(SharedMetabolismSystem)); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + foreach (var heart in ComponentManager.EntityQuery()) + { + heart.Update(frameTime); + } + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/LungSystem.cs b/Content.Server/GameObjects/EntitySystems/LungSystem.cs new file mode 100644 index 0000000000..07c1414f58 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/LungSystem.cs @@ -0,0 +1,28 @@ +using Content.Shared.GameObjects.Components.Body.Behavior; +using Content.Shared.GameObjects.EntitySystems; +using JetBrains.Annotations; +using Robust.Shared.GameObjects.Systems; + +namespace Content.Server.GameObjects.EntitySystems +{ + [UsedImplicitly] + public class LungSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + UpdatesBefore.Add(typeof(SharedMetabolismSystem)); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + foreach (var lung in ComponentManager.EntityQuery()) + { + lung.Update(frameTime); + } + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/StationEvents/RadiationPulseSystem.cs b/Content.Server/GameObjects/EntitySystems/StationEvents/RadiationPulseSystem.cs index cf2abe467d..9dd45401be 100644 --- a/Content.Server/GameObjects/EntitySystems/StationEvents/RadiationPulseSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/StationEvents/RadiationPulseSystem.cs @@ -1,4 +1,5 @@ using Content.Server.GameObjects.Components.StationEvents; +using Content.Shared.GameObjects.Components.Body; using Content.Shared.Interfaces.GameObjects.Components; using JetBrains.Annotations; using Robust.Shared.GameObjects.Systems; diff --git a/Content.Server/GameTicking/GameRules/RuleDeathMatch.cs b/Content.Server/GameTicking/GameRules/RuleDeathMatch.cs index b7a680be96..03874a6d99 100644 --- a/Content.Server/GameTicking/GameRules/RuleDeathMatch.cs +++ b/Content.Server/GameTicking/GameRules/RuleDeathMatch.cs @@ -68,7 +68,7 @@ namespace Content.Server.GameTicking.GameRules continue; } - if (damageable.CurrentDamageState != DamageState.Alive) + if (damageable.CurrentState != DamageState.Alive) { continue; } diff --git a/Content.Server/GameTicking/GameRules/RuleSuspicion.cs b/Content.Server/GameTicking/GameRules/RuleSuspicion.cs index 42105ef069..a6c33264fc 100644 --- a/Content.Server/GameTicking/GameRules/RuleSuspicion.cs +++ b/Content.Server/GameTicking/GameRules/RuleSuspicion.cs @@ -73,7 +73,7 @@ namespace Content.Server.GameTicking.GameRules continue; } - if (damageable.CurrentDamageState != DamageState.Alive) + if (damageable.CurrentState != DamageState.Alive) { continue; } diff --git a/Content.Server/Interfaces/GameObjects/Components/Interaction/IBodyPartAdded.cs b/Content.Server/Interfaces/GameObjects/Components/Interaction/IBodyPartAdded.cs deleted file mode 100644 index d18fe596ee..0000000000 --- a/Content.Server/Interfaces/GameObjects/Components/Interaction/IBodyPartAdded.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using Content.Server.Body; - -namespace Content.Server.Interfaces.GameObjects.Components.Interaction -{ - /// - /// This interface gives components behavior when a body part - /// is added to their owning entity. - /// - public interface IBodyPartAdded - { - void BodyPartAdded(BodyPartAddedEventArgs eventArgs); - } - - public class BodyPartAddedEventArgs : EventArgs - { - public BodyPartAddedEventArgs(IBodyPart part, string slotName) - { - Part = part; - SlotName = slotName; - } - - public IBodyPart Part { get; } - - public string SlotName { get; } - } - - /// - /// This interface gives components behavior when a body part - /// is removed from their owning entity. - /// - public interface IBodyPartRemoved - { - void BodyPartRemoved(BodyPartRemovedEventArgs eventArgs); - } - - public class BodyPartRemovedEventArgs : EventArgs - { - public BodyPartRemovedEventArgs(IBodyPart part, string slotName) - { - Part = part; - SlotName = slotName; - } - - public IBodyPart Part { get; } - - public string SlotName { get; } - } -} diff --git a/Content.Server/Observer/Ghost.cs b/Content.Server/Observer/Ghost.cs index 2f094a9f74..8e97025f66 100644 --- a/Content.Server/Observer/Ghost.cs +++ b/Content.Server/Observer/Ghost.cs @@ -50,7 +50,7 @@ namespace Content.Server.Observer if (canReturn && player.AttachedEntity.TryGetComponent(out IDamageableComponent damageable)) { - switch (damageable.CurrentDamageState) + switch (damageable.CurrentState) { case DamageState.Dead: canReturn = true; diff --git a/Content.Server/ServerContentIoC.cs b/Content.Server/ServerContentIoC.cs index b8b7e665ef..dadb213851 100644 --- a/Content.Server/ServerContentIoC.cs +++ b/Content.Server/ServerContentIoC.cs @@ -1,6 +1,5 @@ using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; -using Content.Server.Body.Network; using Content.Server.Cargo; using Content.Server.Chat; using Content.Server.Database; @@ -43,7 +42,6 @@ namespace Content.Server IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); - IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); } diff --git a/Content.Server/StationEvents/BoltsDown.cs b/Content.Server/StationEvents/BoltsDown.cs index ec15efb6b9..ba64b41be4 100644 --- a/Content.Server/StationEvents/BoltsDown.cs +++ b/Content.Server/StationEvents/BoltsDown.cs @@ -44,7 +44,7 @@ namespace Content.Server.StationEvents if (inventory.TryGetSlotItem(EquipmentSlotDefines.Slots.BELT, out ItemComponent? item) && item?.Owner.Prototype?.ID == "UtilityBeltClothingFilledEvent") return; if (player.AttachedEntity.TryGetComponent(out var damageable) - && damageable.CurrentDamageState == DamageState.Dead) return; + && damageable.CurrentState == DamageState.Dead) return; var entityManager = IoCManager.Resolve(); var playerPos = player.AttachedEntity.Transform.Coordinates; diff --git a/Content.Shared/Body/Mechanism/MechanismPrototype.cs b/Content.Shared/Body/Mechanism/MechanismPrototype.cs deleted file mode 100644 index fc84807182..0000000000 --- a/Content.Shared/Body/Mechanism/MechanismPrototype.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using Content.Shared.GameObjects.Components.Body; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; -using Robust.Shared.ViewVariables; -using YamlDotNet.RepresentationModel; - -namespace Content.Shared.Body.Mechanism -{ - /// - /// Prototype for the Mechanism class. - /// - [Prototype("mechanism")] - [Serializable, NetSerializable] - public class MechanismPrototype : IPrototype, IIndexedPrototype - { - private List _behaviorClasses; - private BodyPartCompatibility _compatibility; - private string _description; - private int _destroyThreshold; - private int _durability; - private string _examineMessage; - private string _id; - private string _name; - private int _resistance; - private string _rsiPath; - private string _rsiState; - private int _size; - - [ViewVariables] public string Name => _name; - - [ViewVariables] public string Description => _description; - - [ViewVariables] public string ExamineMessage => _examineMessage; - - [ViewVariables] public string RSIPath => _rsiPath; - - [ViewVariables] public string RSIState => _rsiState; - - [ViewVariables] public int Durability => _durability; - - [ViewVariables] public int DestroyThreshold => _destroyThreshold; - - [ViewVariables] public int Resistance => _resistance; - - [ViewVariables] public int Size => _size; - - [ViewVariables] public BodyPartCompatibility Compatibility => _compatibility; - - [ViewVariables] public List BehaviorClasses => _behaviorClasses; - - [ViewVariables] public string ID => _id; - - public virtual void LoadFrom(YamlMappingNode mapping) - { - var serializer = YamlObjectSerializer.NewReader(mapping); - - serializer.DataField(ref _id, "id", string.Empty); - serializer.DataField(ref _name, "name", string.Empty); - serializer.DataField(ref _description, "description", string.Empty); - serializer.DataField(ref _examineMessage, "examineMessage", string.Empty); - serializer.DataField(ref _rsiPath, "rsiPath", string.Empty); - serializer.DataField(ref _rsiState, "rsiState", string.Empty); - serializer.DataField(ref _durability, "durability", 0); - serializer.DataField(ref _destroyThreshold, "destroyThreshold", 0); - serializer.DataField(ref _resistance, "resistance", 0); - serializer.DataField(ref _size, "size", 2); - serializer.DataField(ref _compatibility, "compatibility", BodyPartCompatibility.Universal); - serializer.DataField(ref _behaviorClasses, "behaviors", new List()); - } - } -} diff --git a/Content.Shared/Body/Part/BodyPartPrototype.cs b/Content.Shared/Body/Part/BodyPartPrototype.cs deleted file mode 100644 index dd3f64a544..0000000000 --- a/Content.Shared/Body/Part/BodyPartPrototype.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Content.Shared.GameObjects.Components.Body; -using Robust.Shared.Interfaces.Serialization; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; -using Robust.Shared.ViewVariables; -using YamlDotNet.RepresentationModel; - -namespace Content.Shared.Body.Part -{ - /// - /// Prototype for the BodyPart class. - /// - [Prototype("bodyPart")] - [Serializable, NetSerializable] - public class BodyPartPrototype : IPrototype, IIndexedPrototype - { - private BodyPartCompatibility _compatibility; - private string _damageContainerPresetId; - private int _destroyThreshold; - private int _durability; - private string _id; - private List _mechanisms; - private string _name; - private BodyPartType _partType; - private string _plural; - private List _properties; - private float _resistance; - private string _resistanceSetId; - private string _rsiPath; - private string _rsiState; - private int _size; - private string _surgeryDataName; - private bool _isVital; - - - [ViewVariables] public string Name => _name; - - [ViewVariables] public string Plural => _plural; - - [ViewVariables] public string RSIPath => _rsiPath; - - [ViewVariables] public string RSIState => _rsiState; - - [ViewVariables] public BodyPartType PartType => _partType; - - [ViewVariables] public int Durability => _durability; - - [ViewVariables] public int DestroyThreshold => _destroyThreshold; - - [ViewVariables] public float Resistance => _resistance; - - [ViewVariables] public int Size => _size; - - [ViewVariables] public BodyPartCompatibility Compatibility => _compatibility; - - [ViewVariables] public string DamageContainerPresetId => _damageContainerPresetId; - - [ViewVariables] public string ResistanceSetId => _resistanceSetId; - - [ViewVariables] public string SurgeryDataName => _surgeryDataName; - - [ViewVariables] public List Properties => _properties; - - [ViewVariables] public List Mechanisms => _mechanisms; - - [ViewVariables] public string ID => _id; - - [ViewVariables] public bool IsVital => _isVital; - - public virtual void LoadFrom(YamlMappingNode mapping) - { - var serializer = YamlObjectSerializer.NewReader(mapping); - - serializer.DataField(ref _name, "name", string.Empty); - serializer.DataField(ref _id, "id", string.Empty); - serializer.DataField(ref _plural, "plural", string.Empty); - serializer.DataField(ref _rsiPath, "rsiPath", string.Empty); - serializer.DataField(ref _rsiState, "rsiState", string.Empty); - serializer.DataField(ref _partType, "partType", BodyPartType.Other); - serializer.DataField(ref _surgeryDataName, "surgeryDataType", "BiologicalSurgeryData"); - serializer.DataField(ref _durability, "durability", 50); - serializer.DataField(ref _destroyThreshold, "destroyThreshold", -50); - serializer.DataField(ref _resistance, "resistance", 0f); - serializer.DataField(ref _size, "size", 0); - serializer.DataField(ref _compatibility, "compatibility", BodyPartCompatibility.Universal); - serializer.DataField(ref _damageContainerPresetId, "damageContainer", string.Empty); - serializer.DataField(ref _resistanceSetId, "resistances", string.Empty); - serializer.DataField(ref _properties, "properties", new List()); - serializer.DataField(ref _mechanisms, "mechanisms", new List()); - serializer.DataField(ref _isVital, "isVital", false); - - foreach (var property in _properties) - { - if (_properties.Count(x => x.GetType() == property.GetType()) > 1) - { - throw new InvalidOperationException( - $"Multiple {nameof(BodyPartPrototype)} of the same type were defined in prototype {ID}"); - } - } - } - } -} diff --git a/Content.Shared/Body/Part/Properties/BodyPartProperty.cs b/Content.Shared/Body/Part/Properties/BodyPartProperty.cs deleted file mode 100644 index 8da251a565..0000000000 --- a/Content.Shared/Body/Part/Properties/BodyPartProperty.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Robust.Shared.Interfaces.Serialization; -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Part.Properties -{ - /// - /// Property attachable to a . - /// For example, this is used to define the speed capabilities of a - /// leg. The movement system will look for a LegProperty on all BodyParts. - /// - public abstract class BodyPartProperty : IExposeData - { - /// - /// Whether this property is currently active. - /// - public bool Active; - - public virtual void ExposeData(ObjectSerializer serializer) - { - serializer.DataField(ref Active, "active", true); - } - } -} diff --git a/Content.Shared/Body/Part/Properties/Movement/FootProperty.cs b/Content.Shared/Body/Part/Properties/Movement/FootProperty.cs deleted file mode 100644 index 95432cbfe6..0000000000 --- a/Content.Shared/Body/Part/Properties/Movement/FootProperty.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Content.Shared.Body.Part.Properties.Movement -{ - /// - /// Defines the leg-based locomotion ability of a BodyPart. Required for humanoid-like movement. Must be connected to a - /// with - /// and to work. - /// - public class FootProperty : BodyPartProperty - { - } -} diff --git a/Content.Shared/Body/Part/Properties/Movement/LegProperty.cs b/Content.Shared/Body/Part/Properties/Movement/LegProperty.cs deleted file mode 100644 index 77ba06f888..0000000000 --- a/Content.Shared/Body/Part/Properties/Movement/LegProperty.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Part.Properties.Movement -{ - /// - /// Defines the speed of humanoid-like movement. Must be connected to a - /// with and have - /// on the same organ and down to the foot to work. - /// - public class LegProperty : BodyPartProperty - { - /// - /// Speed (in tiles per second). - /// - public float Speed; - - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - serializer.DataField(ref Speed, "speed", 1f); - } - } -} diff --git a/Content.Shared/Body/Part/Properties/Other/ExtensionProperty.cs b/Content.Shared/Body/Part/Properties/Other/ExtensionProperty.cs deleted file mode 100644 index 52d5965083..0000000000 --- a/Content.Shared/Body/Part/Properties/Other/ExtensionProperty.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Part.Properties.Other -{ - /// - /// Defines the extension ability of a BodyPart. Used to determine things like reach distance and running speed. - /// - public class ExtensionProperty : BodyPartProperty - { - /// - /// Current reach distance (in tiles). - /// - public float ReachDistance; - - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - serializer.DataField(ref ReachDistance, "reachDistance", 2f); - } - } -} diff --git a/Content.Shared/Body/Part/Properties/Other/GraspProperty.cs b/Content.Shared/Body/Part/Properties/Other/GraspProperty.cs deleted file mode 100644 index b3a774918e..0000000000 --- a/Content.Shared/Body/Part/Properties/Other/GraspProperty.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Content.Shared.Body.Part.Properties.Other -{ - /// - /// Defines the item grasping ability of a BodyPart. Distance is determined by the - /// ExtensionProperties of the current BodyPart or attached BodyParts. - /// - public class GraspProperty : BodyPartProperty - { - } -} diff --git a/Content.Shared/Body/Scanner/BodyScannerSharedValues.cs b/Content.Shared/Body/Scanner/BodyScannerSharedValues.cs deleted file mode 100644 index 83b11556ba..0000000000 --- a/Content.Shared/Body/Scanner/BodyScannerSharedValues.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Collections.Generic; -using Content.Shared.GameObjects.Components.Body; -using Robust.Shared.GameObjects.Components.UserInterface; -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Scanner -{ - [Serializable, NetSerializable] - public enum BodyScannerUiKey - { - Key - } - - [Serializable, NetSerializable] - public class BodyScannerInterfaceState : BoundUserInterfaceState - { - public readonly Dictionary Parts; - public readonly BodyScannerTemplateData Template; - - public BodyScannerInterfaceState(Dictionary parts, - BodyScannerTemplateData template) - { - Template = template; - Parts = parts; - } - } - - [Serializable, NetSerializable] - public class BodyScannerBodyPartData - { - public readonly int CurrentDurability; - public readonly int MaxDurability; - public readonly List Mechanisms; - public readonly string Name; - public readonly string RSIPath; - public readonly string RSIState; - - public BodyScannerBodyPartData(string name, string rsiPath, string rsiState, int maxDurability, - int currentDurability, List mechanisms) - { - Name = name; - RSIPath = rsiPath; - RSIState = rsiState; - MaxDurability = maxDurability; - CurrentDurability = currentDurability; - Mechanisms = mechanisms; - } - } - - [Serializable, NetSerializable] - public class BodyScannerMechanismData - { - public readonly int CurrentDurability; - public readonly string Description; - public readonly int MaxDurability; - public readonly string Name; - public readonly string RSIPath; - public readonly string RSIState; - - public BodyScannerMechanismData(string name, string description, string rsiPath, string rsiState, - int maxDurability, int currentDurability) - { - Name = name; - Description = description; - RSIPath = rsiPath; - RSIState = rsiState; - MaxDurability = maxDurability; - CurrentDurability = currentDurability; - } - } - - [Serializable, NetSerializable] - public class BodyScannerTemplateData - { - public readonly string Name; - public readonly Dictionary Slots; - - public BodyScannerTemplateData(string name, Dictionary slots) - { - Name = name; - Slots = slots; - } - } -} diff --git a/Content.Shared/Chemistry/Solution.cs b/Content.Shared/Chemistry/Solution.cs index 50eed0c8c6..13a425a61b 100644 --- a/Content.Shared/Chemistry/Solution.cs +++ b/Content.Shared/Chemistry/Solution.cs @@ -12,11 +12,13 @@ namespace Content.Shared.Chemistry /// /// A solution of reagents. /// + [Serializable, NetSerializable] public class Solution : IExposeData, IEnumerable { // Most objects on the station hold only 1 or 2 reagents [ViewVariables] private List _contents = new List(2); + public IReadOnlyList Contents => _contents; /// diff --git a/Content.Shared/Damage/DamageContainer/DamageContainer.cs b/Content.Shared/Damage/DamageContainer/DamageContainer.cs index 1f185a7c38..141b0f1d30 100644 --- a/Content.Shared/Damage/DamageContainer/DamageContainer.cs +++ b/Content.Shared/Damage/DamageContainer/DamageContainer.cs @@ -23,20 +23,12 @@ namespace Content.Shared.Damage.DamageContainer public DamageContainer(HealthChangedDelegate onHealthChanged, DamageContainerPrototype data) { + ID = data.ID; OnHealthChanged = onHealthChanged; SupportedClasses = data.ActiveDamageClasses; } - public DamageContainer(HealthChangedDelegate onHealthChanged, List supportedClasses) - { - OnHealthChanged = onHealthChanged; - SupportedClasses = supportedClasses; - } - - public DamageContainer(HealthChangedDelegate onHealthChanged) - { - OnHealthChanged = onHealthChanged; - } + public string ID { get; } [ViewVariables] public virtual List SupportedClasses { get; } diff --git a/Content.Shared/Damage/DamageContainer/DamageContainerPrototype.cs b/Content.Shared/Damage/DamageContainer/DamageContainerPrototype.cs index cde45fb999..63a67a2a0a 100644 --- a/Content.Shared/Damage/DamageContainer/DamageContainerPrototype.cs +++ b/Content.Shared/Damage/DamageContainer/DamageContainerPrototype.cs @@ -11,8 +11,7 @@ namespace Content.Shared.Damage.DamageContainer /// Prototype for the DamageContainer class. /// [Prototype("damageContainer")] - [NetSerializable] - [Serializable] + [Serializable, NetSerializable] public class DamageContainerPrototype : IPrototype, IIndexedPrototype { private List _activeDamageClasses; diff --git a/Content.Shared/Damage/ResistanceSet/ResistanceSet.cs b/Content.Shared/Damage/ResistanceSet/ResistanceSet.cs index 8aa29edf3d..b076df8fcb 100644 --- a/Content.Shared/Damage/ResistanceSet/ResistanceSet.cs +++ b/Content.Shared/Damage/ResistanceSet/ResistanceSet.cs @@ -26,9 +26,12 @@ namespace Content.Shared.Damage.ResistanceSet public ResistanceSet(ResistanceSetPrototype data) { + ID = data.ID; _resistances = data.Resistances; } + public string ID { get; } + /// /// Adjusts input damage with the resistance set values. Only applies reduction if the amount is damage (positive), not /// healing (negative). diff --git a/Content.Shared/Damage/ResistanceSet/ResistanceSetPrototype.cs b/Content.Shared/Damage/ResistanceSet/ResistanceSetPrototype.cs index 27ae9a0db7..821723053f 100644 --- a/Content.Shared/Damage/ResistanceSet/ResistanceSetPrototype.cs +++ b/Content.Shared/Damage/ResistanceSet/ResistanceSetPrototype.cs @@ -11,8 +11,7 @@ namespace Content.Shared.Damage.ResistanceSet /// Prototype for the BodyPart class. /// [Prototype("resistanceSet")] - [NetSerializable] - [Serializable] + [Serializable, NetSerializable] public class ResistanceSetPrototype : IPrototype, IIndexedPrototype { private Dictionary _coefficients; diff --git a/Content.Shared/GameObjects/Components/Body/Behavior/BrainBehaviorComponent.cs b/Content.Shared/GameObjects/Components/Body/Behavior/BrainBehaviorComponent.cs new file mode 100644 index 0000000000..706347d994 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Behavior/BrainBehaviorComponent.cs @@ -0,0 +1,15 @@ +using Robust.Shared.GameObjects; + +namespace Content.Shared.GameObjects.Components.Body.Behavior +{ + [RegisterComponent] + public class BrainBehaviorComponent : MechanismBehaviorComponent + { + public override string Name => "Brain"; + + public override void Update(float frameTime) + { + // TODO BODY + } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Behavior/IMechanismBehavior.cs b/Content.Shared/GameObjects/Components/Body/Behavior/IMechanismBehavior.cs new file mode 100644 index 0000000000..64415689bd --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Behavior/IMechanismBehavior.cs @@ -0,0 +1,19 @@ +#nullable enable +using Content.Shared.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Part; + +namespace Content.Shared.GameObjects.Components.Body.Behavior +{ + public interface IMechanismBehavior : IHasBody + { + IBodyPart? Part { get; } + + /// + /// Upward reference to the parent that this + /// behavior is attached to. + /// + IMechanism? Mechanism { get; } + + void Update(float frameTime); + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Behavior/MechanismBehaviorComponent.cs b/Content.Shared/GameObjects/Components/Body/Behavior/MechanismBehaviorComponent.cs new file mode 100644 index 0000000000..1ace5e007e --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Behavior/MechanismBehaviorComponent.cs @@ -0,0 +1,86 @@ +#nullable enable +using Content.Shared.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Part; +using Robust.Shared.GameObjects; + +namespace Content.Shared.GameObjects.Components.Body.Behavior +{ + public abstract class MechanismBehaviorComponent : Component, IMechanismBehavior + { + public IBody? Body => Part?.Body; + + public IBodyPart? Part => Mechanism?.Part; + + public IMechanism? Mechanism => Owner.GetComponentOrNull(); + + public abstract void Update(float frameTime); + + /// + /// 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 AddedToBody() + { + OnAddedToBody(); + } + + /// + /// Called when the parent is + /// added into a . + /// For instance, putting a brain into an empty head. + /// + public void AddedToPart() + { + OnAddedToPart(); + } + + /// + /// Called when the containing is removed from a + /// . + /// For instance, cutting off ones head will call this on the brain inside. + /// + public void RemovedFromBody(IBody old) + { + OnRemovedFromBody(old); + } + + /// + /// Called when the parent is + /// removed from a . + /// For instance, taking a brain out of ones head. + /// + public void RemovedFromPart(IBodyPart old) + { + OnRemovedFromPart(old); + } + + /// + /// 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 OnAddedToBody() { } + + /// + /// Called when the parent is + /// added into a . + /// For instance, putting a brain into an empty head. + /// + protected virtual void OnAddedToPart() { } + + /// + /// 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(IBody old) { } + + /// + /// Called when the parent is + /// removed from a . + /// For instance, taking a brain out of ones head. + /// + protected virtual void OnRemovedFromPart(IBodyPart old) { } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Behavior/SharedHeartBehaviorComponent.cs b/Content.Shared/GameObjects/Components/Body/Behavior/SharedHeartBehaviorComponent.cs new file mode 100644 index 0000000000..6d4f6c0f3d --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Behavior/SharedHeartBehaviorComponent.cs @@ -0,0 +1,8 @@ +#nullable enable +namespace Content.Shared.GameObjects.Components.Body.Behavior +{ + public abstract class SharedHeartBehaviorComponent : MechanismBehaviorComponent + { + public override string Name => "Heart"; + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Behavior/SharedLungBehaviorComponent.cs b/Content.Shared/GameObjects/Components/Body/Behavior/SharedLungBehaviorComponent.cs new file mode 100644 index 0000000000..dbbff4e9d9 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Behavior/SharedLungBehaviorComponent.cs @@ -0,0 +1,39 @@ +#nullable enable +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Shared.GameObjects.Components.Body.Behavior +{ + public abstract class SharedLungBehaviorComponent : MechanismBehaviorComponent + { + public override string Name => "Lung"; + + [ViewVariables] public abstract float Temperature { get; } + + [ViewVariables] public abstract float Volume { get; } + + [ViewVariables] public LungStatus Status { get; set; } + + [ViewVariables] public float CycleDelay { get; set; } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(this, l => l.CycleDelay, "cycleDelay", 2); + } + + public abstract void Inhale(float frameTime); + + public abstract void Exhale(float frameTime); + + public abstract void Gasp(); + } + + public enum LungStatus + { + None = 0, + Inhaling, + Exhaling + } +} diff --git a/Content.Server/GameObjects/Components/Body/Digestive/StomachComponent.cs b/Content.Shared/GameObjects/Components/Body/Behavior/SharedStomachBehaviorComponent.cs similarity index 65% rename from Content.Server/GameObjects/Components/Body/Digestive/StomachComponent.cs rename to Content.Shared/GameObjects/Components/Body/Behavior/SharedStomachBehaviorComponent.cs index 1a1e3cfc75..2045a1f0fa 100644 --- a/Content.Server/GameObjects/Components/Body/Digestive/StomachComponent.cs +++ b/Content.Shared/GameObjects/Components/Body/Behavior/SharedStomachBehaviorComponent.cs @@ -1,33 +1,85 @@ -#nullable enable +#nullable enable using System.Collections.Generic; using System.Linq; -using Content.Server.GameObjects.Components.Body.Circulatory; -using Content.Server.GameObjects.Components.Chemistry; using Content.Shared.Chemistry; -using Content.Shared.GameObjects.Components.Nutrition; +using Content.Shared.GameObjects.Components.Body.Networks; +using Content.Shared.GameObjects.Components.Chemistry; using Robust.Shared.GameObjects; -using Robust.Shared.Log; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; -namespace Content.Server.GameObjects.Components.Body.Digestive +namespace Content.Shared.GameObjects.Components.Body.Behavior { /// /// Where reagents go when ingested. Tracks ingested reagents over time, and - /// eventually transfers them to once digested. + /// eventually transfers them to once digested. /// - [RegisterComponent] - public class StomachComponent : SharedStomachComponent + public abstract class SharedStomachBehaviorComponent : MechanismBehaviorComponent { + public override string Name => "Stomach"; + + private float _accumulatedFrameTime; + + /// + /// Updates digestion status of ingested reagents. + /// Once reagents surpass _digestionDelay they are moved to the + /// bloodstream, where they are then metabolized. + /// + /// + /// The time since the last update in seconds. + /// + public override void Update(float frameTime) + { + if (Body == null) + { + return; + } + + _accumulatedFrameTime += frameTime; + + // Update at most once per second + if (_accumulatedFrameTime < 1) + { + return; + } + + _accumulatedFrameTime -= 1; + + if (!Body.Owner.TryGetComponent(out SharedSolutionContainerComponent? solution) || + !Body.Owner.TryGetComponent(out SharedBloodstreamComponent? bloodstream)) + { + return; + } + + // Add reagents ready for transfer to bloodstream to transferSolution + var transferSolution = new Solution(); + + // Use ToList here to remove entries while iterating + foreach (var delta in _reagentDeltas.ToList()) + { + //Increment lifetime of reagents + delta.Increment(frameTime); + if (delta.Lifetime > _digestionDelay) + { + solution.TryRemoveReagent(delta.ReagentId, delta.Quantity); + transferSolution.AddReagent(delta.ReagentId, delta.Quantity); + _reagentDeltas.Remove(delta); + } + } + + // Transfer digested reagents to bloodstream + bloodstream.TryTransferSolution(transferSolution); + } + /// /// Max volume of internal solution storage /// public ReagentUnit MaxVolume { - get => Owner.TryGetComponent(out SolutionContainerComponent? solution) ? solution.MaxVolume : ReagentUnit.Zero; + get => Owner.TryGetComponent(out SharedSolutionContainerComponent? solution) ? solution.MaxVolume : ReagentUnit.Zero; set { - if (Owner.TryGetComponent(out SolutionContainerComponent? solution)) + if (Owner.TryGetComponent(out SharedSolutionContainerComponent? solution)) { solution.MaxVolume = value; } @@ -38,11 +90,11 @@ namespace Content.Server.GameObjects.Components.Body.Digestive /// Initial internal solution storage volume /// [ViewVariables] - private ReagentUnit _initialMaxVolume; + protected ReagentUnit InitialMaxVolume { get; private set; } /// - /// Time in seconds between reagents being ingested and them being transferred - /// to + /// Time in seconds between reagents being ingested and them being + /// transferred to /// [ViewVariables] private float _digestionDelay; @@ -56,25 +108,13 @@ namespace Content.Server.GameObjects.Components.Body.Digestive public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); - serializer.DataField(ref _initialMaxVolume, "maxVolume", ReagentUnit.New(100)); + serializer.DataField(this, s => s.InitialMaxVolume, "maxVolume", ReagentUnit.New(100)); serializer.DataField(ref _digestionDelay, "digestionDelay", 20); } - protected override void Startup() - { - base.Startup(); - - if (!Owner.EnsureComponent(out SolutionContainerComponent solution)) - { - Logger.Warning($"Entity {Owner} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionContainerComponent)}"); - } - - solution.MaxVolume = _initialMaxVolume; - } - public bool CanTransferSolution(Solution solution) { - if (!Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent)) + if (!Owner.TryGetComponent(out SharedSolutionContainerComponent? solutionComponent)) { return false; } @@ -93,7 +133,10 @@ namespace Content.Server.GameObjects.Components.Body.Digestive if (!CanTransferSolution(solution)) return false; - var solutionComponent = Owner.GetComponent(); + if (!Owner.TryGetComponent(out SharedSolutionContainerComponent? solutionComponent)) + { + return false; + } // Add solution to _stomachContents solutionComponent.TryAddSolution(solution, false, true); @@ -106,44 +149,10 @@ namespace Content.Server.GameObjects.Components.Body.Digestive return true; } - /// - /// Updates digestion status of ingested reagents. - /// Once reagents surpass _digestionDelay they are moved to the bloodstream, - /// where they are then metabolized. - /// - /// The time since the last update in seconds. - public void Update(float frameTime) - { - if (!Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent) || - !Owner.TryGetComponent(out BloodstreamComponent? bloodstream)) - { - return; - } - - // Add reagents ready for transfer to bloodstream to transferSolution - var transferSolution = new Solution(); - - // Use ToList here to remove entries while iterating - foreach (var delta in _reagentDeltas.ToList()) - { - //Increment lifetime of reagents - delta.Increment(frameTime); - if (delta.Lifetime > _digestionDelay) - { - solutionComponent.TryRemoveReagent(delta.ReagentId, delta.Quantity); - transferSolution.AddReagent(delta.ReagentId, delta.Quantity); - _reagentDeltas.Remove(delta); - } - } - - // Transfer digested reagents to bloodstream - bloodstream.TryTransferSolution(transferSolution); - } - /// /// Used to track quantity changes when ingesting & digesting reagents /// - private class ReagentDelta + protected class ReagentDelta { public readonly string ReagentId; public readonly ReagentUnit Quantity; diff --git a/Content.Shared/GameObjects/Components/Body/BodyExtensions.cs b/Content.Shared/GameObjects/Components/Body/BodyExtensions.cs new file mode 100644 index 0000000000..209e07ff30 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/BodyExtensions.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Diagnostics.CodeAnalysis; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Shared.GameObjects.Components.Body +{ + public static class BodyExtensions + { + public static T? GetBody(this IEntity entity) where T : class, IBody + { + return entity.GetComponentOrNull(); + } + + public static bool TryGetBody(this IEntity entity, [NotNullWhen(true)] out T? body) where T : class, IBody + { + return (body = entity.GetBody()) != null; + } + + public static IBody? GetBody(this IEntity entity) + { + return entity.GetComponentOrNull(); + } + + public static bool TryGetBody(this IEntity entity, [NotNullWhen(true)] out IBody? body) + { + return (body = entity.GetBody()) != null; + } + } +} diff --git a/Content.Server/GameObjects/Components/Body/IBodyPartManager.cs b/Content.Shared/GameObjects/Components/Body/IBody.cs similarity index 62% rename from Content.Server/GameObjects/Components/Body/IBodyPartManager.cs rename to Content.Shared/GameObjects/Components/Body/IBody.cs index 86c015e74f..1d256f97e6 100644 --- a/Content.Server/GameObjects/Components/Body/IBodyPartManager.cs +++ b/Content.Shared/GameObjects/Components/Body/IBody.cs @@ -1,28 +1,57 @@ -#nullable enable +#nullable enable +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using Content.Server.Body; -using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.Body.Part.Property; using Robust.Shared.Interfaces.GameObjects; -namespace Content.Server.GameObjects.Components.Body +namespace Content.Shared.GameObjects.Components.Body { - public interface IBodyPartManager : IComponent + /// + /// Component representing a collection of s + /// attached to each other. + /// + public interface IBody : IComponent, IBodyPartContainer { + public string? TemplateName { get; } + + public string? PresetName { get; } + + // TODO BODY tf is this /// - /// The that this - /// - /// is adhering to. + /// Maps all parts on this template to its BodyPartType. + /// For instance, "right arm" is mapped to "BodyPartType.arm" on the humanoid + /// template. /// - public BodyPreset Preset { get; } + public Dictionary Slots { get; } /// - /// Installs the given into the - /// given slot, deleting the afterwards. + /// Maps slots to the part filling each one. + /// + public IReadOnlyDictionary Parts { get; } + + // TODO BODY what am i doing + /// + /// 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". + /// + public Dictionary> Connections { get; } + + /// + /// Maps a template slot to the ID of the + /// that should fill it. E.g. "right arm" : "BodyPart.arm.basic_human". + /// + public IReadOnlyDictionary PartIds { get; } + + /// + /// Adds the given into the given slot. /// /// True if successful, false otherwise. - bool TryAddPart(string slot, DroppedBodyPartComponent part, bool force = false); - bool TryAddPart(string slot, IBodyPart part, bool force = false); bool HasPart(string slot); @@ -55,14 +84,17 @@ namespace Content.Server.GameObjects.Components.Body /// /// Disconnects the given reference, potentially - /// dropping other BodyParts if they were hanging - /// off of it. + /// dropping other BodyParts if they + /// were hanging off of it. /// + /// The part to drop. + /// + /// All of the parts that were dropped, including . + /// /// - /// The representing the dropped - /// , or null if none was dropped. + /// True if the part was dropped, false otherwise. /// - IEntity? DropPart(IBodyPart part); + bool TryDropPart(IBodyPart part, [NotNullWhen(true)] out List? dropped); /// /// Recursively searches for if is connected to @@ -99,7 +131,9 @@ namespace Content.Server.GameObjects.Components.Body /// /// Finds the slotName that the given resides in. /// - /// The to find the slot for. + /// + /// The to find the slot for. + /// /// The slot found, if any. /// True if a slot was found, false otherwise bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out string? slot); @@ -147,8 +181,28 @@ namespace Content.Server.GameObjects.Components.Body bool TryGetPartConnections(IBodyPart part, [NotNullWhen(true)] out List? connections); /// - /// Grabs all of the given type in this body. + /// Finds all s of the given type in this body. /// + /// A list of parts of that type. List GetPartsOfType(BodyPartType type); + + /// + /// Finds all s with the given property in this body. + /// + /// The property type to look for. + /// A list of parts with that property. + List<(IBodyPart part, IBodyPartProperty property)> GetPartsWithProperty(Type type); + + /// + /// Finds all s with the given property in this body. + /// + /// The property type to look for. + /// A list of parts with that property. + List<(IBodyPart part, T property)> GetPartsWithProperty() where T : class, IBodyPartProperty; + + // TODO BODY Make a slot object that makes sense to the human mind, and make it serializable. Imagine the possibilities! + KeyValuePair SlotAt(int index); + + KeyValuePair PartAt(int index); } } diff --git a/Content.Shared/GameObjects/Components/Body/IHasBody.cs b/Content.Shared/GameObjects/Components/Body/IHasBody.cs new file mode 100644 index 0000000000..6636fe4826 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/IHasBody.cs @@ -0,0 +1,13 @@ +#nullable enable +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Shared.GameObjects.Components.Body +{ + public interface IHasBody : IComponent + { + /// + /// The body that this component is currently a part of, if any. + /// + IBody? Body { get; } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/ISharedBodyManagerComponent.cs b/Content.Shared/GameObjects/Components/Body/ISharedBodyManagerComponent.cs deleted file mode 100644 index 9a8c026460..0000000000 --- a/Content.Shared/GameObjects/Components/Body/ISharedBodyManagerComponent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Content.Shared.GameObjects.Components.Damage; - -namespace Content.Shared.GameObjects.Components.Body -{ - public interface ISharedBodyManagerComponent : IDamageableComponent - { - } -} diff --git a/Content.Shared/GameObjects/Components/Body/Mechanism/IMechanism.cs b/Content.Shared/GameObjects/Components/Body/Mechanism/IMechanism.cs new file mode 100644 index 0000000000..16287138a2 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Mechanism/IMechanism.cs @@ -0,0 +1,71 @@ +#nullable enable +using Content.Shared.GameObjects.Components.Body.Part; + +namespace Content.Shared.GameObjects.Components.Body.Mechanism +{ + public interface IMechanism : IHasBody + { + IBodyPart? Part { get; set; } + + /// + /// Professional description of the . + /// + string Description { get; set; } + + /// + /// The message to display upon examining a mob with this + /// added. + /// If the string is empty (""), no message will be displayed. + /// + string ExamineMessage { get; set; } + + /// + /// Max HP of this . + /// + int MaxDurability { get; set; } + + /// + /// Current HP of this . + /// + int CurrentDurability { get; set; } + + /// + /// At what HP this is completely destroyed. + /// + int DestroyThreshold { get; set; } + + /// + /// Armor of this against attacks. + /// + int Resistance { get; set; } + + /// + /// Determines a handful of things - mostly whether this + /// can fit into a . + /// + // TODO BODY OnSizeChanged + int Size { get; set; } + + /// + /// What kind of this + /// can be easily installed into. + /// + BodyPartCompatibility Compatibility { get; set; } + + /// + /// Called when the part housing this mechanism is added to a body. + /// DO NOT CALL THIS DIRECTLY FROM OUTSIDE BODY PART CODE! + /// + /// The previous body, if any. + /// The new body. + void OnBodyAdd(IBody? old, IBody current); + + /// + /// Called when the part housing this mechanism is removed from + /// a body. + /// DO NOT CALL THIS DIRECTLY FROM OUTSIDE BODY PART CODE! + /// + /// The old body. + void OnBodyRemove(IBody old); + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Mechanism/MechanismExtensions.cs b/Content.Shared/GameObjects/Components/Body/Mechanism/MechanismExtensions.cs new file mode 100644 index 0000000000..34eaa521eb --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Mechanism/MechanismExtensions.cs @@ -0,0 +1,50 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared.GameObjects.Components.Body.Behavior; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Shared.GameObjects.Components.Body.Mechanism +{ + public static class MechanismExtensions + { + public static bool HasMechanismBehavior(this IEntity entity) where T : IMechanismBehavior + { + // TODO BODY optimize + return entity.TryGetBody(out var body) && + body.Parts.Values.Any(p => p.Mechanisms.Any(m => m.Owner.HasComponent())); + } + + public static IEnumerable GetMechanismBehaviors(this IEntity entity) where T : class, IMechanismBehavior + { + if (!entity.TryGetBody(out var body)) + { + yield break; + } + + foreach (var part in body.Parts.Values) + foreach (var mechanism in part.Mechanisms) + { + if (mechanism.Owner.TryGetComponent(out T? behavior)) + { + yield return behavior; + } + } + } + + public static bool TryGetMechanismBehaviors(this IEntity entity, [NotNullWhen(true)] out List? behaviors) + where T : class, IMechanismBehavior + { + behaviors = entity.GetMechanismBehaviors().ToList(); + + if (behaviors.Count == 0) + { + behaviors = null; + return false; + } + + return true; + } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Mechanism/SharedMechanismComponent.cs b/Content.Shared/GameObjects/Components/Body/Mechanism/SharedMechanismComponent.cs new file mode 100644 index 0000000000..2a79091c68 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Mechanism/SharedMechanismComponent.cs @@ -0,0 +1,103 @@ +#nullable enable +using System.Collections.Generic; +using Content.Shared.GameObjects.Components.Body.Part; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Body.Mechanism +{ + public abstract class SharedMechanismComponent : Component, IMechanism + { + public override string Name => "Mechanism"; + + private IBodyPart? _part; + + protected readonly Dictionary OptionsCache = new Dictionary(); + + protected IBody? BodyCache; + + protected int IdHash; + + protected IEntity? PerformerCache; + + public IBody? Body => Part?.Body; + + public IBodyPart? Part + { + get => _part; + set + { + if (_part == value) + { + return; + } + + var old = _part; + _part = value; + + if (value != null) + { + OnPartAdd(old, value); + } + else if (old != null) + { + OnPartRemove(old); + } + } + } + + public string Description { get; set; } = string.Empty; + + public string ExamineMessage { get; set; } = string.Empty; + + public int MaxDurability { get; set; } + + public int CurrentDurability { get; set; } + + public int DestroyThreshold { get; set; } + + // TODO BODY + public int Resistance { get; set; } + + // TODO BODY OnSizeChanged + public int Size { get; set; } + + public BodyPartCompatibility Compatibility { get; set; } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(this, m => m.Description, "description", string.Empty); + + serializer.DataField(this, m => m.ExamineMessage, "examineMessage", string.Empty); + + serializer.DataField(this, m => m.MaxDurability, "maxDurability", 10); + + serializer.DataField(this, m => m.CurrentDurability, "currentDurability", MaxDurability); + + serializer.DataField(this, m => m.DestroyThreshold, "destroyThreshold", -MaxDurability); + + serializer.DataField(this, m => m.Resistance, "resistance", 0); + + serializer.DataField(this, m => m.Size, "size", 1); + + serializer.DataField(this, m => m.Compatibility, "compatibility", BodyPartCompatibility.Universal); + } + + public virtual void OnBodyAdd(IBody? old, IBody current) { } + + public virtual void OnBodyRemove(IBody old) { } + + protected virtual void OnPartAdd(IBodyPart? old, IBodyPart current) + { + Owner.Transform.AttachParent(current.Owner); + } + + protected virtual void OnPartRemove(IBodyPart old) + { + Owner.Transform.AttachToGridOrMap(); + } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Networks/SharedBloodstreamComponent.cs b/Content.Shared/GameObjects/Components/Body/Networks/SharedBloodstreamComponent.cs new file mode 100644 index 0000000000..12827bd9fc --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Networks/SharedBloodstreamComponent.cs @@ -0,0 +1,16 @@ +using Content.Shared.Chemistry; +using Robust.Shared.GameObjects; + +namespace Content.Shared.GameObjects.Components.Body.Networks +{ + public abstract class SharedBloodstreamComponent : Component + { + /// + /// Attempt to transfer provided solution to internal solution. + /// Only supports complete transfers + /// + /// Solution to be transferred + /// Whether or not transfer was a success + public abstract bool TryTransferSolution(Solution solution); + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/BodyPartCompatibility.cs b/Content.Shared/GameObjects/Components/Body/Part/BodyPartCompatibility.cs new file mode 100644 index 0000000000..3b806f06a7 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/BodyPartCompatibility.cs @@ -0,0 +1,16 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Body.Part +{ + /// + /// Used to determine whether a BodyPart can connect to another BodyPart. + /// + [Serializable, NetSerializable] + public enum BodyPartCompatibility + { + Universal = 0, + Biological, + Mechanical + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/BodyPartExtensions.cs b/Content.Shared/GameObjects/Components/Body/Part/BodyPartExtensions.cs new file mode 100644 index 0000000000..79f0b832d2 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/BodyPartExtensions.cs @@ -0,0 +1,37 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; +using Content.Shared.GameObjects.Components.Body.Part.Property; + +namespace Content.Shared.GameObjects.Components.Body.Part +{ + public static class BodyPartExtensions + { + public static bool HasProperty(this IBodyPart part, Type type) + { + return part.Owner.HasComponent(type); + } + + public static bool HasProperty(this IBodyPart part) where T : class, IBodyPartProperty + { + return part.HasProperty(typeof(T)); + } + + public static bool TryGetProperty(this IBodyPart part, Type type, + [NotNullWhen(true)] out IBodyPartProperty? property) + { + if (!part.Owner.TryGetComponent(type, out var component)) + { + property = null; + return false; + } + + return (property = component as IBodyPartProperty) != null; + } + + public static bool TryGetProperty(this IBodyPart part, [NotNullWhen(true)] out T? property) where T : class, IBodyPartProperty + { + return part.Owner.TryGetComponent(out property); + } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/BodyPartSymmetry.cs b/Content.Shared/GameObjects/Components/Body/Part/BodyPartSymmetry.cs new file mode 100644 index 0000000000..c8b9ccb62a --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/BodyPartSymmetry.cs @@ -0,0 +1,13 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Body.Part +{ + [Serializable, NetSerializable] + public enum BodyPartSymmetry + { + None = 0, + Left, + Right + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/BodyPartType.cs b/Content.Shared/GameObjects/Components/Body/Part/BodyPartType.cs new file mode 100644 index 0000000000..e94cd5fc52 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/BodyPartType.cs @@ -0,0 +1,21 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Body.Part +{ + /// + /// Each BodyPart has a BodyPartType used to determine a variety of things. + /// For instance, what slots it can fit into. + /// + [Serializable, NetSerializable] + public enum BodyPartType + { + Other = 0, + Torso, + Head, + Arm, + Hand, + Leg, + Foot + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/IBodyPart.cs b/Content.Shared/GameObjects/Components/Body/Part/IBodyPart.cs new file mode 100644 index 0000000000..674d15e226 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/IBodyPart.cs @@ -0,0 +1,110 @@ +#nullable enable +using System.Collections.Generic; +using Content.Shared.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Surgery; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Map; + +namespace Content.Shared.GameObjects.Components.Body.Part +{ + public interface IBodyPart : IHasBody, IBodyPartContainer + { + new IBody? Body { get; set; } + + /// + /// that this is considered + /// to be. + /// For example, . + /// + BodyPartType PartType { get; } + + /// + /// Plural version of this name. + /// + public string Plural { get; } + + /// + /// Determines many things: how many mechanisms can be fit inside this + /// , whether a body can fit through tiny crevices, + /// etc. + /// + int Size { get; } + + // TODO BODY Mechanisms occupying different parts at the body level + /// + /// Collection of all s currently inside this + /// . + /// To add and remove from this list see and + /// + /// + IReadOnlyCollection Mechanisms { get; } + + /// + /// If body part is vital + /// + public bool IsVital { get; } + + public BodyPartSymmetry Symmetry { get; } + + bool Drop(); + + /// + /// Checks if the given can be used on + /// the current state of this . + /// + /// True if it can be used, false otherwise. + bool SurgeryCheck(SurgeryType surgery); + + /// + /// 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); + + /// + /// Checks if another can be connected to this one. + /// + /// The part to connect. + /// True if it can be connected, false otherwise. + bool CanAttachPart(IBodyPart part); + + /// + /// Checks if a can be added on this + /// . + /// + /// True if it can be added, false otherwise. + bool CanAddMechanism(IMechanism mechanism); + + bool TryAddMechanism(IMechanism mechanism, bool force = false); + + /// + /// Tries to remove the given from this + /// . + /// + /// The mechanism to remove. + /// True if it was removed, false otherwise. + bool RemoveMechanism(IMechanism mechanism); + + /// + /// Tries to remove the given from this + /// and drops it at the specified coordinates. + /// + /// The mechanism to remove. + /// The coordinates to drop it at. + /// True if it was removed, false otherwise. + bool RemoveMechanism(IMechanism mechanism, EntityCoordinates dropAt); + + /// + /// Tries to destroy the given from + /// this . + /// The mechanism won't be deleted if it is not in this body part. + /// + /// + /// True if the mechanism was in this body part and destroyed, + /// false otherwise. + /// + bool DeleteMechanism(IMechanism mechanism); + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/IBodyPartAdded.cs b/Content.Shared/GameObjects/Components/Body/Part/IBodyPartAdded.cs new file mode 100644 index 0000000000..a14c52d2f9 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/IBodyPartAdded.cs @@ -0,0 +1,26 @@ +using System; + +namespace Content.Shared.GameObjects.Components.Body.Part +{ + /// + /// This interface gives components behavior when a body part + /// is added to their owning entity. + /// + public interface IBodyPartAdded + { + void BodyPartAdded(BodyPartAddedEventArgs args); + } + + public class BodyPartAddedEventArgs : EventArgs + { + public BodyPartAddedEventArgs(IBodyPart part, string slot) + { + Part = part; + Slot = slot; + } + + public IBodyPart Part { get; } + + public string Slot { get; } + } +} diff --git a/Content.Server/Body/IBodyPartContainer.cs b/Content.Shared/GameObjects/Components/Body/Part/IBodyPartContainer.cs similarity index 54% rename from Content.Server/Body/IBodyPartContainer.cs rename to Content.Shared/GameObjects/Components/Body/Part/IBodyPartContainer.cs index f47d9b4657..366cae3e5c 100644 --- a/Content.Server/Body/IBodyPartContainer.cs +++ b/Content.Shared/GameObjects/Components/Body/Part/IBodyPartContainer.cs @@ -1,18 +1,16 @@ -using Content.Server.Body.Surgery; -using Content.Server.GameObjects.Components.Body; - -namespace Content.Server.Body +namespace Content.Shared.GameObjects.Components.Body.Part { /// - /// Making a class inherit from this interface allows you to do many things with - /// it in the class. + /// 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). + /// its parent (i.e. the holds many + /// s, each of which have an upward reference to it). /// + // TODO BODY Remove public interface IBodyPartContainer { } diff --git a/Content.Shared/GameObjects/Components/Body/Part/IBodyPartRemoved.cs b/Content.Shared/GameObjects/Components/Body/Part/IBodyPartRemoved.cs new file mode 100644 index 0000000000..a7c4b8da91 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/IBodyPartRemoved.cs @@ -0,0 +1,26 @@ +using System; + +namespace Content.Shared.GameObjects.Components.Body.Part +{ + /// + /// This interface gives components behavior when a body part + /// is removed from their owning entity. + /// + public interface IBodyPartRemoved + { + void BodyPartRemoved(BodyPartRemovedEventArgs args); + } + + public class BodyPartRemovedEventArgs : EventArgs + { + public BodyPartRemovedEventArgs(IBodyPart part, string slot) + { + Part = part; + Slot = slot; + } + + public IBodyPart Part { get; } + + public string Slot { get; } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/Property/BodyPartPropertyComponent.cs b/Content.Shared/GameObjects/Components/Body/Part/Property/BodyPartPropertyComponent.cs new file mode 100644 index 0000000000..30f1cef5ab --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/Property/BodyPartPropertyComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Body.Part.Property +{ + /// + /// Property attachable to a . + /// For example, this is used to define the speed capabilities of a + /// leg. The movement system will look for a LegProperty on all BodyParts. + /// + public abstract class BodyPartPropertyComponent : Component, IBodyPartProperty + { + /// + /// Whether this property is currently active. + /// + public bool Active { get; set; } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(this, b => b.Active, "active", true); + } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/Property/ExtensionComponent.cs b/Content.Shared/GameObjects/Components/Body/Part/Property/ExtensionComponent.cs new file mode 100644 index 0000000000..15a9d687b8 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/Property/ExtensionComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Body.Part.Property +{ + [RegisterComponent] + public class ExtensionComponent : BodyPartPropertyComponent + { + public override string Name => "Extension"; + + /// + /// Current distance (in tiles). + /// + public float Distance { get; set; } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(this, e => e.Distance, "distance", 3f); + } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/Property/GraspComponent.cs b/Content.Shared/GameObjects/Components/Body/Part/Property/GraspComponent.cs new file mode 100644 index 0000000000..24966664a0 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/Property/GraspComponent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.GameObjects; + +namespace Content.Shared.GameObjects.Components.Body.Part.Property +{ + /// + /// Defines an entity as being able to pick up items + /// + // TODO BODY Implement + [RegisterComponent] + public class GraspComponent : BodyPartPropertyComponent + { + public override string Name => "Grasp"; + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/Property/IBodyPartProperty.cs b/Content.Shared/GameObjects/Components/Body/Part/Property/IBodyPartProperty.cs new file mode 100644 index 0000000000..94f7dfac41 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/Property/IBodyPartProperty.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Shared.GameObjects.Components.Body.Part.Property +{ + public interface IBodyPartProperty : IComponent + { + bool Active { get; set; } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/Property/LegComponent.cs b/Content.Shared/GameObjects/Components/Body/Part/Property/LegComponent.cs new file mode 100644 index 0000000000..f80abbd209 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/Property/LegComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Body.Part.Property +{ + [RegisterComponent] + public class LegComponent : BodyPartPropertyComponent + { + public override string Name => "Leg"; + + /// + /// Speed (in tiles per second). + /// + public float Speed { get; set; } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(this, l => l.Speed, "speed", 2.6f); + } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Part/SharedBodyPartComponent.cs b/Content.Shared/GameObjects/Components/Body/Part/SharedBodyPartComponent.cs new file mode 100644 index 0000000000..efcca24342 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Part/SharedBodyPartComponent.cs @@ -0,0 +1,346 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Shared.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Surgery; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; + +namespace Content.Shared.GameObjects.Components.Body.Part +{ + public abstract class SharedBodyPartComponent : Component, IBodyPart + { + public override string Name => "BodyPart"; + + public override uint? NetID => ContentNetIDs.BODY_PART; + + private IBody? _body; + + // TODO BODY Remove + private List _mechanismIds = new List(); + public IReadOnlyList MechanismIds => _mechanismIds; + + [ViewVariables] + private HashSet _mechanisms = new HashSet(); + + [ViewVariables] + public IBody? Body + { + get => _body; + set + { + if (_body == value) + { + return; + } + + var old = _body; + _body = value; + + if (value != null) + { + foreach (var mechanism in _mechanisms) + { + mechanism.OnBodyAdd(old, value); + } + } + else if (old != null) + { + foreach (var mechanism in _mechanisms) + { + mechanism.OnBodyRemove(old); + } + } + } + } + + [ViewVariables] public BodyPartType PartType { get; private set; } + + [ViewVariables] public string Plural { get; private set; } = string.Empty; + + [ViewVariables] public int Size { get; private set; } + + [ViewVariables] public int SizeUsed { get; private set; } + + // TODO BODY size used + // TODO BODY surgerydata + + /// + /// 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; + + // TODO BODY Replace with a simulation of organs + /// + /// Represents if body part is vital for creature. + /// If the last vital body part is removed creature dies + /// + [ViewVariables] + public bool IsVital { get; private set; } + + [ViewVariables] + public BodyPartSymmetry Symmetry { get; private set; } + + // TODO BODY + [ViewVariables] + public SurgeryDataComponent? SurgeryDataComponent => Owner.GetComponentOrNull(); + + protected virtual void OnAddMechanism(IMechanism mechanism) + { + var prototypeId = mechanism.Owner.Prototype!.ID; + + if (!_mechanismIds.Contains(prototypeId)) + { + _mechanismIds.Add(prototypeId); + } + + mechanism.Part = this; + SizeUsed += mechanism.Size; + + Dirty(); + } + + protected virtual void OnRemoveMechanism(IMechanism mechanism) + { + _mechanismIds.Remove(mechanism.Owner.Prototype!.ID); + mechanism.Part = null; + SizeUsed -= mechanism.Size; + + Dirty(); + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + // TODO BODY serialize any changed properties? + + serializer.DataField(this, b => b.PartType, "partType", BodyPartType.Other); + + serializer.DataField(this, b => b.Plural, "plural", string.Empty); + + serializer.DataField(this, b => b.Size, "size", 1); + + serializer.DataField(this, b => b.Compatibility, "compatibility", BodyPartCompatibility.Universal); + + serializer.DataField(this, b => b.IsVital, "vital", false); + + serializer.DataField(this, b => b.Symmetry, "symmetry", BodyPartSymmetry.None); + + serializer.DataField(ref _mechanismIds, "mechanisms", new List()); + } + + public override ComponentState GetComponentState() + { + var mechanismIds = new EntityUid[_mechanisms.Count]; + + var i = 0; + foreach (var mechanism in _mechanisms) + { + mechanismIds[i] = mechanism.Owner.Uid; + i++; + } + + return new BodyPartComponentState(mechanismIds); + } + + public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) + { + base.HandleComponentState(curState, nextState); + + if (!(curState is BodyPartComponentState state)) + { + return; + } + + var newMechanisms = state.Mechanisms(); + + foreach (var mechanism in _mechanisms.ToArray()) + { + if (!newMechanisms.Contains(mechanism)) + { + RemoveMechanism(mechanism); + } + } + + foreach (var mechanism in newMechanisms) + { + if (!_mechanisms.Contains(mechanism)) + { + TryAddMechanism(mechanism, true); + } + } + } + + public bool Drop() + { + Body = null; + Owner.Transform.AttachToGridOrMap(); + return true; + } + + public bool SurgeryCheck(SurgeryType surgery) + { + return SurgeryDataComponent?.CheckSurgery(surgery) ?? false; + } + + /// + /// 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) + { + DebugTools.AssertNotNull(toolType); + DebugTools.AssertNotNull(target); + DebugTools.AssertNotNull(surgeon); + DebugTools.AssertNotNull(performer); + + return SurgeryDataComponent?.PerformSurgery(toolType, target, surgeon, performer) ?? false; + } + + public bool CanAttachPart(IBodyPart part) + { + DebugTools.AssertNotNull(part); + + return SurgeryDataComponent?.CanAttachBodyPart(part) ?? false; + } + + public bool CanAddMechanism(IMechanism mechanism) + { + DebugTools.AssertNotNull(mechanism); + + return SurgeryDataComponent != null && + SizeUsed + mechanism.Size <= Size && + SurgeryDataComponent.CanAddMechanism(mechanism); + } + + /// + /// Tries to add a mechanism onto this body part. + /// + /// The mechanism to try to add. + /// + /// Whether or not to check if the mechanism can be added. + /// + /// + /// True if successful, false if there was an error + /// (e.g. not enough room in ). + /// Will return false even when forced if the mechanism is already + /// added in this . + /// + public bool TryAddMechanism(IMechanism mechanism, bool force = false) + { + DebugTools.AssertNotNull(mechanism); + + if (!force && !CanAddMechanism(mechanism)) + { + return false; + } + + if (!_mechanisms.Add(mechanism)) + { + return false; + } + + OnAddMechanism(mechanism); + + return true; + } + + public bool RemoveMechanism(IMechanism mechanism) + { + DebugTools.AssertNotNull(mechanism); + + if (!_mechanisms.Remove(mechanism)) + { + return false; + } + + OnRemoveMechanism(mechanism); + + return true; + } + + public bool RemoveMechanism(IMechanism mechanism, EntityCoordinates coordinates) + { + if (RemoveMechanism(mechanism)) + { + mechanism.Owner.Transform.Coordinates = coordinates; + return true; + } + + return false; + } + + public bool DeleteMechanism(IMechanism mechanism) + { + DebugTools.AssertNotNull(mechanism); + + if (!RemoveMechanism(mechanism)) + { + return false; + } + + mechanism.Owner.Delete(); + return true; + } + } + + [Serializable, NetSerializable] + public class BodyPartComponentState : ComponentState + { + private List? _mechanisms; + + public readonly EntityUid[] MechanismIds; + + public BodyPartComponentState(EntityUid[] mechanismIds) : base(ContentNetIDs.BODY_PART) + { + MechanismIds = mechanismIds; + } + + public List Mechanisms(IEntityManager? entityManager = null) + { + if (_mechanisms != null) + { + return _mechanisms; + } + + entityManager ??= IoCManager.Resolve(); + + var mechanisms = new List(MechanismIds.Length); + + foreach (var id in MechanismIds) + { + if (!entityManager.TryGetEntity(id, out var entity)) + { + continue; + } + + if (!entity.TryGetComponent(out IMechanism? mechanism)) + { + continue; + } + + mechanisms.Add(mechanism); + } + + return _mechanisms = mechanisms; + } + } +} diff --git a/Content.Shared/Body/Preset/BodyPresetPrototype.cs b/Content.Shared/GameObjects/Components/Body/Preset/BodyPresetPrototype.cs similarity index 91% rename from Content.Shared/Body/Preset/BodyPresetPrototype.cs rename to Content.Shared/GameObjects/Components/Body/Preset/BodyPresetPrototype.cs index 6fb56cc3ba..5ec6552b41 100644 --- a/Content.Shared/Body/Preset/BodyPresetPrototype.cs +++ b/Content.Shared/GameObjects/Components/Body/Preset/BodyPresetPrototype.cs @@ -5,7 +5,7 @@ using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; using YamlDotNet.RepresentationModel; -namespace Content.Shared.Body.Preset +namespace Content.Shared.GameObjects.Components.Body.Preset { /// /// Prototype for the BodyPreset class. @@ -22,7 +22,7 @@ namespace Content.Shared.Body.Preset [ViewVariables] public string Name => _name; - [ViewVariables] public Dictionary PartIDs => _partIDs; + [ViewVariables] public Dictionary PartIDs => new Dictionary(_partIDs); public virtual void LoadFrom(YamlMappingNode mapping) { diff --git a/Content.Shared/GameObjects/Components/Body/Scanner/SharedBodyScannerComponent.cs b/Content.Shared/GameObjects/Components/Body/Scanner/SharedBodyScannerComponent.cs new file mode 100644 index 0000000000..cf821ab5ff --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Scanner/SharedBodyScannerComponent.cs @@ -0,0 +1,30 @@ +#nullable enable +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Body.Scanner +{ + public abstract class SharedBodyScannerComponent : Component + { + public override string Name => "BodyScanner"; + } + + [Serializable, NetSerializable] + public enum BodyScannerUiKey + { + Key + } + + [Serializable, NetSerializable] + public class BodyScannerUIState : BoundUserInterfaceState + { + public readonly EntityUid Uid; + + public BodyScannerUIState(EntityUid uid) + { + Uid = uid; + } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/SharedBodyComponent.cs b/Content.Shared/GameObjects/Components/Body/SharedBodyComponent.cs new file mode 100644 index 0000000000..59a20226d3 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/SharedBodyComponent.cs @@ -0,0 +1,753 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.Body.Part.Property; +using Content.Shared.GameObjects.Components.Body.Preset; +using Content.Shared.GameObjects.Components.Body.Template; +using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Movement; +using Content.Shared.GameObjects.EntitySystems; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; + +namespace Content.Shared.GameObjects.Components.Body +{ + // TODO BODY Damage methods for collections of IDamageableComponents + public abstract class SharedBodyComponent : Component, IBody + { + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public override string Name => "Body"; + + public override uint? NetID => ContentNetIDs.BODY; + + private string? _centerSlot; + + private Dictionary _partIds = new Dictionary(); + + private readonly Dictionary _parts = new Dictionary(); + + [ViewVariables] public string? TemplateName { get; private set; } + + [ViewVariables] public string? PresetName { get; private set; } + + [ViewVariables] + public Dictionary Slots { get; private set; } = new Dictionary(); + + [ViewVariables] + public Dictionary> Connections { get; private set; } = new Dictionary>(); + + /// + /// Maps slots to the part filling each one. + /// + [ViewVariables] + public IReadOnlyDictionary Parts => _parts; + + public IReadOnlyDictionary PartIds => _partIds; + + [ViewVariables] public IReadOnlyDictionary PartIDs => _partIds; + + protected virtual bool CanAddPart(string slot, IBodyPart part) + { + if (!HasSlot(slot) || !_parts.TryAdd(slot, part)) + { + return false; + } + + return true; + } + + protected virtual void OnAddPart(string slot, IBodyPart part) + { + part.Owner.Transform.AttachParent(Owner); + part.Body = this; + + var argsAdded = new BodyPartAddedEventArgs(part, slot); + + foreach (var component in Owner.GetAllComponents().ToArray()) + { + component.BodyPartAdded(argsAdded); + } + + // TODO BODY Sort this duplicate out + OnBodyChanged(); + } + + protected virtual void OnRemovePart(string slot, IBodyPart part) + { + // TODO BODY Move to Body part + if (!part.Owner.Transform.Deleted) + { + part.Owner.Transform.AttachToGridOrMap(); + } + + part.Body = null; + + var args = new BodyPartRemovedEventArgs(part, slot); + + foreach (var component in Owner.GetAllComponents()) + { + component.BodyPartRemoved(args); + } + + // creadth: fall down if no legs + if (part.PartType == BodyPartType.Leg && Parts.Count(x => x.Value.PartType == BodyPartType.Leg) == 0) + { + EntitySystem.Get().Down(Owner); + } + + // creadth: immediately kill entity if last vital part removed + if (Owner.TryGetComponent(out IDamageableComponent? damageable)) + { + if (part.IsVital && Parts.Count(x => x.Value.PartType == part.PartType) == 0) + { + damageable.CurrentState = DamageState.Dead; + damageable.ForceHealthChangedEvent(); + } + } + + OnBodyChanged(); + } + + public bool TryAddPart(string slot, IBodyPart part, bool force = false) + { + DebugTools.AssertNotNull(part); + DebugTools.AssertNotNull(slot); + + if (force) + { + if (!HasSlot(slot)) + { + Slots[slot] = part.PartType; + } + + _parts[slot] = part; + } + else + { + if (!CanAddPart(slot, part)) + { + return false; + } + } + + OnAddPart(slot, part); + + return true; + } + + public bool HasPart(string slot) + { + DebugTools.AssertNotNull(slot); + + return _parts.ContainsKey(slot); + } + + public void RemovePart(IBodyPart part, bool drop) + { + DebugTools.AssertNotNull(part); + + var slotName = _parts.FirstOrDefault(x => x.Value == part).Key; + + if (string.IsNullOrEmpty(slotName)) + { + return; + } + + RemovePart(slotName, drop); + } + + // TODO BODY invert this behavior with the one above + public bool RemovePart(string slot, bool drop) + { + DebugTools.AssertNotNull(slot); + + if (!_parts.Remove(slot, out var part)) + { + return false; + } + + if (drop) + { + part.Drop(); + } + + OnRemovePart(slot, part); + + if (TryGetSlotConnections(slot, out var connections)) + { + foreach (var connectionName in connections) + { + if (TryGetPart(connectionName, out var result) && !ConnectedToCenter(result)) + { + RemovePart(connectionName, drop); + } + } + } + + return true; + } + + public bool RemovePart(IBodyPart part, [NotNullWhen(true)] out string? slotName) + { + DebugTools.AssertNotNull(part); + + var pair = _parts.FirstOrDefault(kvPair => kvPair.Value == part); + + if (pair.Equals(default)) + { + slotName = null; + return false; + } + + if (RemovePart(pair.Key, false)) + { + slotName = pair.Key; + return true; + } + + slotName = null; + return false; + } + + public bool TryDropPart(IBodyPart part, [NotNullWhen(true)] out List? dropped) + { + DebugTools.AssertNotNull(part); + + if (!_parts.ContainsValue(part)) + { + dropped = null; + return false; + } + + if (!RemovePart(part, out var slotName)) + { + dropped = null; + return false; + } + + part.Drop(); + + dropped = new List {part}; + // Call disconnect on all limbs that were hanging off this limb. + if (TryGetSlotConnections(slotName, out var connections)) + { + // TODO BODY optimize + foreach (var connectionName in connections) + { + if (TryGetPart(connectionName, out var result) && + !ConnectedToCenter(result) && + RemovePart(connectionName, true)) + { + dropped.Add(result); + } + } + } + + OnBodyChanged(); + return true; + } + + public bool ConnectedToCenter(IBodyPart part) + { + var searchedSlots = new List(); + + return TryGetSlot(part, out var result) && + ConnectedToCenterPartRecursion(searchedSlots, result); + } + + private bool ConnectedToCenterPartRecursion(ICollection searchedSlots, string slotName) + { + if (!TryGetPart(slotName, out var part)) + { + return false; + } + + if (part == CenterPart()) + { + return true; + } + + searchedSlots.Add(slotName); + + if (!TryGetSlotConnections(slotName, out var connections)) + { + return false; + } + + foreach (var connection in connections) + { + if (!searchedSlots.Contains(connection) && + ConnectedToCenterPartRecursion(searchedSlots, connection)) + { + return true; + } + } + + return false; + } + + public IBodyPart? CenterPart() + { + if (_centerSlot == null) return null; + + return Parts.GetValueOrDefault(_centerSlot); + } + + public bool HasSlot(string slot) + { + return Slots.ContainsKey(slot); + } + + public bool TryGetPart(string slot, [NotNullWhen(true)] out IBodyPart? result) + { + return Parts.TryGetValue(slot, out result); + } + + public bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out string? slot) + { + // 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. + var pair = Parts.FirstOrDefault(x => x.Value == part); + slot = pair.Key; + + return !pair.Equals(default); + } + + public bool TryGetSlotType(string slot, out BodyPartType result) + { + return Slots.TryGetValue(slot, out result); + } + + public bool TryGetSlotConnections(string slot, [NotNullWhen(true)] out List? connections) + { + return Connections.TryGetValue(slot, out connections); + } + + public bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List? connections) + { + if (!Connections.TryGetValue(slot, out var slotConnections)) + { + connections = null; + return false; + } + + connections = new List(); + foreach (var connection in slotConnections) + { + if (TryGetPart(connection, out var part)) + { + connections.Add(part); + } + } + + if (connections.Count <= 0) + { + connections = null; + return false; + } + + return true; + } + + public bool TryGetPartConnections(IBodyPart part, [NotNullWhen(true)] out List? connections) + { + connections = null; + + return TryGetSlot(part, out var slotName) && + TryGetPartConnections(slotName, out connections); + } + + public List GetPartsOfType(BodyPartType type) + { + var parts = new List(); + + foreach (var part in Parts.Values) + { + if (part.PartType == type) + { + parts.Add(part); + } + } + + return parts; + } + + public List<(IBodyPart part, IBodyPartProperty property)> GetPartsWithProperty(Type type) + { + var parts = new List<(IBodyPart, IBodyPartProperty)>(); + + foreach (var part in Parts.Values) + { + if (part.TryGetProperty(type, out var property)) + { + parts.Add((part, property)); + } + } + + return parts; + } + + public List<(IBodyPart part, T property)> GetPartsWithProperty() where T : class, IBodyPartProperty + { + var parts = new List<(IBodyPart, T)>(); + + foreach (var part in Parts.Values) + { + if (part.TryGetProperty(out var property)) + { + parts.Add((part, property)); + } + } + + return parts; + } + + private void CalculateSpeed() + { + if (!Owner.TryGetComponent(out MovementSpeedModifierComponent? playerMover)) + { + return; + } + + var legs = GetPartsWithProperty(); + float speedSum = 0; + + foreach (var leg in GetPartsWithProperty()) + { + var footDistance = DistanceToNearestFoot(leg.part); + + if (Math.Abs(footDistance - float.MinValue) <= 0.001f) + { + continue; + } + + speedSum += leg.property.Speed * (1 + (float) Math.Log(footDistance, 1024.0)); + } + + if (speedSum <= 0.001f) + { + playerMover.BaseWalkSpeed = 0.8f; + playerMover.BaseSprintSpeed = 2.0f; + } + else + { + // Extra legs stack diminishingly. + playerMover.BaseWalkSpeed = + speedSum / (legs.Count - (float) Math.Log(legs.Count, 4.0)); + + playerMover.BaseSprintSpeed = playerMover.BaseWalkSpeed * 1.75f; + } + } + + /// + /// Called when the layout of this body changes. + /// + private void OnBodyChanged() + { + // Calculate move speed based on this body. + if (Owner.HasComponent()) + { + CalculateSpeed(); + } + + Dirty(); + } + + /// + /// Returns the combined length of the distance to the nearest + /// that is a foot. + /// If you consider a a node map, then it will + /// look for a foot node from the given node. It can only search + /// through s with an + /// . + /// + /// + /// The distance to the foot if found, + /// otherwise. + /// + public float DistanceToNearestFoot(IBodyPart source) + { + if (source.PartType == BodyPartType.Foot && + source.TryGetProperty(out var extension)) + { + return extension.Distance; + } + + return LookForFootRecursion(source, new List()); + } + + private float LookForFootRecursion(IBodyPart 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 (!TryGetPartConnections(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 (connection.PartType == BodyPartType.Foot && + !searchedParts.Contains(connection)) + { + return extProperty.Distance; + } + } + + // 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(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.Distance; + } + + return float.MinValue; + } + + // TODO BODY optimize this + public KeyValuePair SlotAt(int index) + { + return Slots.ElementAt(index); + } + + public KeyValuePair PartAt(int index) + { + return Parts.ElementAt(index); + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataReadWriteFunction( + "template", + null, + name => + { + if (string.IsNullOrEmpty(name)) + { + return; + } + + var template = _prototypeManager.Index(name); + + Connections = template.Connections; + Slots = template.Slots; + _centerSlot = template.CenterSlot; + + TemplateName = name; + }, + () => TemplateName); + + serializer.DataReadWriteFunction( + "preset", + null, + name => + { + if (string.IsNullOrEmpty(name)) + { + return; + } + + var preset = _prototypeManager.Index(name); + + _partIds = preset.PartIDs; + }, + () => PresetName); + + serializer.DataReadWriteFunction( + "connections", + new Dictionary>(), + connections => + { + foreach (var (from, to) in connections) + { + Connections.GetOrNew(from).AddRange(to); + } + }, + () => Connections); + + serializer.DataReadWriteFunction( + "slots", + new Dictionary(), + slots => + { + foreach (var (part, type) in slots) + { + Slots[part] = type; + } + }, + () => Slots); + + // TODO BODY Move to template or somewhere else + serializer.DataReadWriteFunction( + "centerSlot", + null, + slot => _centerSlot = slot, + () => _centerSlot); + + serializer.DataReadWriteFunction( + "partIds", + new Dictionary(), + partIds => + { + foreach (var (slot, part) in partIds) + { + _partIds[slot] = part; + } + }, + () => _partIds); + + // Our prototypes don't force the user to define a BodyPart connection twice. E.g. Head: Torso v.s. Torso: Head. + // The user only has to do one. We want it to be that way in the code, though, so this cleans that up. + var cleanedConnections = new Dictionary>(); + foreach (var targetSlotName in Slots.Keys) + { + var tempConnections = new List(); + foreach (var (slotName, slotConnections) in Connections) + { + if (slotName == targetSlotName) + { + foreach (var connection in slotConnections) + { + if (!tempConnections.Contains(connection)) + { + tempConnections.Add(connection); + } + } + } + else if (slotConnections.Contains(targetSlotName)) + { + tempConnections.Add(slotName); + } + } + + if (tempConnections.Count > 0) + { + cleanedConnections.Add(targetSlotName, tempConnections); + } + } + + Connections = cleanedConnections; + } + + public override ComponentState GetComponentState() + { + var parts = new (string slot, EntityUid partId)[_parts.Count]; + + var i = 0; + foreach (var (slot, part) in _parts) + { + parts[i] = (slot, part.Owner.Uid); + i++; + } + + return new BodyComponentState(parts); + } + + public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) + { + base.HandleComponentState(curState, nextState); + + if (!(curState is BodyComponentState state)) + { + return; + } + + var newParts = state.Parts(); + + foreach (var (slot, oldPart) in _parts) + { + if (!newParts.TryGetValue(slot, out var newPart) || + newPart != oldPart) + { + RemovePart(oldPart, false); + } + } + + foreach (var (slot, newPart) in newParts) + { + if (!_parts.TryGetValue(slot, out var oldPart) || + oldPart != newPart) + { + TryAddPart(slot, newPart, true); + } + } + } + } + + [Serializable, NetSerializable] + public class BodyComponentState : ComponentState + { + private Dictionary? _parts; + + public readonly (string slot, EntityUid partId)[] PartIds; + + public BodyComponentState((string slot, EntityUid partId)[] partIds) : base(ContentNetIDs.BODY) + { + PartIds = partIds; + } + + public Dictionary Parts(IEntityManager? entityManager = null) + { + if (_parts != null) + { + return _parts; + } + + entityManager ??= IoCManager.Resolve(); + + var parts = new Dictionary(PartIds.Length); + + foreach (var (slot, partId) in PartIds) + { + if (!entityManager.TryGetEntity(partId, out var entity)) + { + continue; + } + + if (!entity.TryGetComponent(out IBodyPart? part)) + { + continue; + } + + parts[slot] = part; + } + + return _parts = parts; + } + } +} diff --git a/Content.Shared/GameObjects/Components/Body/SharedBodyManagerComponent.cs b/Content.Shared/GameObjects/Components/Body/SharedBodyManagerComponent.cs deleted file mode 100644 index 2cb0022e88..0000000000 --- a/Content.Shared/GameObjects/Components/Body/SharedBodyManagerComponent.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using Content.Shared.GameObjects.Components.Damage; -using Robust.Shared.GameObjects; -using Robust.Shared.Serialization; - -namespace Content.Shared.GameObjects.Components.Body -{ - public abstract class SharedBodyManagerComponent : DamageableComponent, ISharedBodyManagerComponent - { - public override string Name => "BodyManager"; - - public override uint? NetID => ContentNetIDs.BODY_MANAGER; - } - - [Serializable, NetSerializable] - public sealed class BodyPartAddedMessage : ComponentMessage - { - public readonly string RSIPath; - public readonly string RSIState; - public readonly Enum RSIMap; - - public BodyPartAddedMessage(string rsiPath, string rsiState, Enum rsiMap) - { - Directed = true; - RSIPath = rsiPath; - RSIState = rsiState; - RSIMap = rsiMap; - } - } - - [Serializable, NetSerializable] - public sealed class BodyPartRemovedMessage : ComponentMessage - { - public readonly Enum RSIMap; - public readonly EntityUid? Dropped; - - public BodyPartRemovedMessage(Enum rsiMap, EntityUid? dropped = null) - { - Directed = true; - RSIMap = rsiMap; - Dropped = dropped; - } - } - - [Serializable, NetSerializable] - public sealed class MechanismSpriteAddedMessage : ComponentMessage - { - public readonly Enum RSIMap; - - public MechanismSpriteAddedMessage(Enum rsiMap) - { - Directed = true; - RSIMap = rsiMap; - } - } - - [Serializable, NetSerializable] - public sealed class MechanismSpriteRemovedMessage : ComponentMessage - { - public readonly Enum RSIMap; - - public MechanismSpriteRemovedMessage(Enum rsiMap) - { - Directed = true; - RSIMap = rsiMap; - } - } - - /// - /// Used to determine whether a BodyPart can connect to another BodyPart. - /// - [Serializable, NetSerializable] - public enum BodyPartCompatibility - { - Universal = 0, - Biological, - Mechanical - } - - /// - /// Each BodyPart has a BodyPartType used to determine a variety of things. - /// For instance, what slots it can fit into. - /// - [Serializable, NetSerializable] - public enum BodyPartType - { - Other = 0, - Torso, - Head, - Arm, - Hand, - Leg, - Foot - } - - /// - /// Defines a surgery operation that can be performed. - /// - [Serializable, NetSerializable] - public enum SurgeryType - { - None = 0, - Incision, - Retraction, - Cauterization, - VesselCompression, - Drilling, - Amputation - } -} diff --git a/Content.Server/Body/Surgery/BiologicalSurgeryData.cs b/Content.Shared/GameObjects/Components/Body/Surgery/BiologicalSurgeryDataComponent.cs similarity index 76% rename from Content.Server/Body/Surgery/BiologicalSurgeryData.cs rename to Content.Shared/GameObjects/Components/Body/Surgery/BiologicalSurgeryDataComponent.cs index 49fad56ec4..c2799fca6c 100644 --- a/Content.Server/Body/Surgery/BiologicalSurgeryData.cs +++ b/Content.Shared/GameObjects/Components/Body/Surgery/BiologicalSurgeryDataComponent.cs @@ -1,32 +1,37 @@ #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.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.Interfaces; -using JetBrains.Annotations; +using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Localization; -namespace Content.Server.Body.Surgery +namespace Content.Shared.GameObjects.Components.Body.Surgery { /// /// Data class representing the surgery state of a biological entity. /// - [UsedImplicitly] - public class BiologicalSurgeryData : SurgeryData + [RegisterComponent] + [ComponentReference(typeof(SurgeryDataComponent))] + public class BiologicalSurgeryDataComponent : SurgeryDataComponent { + public override string Name => "BiologicalSurgeryData"; + private readonly List _disconnectedOrgans = new List(); private bool _skinOpened; private bool _skinRetracted; private bool _vesselsClamped; - public BiologicalSurgeryData(IBodyPart parent) : base(parent) { } - protected override SurgeryAction? GetSurgeryStep(SurgeryType toolType) { + if (Parent == null) + { + return null; + } + if (toolType == SurgeryType.Amputation) { return RemoveBodyPartSurgery; @@ -91,6 +96,11 @@ namespace Content.Server.Body.Surgery public override string GetDescription(IEntity target) { + if (Parent == null) + { + return ""; + } + var toReturn = ""; if (_skinOpened && !_vesselsClamped) @@ -118,46 +128,57 @@ namespace Content.Server.Body.Surgery return toReturn; } - public override bool CanInstallMechanism(IMechanism mechanism) + public override bool CanAddMechanism(IMechanism mechanism) { - return _skinOpened && _vesselsClamped && _skinRetracted; + return Parent != null && + _skinOpened && + _vesselsClamped && + _skinRetracted; } public override bool CanAttachBodyPart(IBodyPart part) { - return true; - // TODO: if a bodypart is disconnected, you should have to do some surgery to allow another bodypart to be attached. + return Parent != null; + // TODO BODY if a part 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(Loc.GetString("Cut open the skin...")); + if (Parent == null) return; - // TODO do_after: Delay + performer.PopupMessage(Loc.GetString("Cut open the skin...")); + + // TODO BODY do_after: Delay _skinOpened = true; } private void ClampVesselsSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer) { + if (Parent == null) return; + performer.PopupMessage(Loc.GetString("Clamp the vessels...")); - // TODO do_after: Delay + // TODO BODY do_after: Delay _vesselsClamped = true; } private void RetractSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer) { + if (Parent == null) return; + performer.PopupMessage(Loc.GetString("Retract the skin...")); - // TODO do_after: Delay + // TODO BODY do_after: Delay _skinRetracted = true; } private void CauterizeIncisionSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer) { + if (Parent == null) return; + performer.PopupMessage(Loc.GetString("Cauterize the incision...")); - // TODO do_after: Delay + // TODO BODY do_after: Delay _skinOpened = false; _vesselsClamped = false; _skinRetracted = false; @@ -165,10 +186,8 @@ namespace Content.Server.Body.Surgery private void LoosenOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer) { - if (Parent.Mechanisms.Count <= 0) - { - return; - } + if (Parent == null) return; + if (Parent.Mechanisms.Count <= 0) return; var toSend = new List(); foreach (var mechanism in Parent.Mechanisms) @@ -185,22 +204,24 @@ namespace Content.Server.Body.Surgery } } - private void LoosenOrganSurgeryCallback(IMechanism target, IBodyPartContainer container, ISurgeon surgeon, + private void LoosenOrganSurgeryCallback(IMechanism? target, IBodyPartContainer container, ISurgeon surgeon, IEntity performer) { - if (target == null || !Parent.Mechanisms.Contains(target)) + if (Parent == null || target == null || !Parent.Mechanisms.Contains(target)) { return; } performer.PopupMessage(Loc.GetString("Loosen the organ...")); - // TODO do_after: Delay + // TODO BODY do_after: Delay _disconnectedOrgans.Add(target); } private void RemoveOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer) { + if (Parent == null) return; + if (_disconnectedOrgans.Count <= 0) { return; @@ -216,34 +237,30 @@ namespace Content.Server.Body.Surgery } } - private void RemoveOrganSurgeryCallback(IMechanism target, IBodyPartContainer container, ISurgeon surgeon, + private void RemoveOrganSurgeryCallback(IMechanism? target, IBodyPartContainer container, ISurgeon surgeon, IEntity performer) { - if (target == null || !Parent.Mechanisms.Contains(target)) + if (Parent == null || target == null || !Parent.Mechanisms.Contains(target)) { return; } performer.PopupMessage(Loc.GetString("Remove the organ...")); - // TODO do_after: Delay - Parent.TryDropMechanism(performer, target, out _); + // TODO BODY do_after: Delay + Parent.RemoveMechanism(target, performer.Transform.Coordinates); _disconnectedOrgans.Remove(target); } private void RemoveBodyPartSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer) { - // This surgery requires a DroppedBodyPartComponent. - if (!(container is BodyManagerComponent)) - { - return; - } + if (Parent == null) return; + if (!(container is IBody body)) return; - var bmTarget = (BodyManagerComponent) container; performer.PopupMessage(Loc.GetString("Saw off the limb!")); - // TODO do_after: Delay - bmTarget.RemovePart(Parent, true); + // TODO BODY do_after: Delay + body.RemovePart(Parent, true); } } } diff --git a/Content.Server/Body/Surgery/ISurgeon.cs b/Content.Shared/GameObjects/Components/Body/Surgery/ISurgeon.cs similarity index 78% rename from Content.Server/Body/Surgery/ISurgeon.cs rename to Content.Shared/GameObjects/Components/Body/Surgery/ISurgeon.cs index b15a2ae112..2ab1b13967 100644 --- a/Content.Server/Body/Surgery/ISurgeon.cs +++ b/Content.Shared/GameObjects/Components/Body/Surgery/ISurgeon.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; -using Content.Server.Body.Mechanisms; -using Content.Server.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Part; using Robust.Shared.Interfaces.GameObjects; -namespace Content.Server.Body.Surgery +namespace Content.Shared.GameObjects.Components.Body.Surgery { /// /// Interface representing an entity capable of performing surgery (performing operations on an - /// class). + /// class). /// For an example see , which inherits from this class. /// public interface ISurgeon @@ -24,7 +24,7 @@ namespace Content.Server.Body.Surgery public float BaseOperationTime { get; set; } /// - /// When performing a surgery, the may sometimes require selecting from a set of Mechanisms + /// 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. diff --git a/Content.Server/Body/Surgery/SurgeryData.cs b/Content.Shared/GameObjects/Components/Body/Surgery/SurgeryDataComponent.cs similarity index 53% rename from Content.Server/Body/Surgery/SurgeryData.cs rename to Content.Shared/GameObjects/Components/Body/Surgery/SurgeryDataComponent.cs index 1a0eec454f..e19ff49255 100644 --- a/Content.Server/Body/Surgery/SurgeryData.cs +++ b/Content.Shared/GameObjects/Components/Body/Surgery/SurgeryDataComponent.cs @@ -1,51 +1,50 @@ #nullable enable -using Content.Server.Body.Mechanisms; -using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Mechanism; +using Content.Shared.GameObjects.Components.Body.Part; +using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Serialization; -namespace Content.Server.Body.Surgery +namespace Content.Shared.GameObjects.Components.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. + /// 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 + public abstract class SurgeryDataComponent : Component { 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. + /// The this + /// is attached to. /// - protected readonly IBodyPart Parent; - - protected SurgeryData(IBodyPart parent) - { - Parent = parent; - } + protected IBodyPart? Parent => Owner.GetComponentOrNull(); /// - /// The of the parent . + /// The of the parent + /// . /// - protected BodyPartType ParentType => Parent.PartType; + protected BodyPartType? ParentType => Parent?.PartType; /// - /// Returns the description of this current to be shown - /// upon observing the given entity. + /// 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. + /// Returns whether a can be added into the + /// this + /// represents. /// - public abstract bool CanInstallMechanism(IMechanism mechanism); + public abstract bool CanAddMechanism(IMechanism mechanism); /// - /// Returns whether the given can be connected to the - /// this represents. + /// Returns whether the given can be connected + /// to the this + /// represents. /// public abstract bool CanAttachBodyPart(IBodyPart part); @@ -54,13 +53,15 @@ namespace Content.Server.Body.Surgery /// . /// /// - /// The corresponding surgery action or null if no step can be performed. + /// 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. + /// Returns whether the given can be used to + /// perform a surgery on the this + /// represents. /// public bool CheckSurgery(SurgeryType toolType) { @@ -68,12 +69,19 @@ namespace Content.Server.Body.Surgery } /// - /// Attempts to perform surgery of the given . Returns whether the operation was successful. + /// Attempts to perform surgery of the given . /// - /// The used for this surgery. - /// The container where the surgery is being done. - /// The entity being used to perform the surgery. + /// + /// 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. + /// True if successful, false otherwise. public bool PerformSurgery(SurgeryType surgeryType, IBodyPartContainer container, ISurgeon surgeon, IEntity performer) { diff --git a/Content.Shared/GameObjects/Components/Body/Surgery/SurgeryType.cs b/Content.Shared/GameObjects/Components/Body/Surgery/SurgeryType.cs new file mode 100644 index 0000000000..1429662bf1 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Surgery/SurgeryType.cs @@ -0,0 +1,20 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Body.Surgery +{ + /// + /// Defines a surgery operation that can be performed. + /// + [Serializable, NetSerializable] + public enum SurgeryType + { + None = 0, + Incision, + Retraction, + Cauterization, + VesselCompression, + Drilling, + Amputation + } +} diff --git a/Content.Shared/GameObjects/Components/Body/Surgery/SurgeryUIKey.cs b/Content.Shared/GameObjects/Components/Body/Surgery/SurgeryUIKey.cs new file mode 100644 index 0000000000..70fe6f303e --- /dev/null +++ b/Content.Shared/GameObjects/Components/Body/Surgery/SurgeryUIKey.cs @@ -0,0 +1,11 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Body.Surgery +{ + [Serializable, NetSerializable] + public enum SurgeryUIKey + { + Key + } +} diff --git a/Content.Shared/Body/Surgery/GenericSurgeryUIMessages.cs b/Content.Shared/GameObjects/Components/Body/Surgery/SurgeryUIMessages.cs similarity index 82% rename from Content.Shared/Body/Surgery/GenericSurgeryUIMessages.cs rename to Content.Shared/GameObjects/Components/Body/Surgery/SurgeryUIMessages.cs index 6a6f224bb1..5c9117f3f8 100644 --- a/Content.Shared/Body/Surgery/GenericSurgeryUIMessages.cs +++ b/Content.Shared/GameObjects/Components/Body/Surgery/SurgeryUIMessages.cs @@ -3,10 +3,9 @@ using System.Collections.Generic; using Robust.Shared.GameObjects.Components.UserInterface; using Robust.Shared.Serialization; -namespace Content.Shared.Body.Surgery +namespace Content.Shared.GameObjects.Components.Body.Surgery { - [Serializable] - [NetSerializable] + [Serializable, NetSerializable] public class RequestBodyPartSurgeryUIMessage : BoundUserInterfaceMessage { public Dictionary Targets; @@ -17,8 +16,7 @@ namespace Content.Shared.Body.Surgery } } - [Serializable] - [NetSerializable] + [Serializable, NetSerializable] public class RequestMechanismSurgeryUIMessage : BoundUserInterfaceMessage { public Dictionary Targets; @@ -29,8 +27,7 @@ namespace Content.Shared.Body.Surgery } } - [Serializable] - [NetSerializable] + [Serializable, NetSerializable] public class RequestBodyPartSlotSurgeryUIMessage : BoundUserInterfaceMessage { public Dictionary Targets; @@ -41,9 +38,7 @@ namespace Content.Shared.Body.Surgery } } - - [Serializable] - [NetSerializable] + [Serializable, NetSerializable] public class ReceiveBodyPartSurgeryUIMessage : BoundUserInterfaceMessage { public int SelectedOptionId; @@ -54,8 +49,7 @@ namespace Content.Shared.Body.Surgery } } - [Serializable] - [NetSerializable] + [Serializable, NetSerializable] public class ReceiveMechanismSurgeryUIMessage : BoundUserInterfaceMessage { public int SelectedOptionId; @@ -66,8 +60,7 @@ namespace Content.Shared.Body.Surgery } } - [Serializable] - [NetSerializable] + [Serializable, NetSerializable] public class ReceiveBodyPartSlotSurgeryUIMessage : BoundUserInterfaceMessage { public int SelectedOptionId; @@ -77,12 +70,4 @@ namespace Content.Shared.Body.Surgery SelectedOptionId = selectedOptionId; } } - - - [NetSerializable] - [Serializable] - public enum GenericSurgeryUiKey - { - Key - } } diff --git a/Content.Shared/Body/Template/BodyTemplatePrototype.cs b/Content.Shared/GameObjects/Components/Body/Template/BodyTemplatePrototype.cs similarity index 96% rename from Content.Shared/Body/Template/BodyTemplatePrototype.cs rename to Content.Shared/GameObjects/Components/Body/Template/BodyTemplatePrototype.cs index d899432cdd..d1468a337d 100644 --- a/Content.Shared/Body/Template/BodyTemplatePrototype.cs +++ b/Content.Shared/GameObjects/Components/Body/Template/BodyTemplatePrototype.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Part; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; using YamlDotNet.RepresentationModel; -namespace Content.Shared.Body.Template +namespace Content.Shared.GameObjects.Components.Body.Template { /// /// Prototype for the BodyTemplate class. diff --git a/Content.Shared/GameObjects/Components/Chemistry/SharedSolutionContainerComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SharedSolutionContainerComponent.cs index 170cf22944..64fcc4746f 100644 --- a/Content.Shared/GameObjects/Components/Chemistry/SharedSolutionContainerComponent.cs +++ b/Content.Shared/GameObjects/Components/Chemistry/SharedSolutionContainerComponent.cs @@ -1,40 +1,126 @@ -using System; +#nullable enable +using System; +using Content.Shared.Chemistry; using Robust.Shared.GameObjects; +using Robust.Shared.Maths; using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; namespace Content.Shared.GameObjects.Components.Chemistry { - public class SharedSolutionContainerComponent : Component + public abstract class SharedSolutionContainerComponent : Component { public override string Name => "SolutionContainer"; /// public sealed override uint? NetID => ContentNetIDs.SOLUTION; - [Serializable, NetSerializable] - public class SolutionComponentState : ComponentState + private Solution _solution = new Solution(); + private ReagentUnit _maxVolume; + private Color _substanceColor; + + /// + /// The contained solution. + /// + [ViewVariables] + public Solution Solution { - public SolutionComponentState() : base(ContentNetIDs.SOLUTION) { } + get => _solution; + set + { + if (_solution == value) + { + return; + } + + _solution = value; + Dirty(); + } } + /// + /// The total volume of all the of the reagents in the container. + /// + [ViewVariables] + public ReagentUnit CurrentVolume => Solution.TotalVolume; + + /// + /// The maximum volume of the container. + /// + [ViewVariables(VVAccess.ReadWrite)] + public ReagentUnit MaxVolume + { + get => _maxVolume; + set + { + if (_maxVolume == value) + { + return; + } + + _maxVolume = value; + Dirty(); + } + } + + /// + /// The current blended color of all the reagents in the container. + /// + [ViewVariables(VVAccess.ReadWrite)] + public virtual Color SubstanceColor + { + get => _substanceColor; + set + { + if (_substanceColor == value) + { + return; + } + + _substanceColor = value; + Dirty(); + } + } + + /// + /// The current capabilities of this container (is the top open to pour? can I inject it into another object?). + /// + [ViewVariables(VVAccess.ReadWrite)] + public SolutionContainerCaps Capabilities { get; set; } + + public abstract bool CanAddSolution(Solution solution); + + public abstract bool TryAddSolution(Solution solution, bool skipReactionCheck = false, bool skipColor = false); + + public abstract bool TryRemoveReagent(string reagentId, ReagentUnit quantity); + /// public override ComponentState GetComponentState() { - return new SolutionComponentState(); + return new SolutionContainerComponentState(Solution); } - /// - public override void HandleComponentState(ComponentState curState, ComponentState nextState) + public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) { base.HandleComponentState(curState, nextState); - if (curState == null) + if (!(curState is SolutionContainerComponentState state)) { return; } - - // var compState = (SolutionComponentState)curState; - // Is there anything we even need to sync with client? + + _solution = state.Solution; + } + } + + [Serializable, NetSerializable] + public class SolutionContainerComponentState : ComponentState + { + public readonly Solution Solution; + + public SolutionContainerComponentState(Solution solution) : base(ContentNetIDs.SOLUTION) + { + Solution = solution; } } } diff --git a/Content.Shared/GameObjects/Components/Damage/DamageableComponent.cs b/Content.Shared/GameObjects/Components/Damage/DamageableComponent.cs index 049dfaae03..118fcd2d29 100644 --- a/Content.Shared/GameObjects/Components/Damage/DamageableComponent.cs +++ b/Content.Shared/GameObjects/Components/Damage/DamageableComponent.cs @@ -5,7 +5,7 @@ using Content.Shared.Damage; using Content.Shared.Damage.DamageContainer; using Content.Shared.Damage.ResistanceSet; using Content.Shared.Interfaces.GameObjects.Components; -using Mono.Collections.Generic; +using Content.Shared.GameObjects.EntitySystems; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; @@ -25,59 +25,42 @@ namespace Content.Shared.GameObjects.Components.Damage { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + // TODO define these in yaml? + public const string DefaultDamageContainer = "metallicDamageContainer"; + public const string DefaultResistanceSet = "defaultResistances"; + public override string Name => "Damageable"; - private DamageState _currentDamageState; - + private DamageState _damageState; private DamageFlag _flags; public event Action? HealthChangedEvent; - /// - /// The threshold of damage, if any, above which the entity enters crit. - /// -1 means that this entity cannot go into crit. - /// - [ViewVariables(VVAccess.ReadWrite)] - public int? CriticalThreshold { get; set; } - - /// - /// The threshold of damage, if any, above which the entity dies. - /// -1 means that this entity cannot die. - /// - [ViewVariables(VVAccess.ReadWrite)] - public int? DeadThreshold { get; set; } - - [ViewVariables] private ResistanceSet Resistance { get; set; } = default!; + [ViewVariables] private ResistanceSet Resistances { get; set; } = default!; [ViewVariables] private DamageContainer Damage { get; set; } = default!; + public Dictionary Thresholds { get; set; } = new Dictionary(); + public virtual List SupportedDamageStates { get { var states = new List {DamageState.Alive}; - if (CriticalThreshold != null) - { - states.Add(DamageState.Critical); - } - - if (DeadThreshold != null) - { - states.Add(DamageState.Dead); - } + states.AddRange(Thresholds.Keys); return states; } } - public virtual DamageState CurrentDamageState + public virtual DamageState CurrentState { - get => _currentDamageState; + get => _damageState; set { - var old = _currentDamageState; - _currentDamageState = value; + var old = _damageState; + _damageState = value; if (old != value) { @@ -128,17 +111,36 @@ namespace Content.Shared.GameObjects.Components.Damage { base.ExposeData(serializer); + // TODO DAMAGE Serialize as a dictionary of damage states to thresholds serializer.DataReadWriteFunction( "criticalThreshold", - -1, - t => CriticalThreshold = t == -1 ? (int?) null : t, - () => CriticalThreshold ?? -1); + null, + t => + { + if (t == null) + { + return; + } + + Thresholds[DamageState.Critical] = t.Value; + }, + () => Thresholds.TryGetValue(DamageState.Critical, out var value) ? value : (int?) null); serializer.DataReadWriteFunction( "deadThreshold", - -1, - t => DeadThreshold = t == -1 ? (int?) null : t, - () => DeadThreshold ?? -1); + null, + t => + { + if (t == null) + { + return; + } + + Thresholds[DamageState.Dead] = t.Value; + }, + () => Thresholds.TryGetValue(DamageState.Dead, out var value) ? value : (int?) null); + + serializer.DataField(ref _damageState, "damageState", DamageState.Alive); serializer.DataReadWriteFunction( "flags", @@ -172,32 +174,26 @@ namespace Content.Shared.GameObjects.Components.Damage return writeFlags; }); - if (serializer.Reading) - { - // Doesn't write to file, TODO? - // Yes, TODO - var containerId = "biologicalDamageContainer"; - var resistanceId = "defaultResistances"; - - serializer.DataField(ref containerId, "damageContainer", "biologicalDamageContainer"); - serializer.DataField(ref resistanceId, "resistances", "defaultResistances"); - - if (!_prototypeManager.TryIndex(containerId!, out DamageContainerPrototype damage)) + // TODO DAMAGE Serialize damage done and resistance changes + serializer.DataReadWriteFunction( + "damagePrototype", + DefaultDamageContainer, + prototype => { - throw new InvalidOperationException( - $"No {nameof(DamageContainerPrototype)} found with name {containerId}"); - } + var damagePrototype = _prototypeManager.Index(prototype); + Damage = new DamageContainer(OnHealthChanged, damagePrototype); + }, + () => Damage.ID); - Damage = new DamageContainer(OnHealthChanged, damage); - - if (!_prototypeManager.TryIndex(resistanceId!, out ResistanceSetPrototype resistance)) + serializer.DataReadWriteFunction( + "resistancePrototype", + DefaultResistanceSet, + prototype => { - throw new InvalidOperationException( - $"No {nameof(ResistanceSetPrototype)} found with name {resistanceId}"); - } - - Resistance = new ResistanceSet(resistance); - } + var resistancePrototype = _prototypeManager.Index(prototype); + Resistances = new ResistanceSet(resistancePrototype); + }, + () => Resistances.ID); } public override void Initialize() @@ -210,6 +206,13 @@ namespace Content.Shared.GameObjects.Components.Damage } } + protected override void Startup() + { + base.Startup(); + + ForceHealthChangedEvent(); + } + public bool TryGetDamage(DamageType type, out int damage) { return Damage.TryGetDamageValue(type, out damage); @@ -229,7 +232,7 @@ namespace Content.Shared.GameObjects.Components.Damage var finalDamage = amount; if (!ignoreResistances) { - finalDamage = Resistance.CalculateDamage(type, amount); + finalDamage = Resistances.CalculateDamage(type, amount); } Damage.ChangeDamageValue(type, finalDamage); @@ -362,6 +365,32 @@ namespace Content.Shared.GameObjects.Components.Damage OnHealthChanged(data); } + public (int current, int max)? Health(DamageState threshold) + { + if (!SupportedDamageStates.Contains(threshold) || + !Thresholds.TryGetValue(threshold, out var thresholdValue)) + { + return null; + } + + var current = thresholdValue - TotalDamage; + return (current, thresholdValue); + } + + public bool TryHealth(DamageState threshold, out (int current, int max) health) + { + var temp = Health(threshold); + + if (temp == null) + { + health = (default, default); + return false; + } + + health = temp.Value; + return true; + } + private void OnHealthChanged(List changes) { var args = new HealthChangedEventArgs(this, changes); @@ -372,19 +401,21 @@ namespace Content.Shared.GameObjects.Components.Damage protected virtual void OnHealthChanged(HealthChangedEventArgs e) { - if (CurrentDamageState != DamageState.Dead) + if (CurrentState != DamageState.Dead) { - if (DeadThreshold != -1 && TotalDamage > DeadThreshold) + if (Thresholds.TryGetValue(DamageState.Dead, out var deadThreshold) && + TotalDamage > deadThreshold) { - CurrentDamageState = DamageState.Dead; + CurrentState = DamageState.Dead; } - else if (CriticalThreshold != -1 && TotalDamage > CriticalThreshold) + else if (Thresholds.TryGetValue(DamageState.Critical, out var critThreshold) && + TotalDamage > critThreshold) { - CurrentDamageState = DamageState.Critical; + CurrentState = DamageState.Critical; } else { - CurrentDamageState = DamageState.Alive; + CurrentState = DamageState.Alive; } } @@ -400,5 +431,19 @@ namespace Content.Shared.GameObjects.Components.Damage ChangeDamage(DamageType.Radiation, totalDamage, false, radiation.Owner); } + + public void OnExplosion(ExplosionEventArgs eventArgs) + { + var damage = eventArgs.Severity switch + { + ExplosionSeverity.Light => 20, + ExplosionSeverity.Heavy => 60, + ExplosionSeverity.Destruction => 250, + _ => throw new ArgumentOutOfRangeException() + }; + + ChangeDamage(DamageType.Piercing, damage, false); + ChangeDamage(DamageType.Heat, damage, false); + } } } diff --git a/Content.Shared/GameObjects/Components/Damage/IDamageableComponent.cs b/Content.Shared/GameObjects/Components/Damage/IDamageableComponent.cs index 8c6891953d..766ddfbfce 100644 --- a/Content.Shared/GameObjects/Components/Damage/IDamageableComponent.cs +++ b/Content.Shared/GameObjects/Components/Damage/IDamageableComponent.cs @@ -19,16 +19,18 @@ namespace Content.Shared.GameObjects.Components.Damage /// event Action HealthChangedEvent; + Dictionary Thresholds { get; } + /// - /// List of all DamageStates that - /// can be. + /// List of all DamageStates that + /// can be. /// List SupportedDamageStates { get; } /// - /// The currently representing this component. + /// The currently representing this component. /// - DamageState CurrentDamageState { get; } + DamageState CurrentState { get; set; } /// /// Sum of all damages taken. @@ -157,26 +159,37 @@ namespace Content.Shared.GameObjects.Components.Damage /// void ForceHealthChangedEvent(); - void IExAct.OnExplosion(ExplosionEventArgs eventArgs) - { - var damage = eventArgs.Severity switch - { - ExplosionSeverity.Light => 20, - ExplosionSeverity.Heavy => 60, - ExplosionSeverity.Destruction => 250, - _ => throw new ArgumentOutOfRangeException() - }; + /// + /// Calculates the health of an entity until it enters + /// . + /// + /// The state to use as a threshold. + /// + /// The current and maximum health on this entity based on + /// , or null if the state is not supported. + /// + (int current, int max)? Health(DamageState threshold); - ChangeDamage(DamageType.Piercing, damage, false); - ChangeDamage(DamageType.Heat, damage, false); - } + /// + /// Calculates the health of an entity until it enters + /// . + /// + /// The state to use as a threshold. + /// + /// The current and maximum health on this entity based on + /// , or null if the state is not supported. + /// + /// + /// True if is supported, false otherwise. + /// + bool TryHealth(DamageState threshold, [NotNullWhen(true)] out (int current, int max) health); } /// /// Data class with information on how to damage a /// . /// While not necessary to damage for all instances, classes such as - /// may require it for extra data + /// may require it for extra data /// (such as selecting which limb to target). /// public class HealthChangeParams : EventArgs diff --git a/Content.Shared/GameObjects/Components/Mobs/State/SharedMobStateManagerComponent.cs b/Content.Shared/GameObjects/Components/Mobs/State/SharedMobStateManagerComponent.cs index 091b7163ca..5bc6391d48 100644 --- a/Content.Shared/GameObjects/Components/Mobs/State/SharedMobStateManagerComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/State/SharedMobStateManagerComponent.cs @@ -97,9 +97,9 @@ namespace Content.Shared.GameObjects.Components.Mobs.State public void OnHealthChanged(HealthChangedEventArgs e) { - if (e.Damageable.CurrentDamageState != CurrentDamageState) + if (e.Damageable.CurrentState != CurrentDamageState) { - CurrentDamageState = e.Damageable.CurrentDamageState; + CurrentDamageState = e.Damageable.CurrentState; CurrentMobState.ExitState(Owner); CurrentMobState = Behavior[CurrentDamageState]; CurrentMobState.EnterState(Owner); diff --git a/Content.Shared/GameObjects/Components/Movement/SharedPlayerInputMoverComponent.cs b/Content.Shared/GameObjects/Components/Movement/SharedPlayerInputMoverComponent.cs index 27e8e5b8cf..17295fa6cb 100644 --- a/Content.Shared/GameObjects/Components/Movement/SharedPlayerInputMoverComponent.cs +++ b/Content.Shared/GameObjects/Components/Movement/SharedPlayerInputMoverComponent.cs @@ -270,7 +270,7 @@ namespace Content.Shared.GameObjects.Components.Movement bool ICollideSpecial.PreventCollide(IPhysBody collidedWith) { // Don't collide with other mobs - return collidedWith.Entity.HasComponent(); + return collidedWith.Entity.HasComponent(); } [Serializable, NetSerializable] diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index 753e768e09..3610015601 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -63,7 +63,7 @@ public const uint GAS_ANALYZER = 1057; public const uint DO_AFTER = 1058; public const uint RADIATION_PULSE = 1059; - public const uint BODY_MANAGER = 1060; + public const uint BODY = 1060; public const uint CLIMBING = 1061; public const uint BOLTACTION_BARREL = 1062; public const uint PUMP_BARREL = 1063; @@ -77,6 +77,7 @@ public const uint SLIP = 1071; public const uint SPACE_VILLAIN_ARCADE = 1072; public const uint BLOCKGAME_ARCADE = 1073; + public const uint BODY_PART = 1074; // Net IDs for integration tests. public const uint PREDICTION_TEST = 10001; diff --git a/Content.Shared/GameObjects/EntitySystems/ExamineSystemShared.cs b/Content.Shared/GameObjects/EntitySystems/ExamineSystemShared.cs index dae1d9b96e..6bf071c03a 100644 --- a/Content.Shared/GameObjects/EntitySystems/ExamineSystemShared.cs +++ b/Content.Shared/GameObjects/EntitySystems/ExamineSystemShared.cs @@ -21,6 +21,7 @@ namespace Content.Shared.GameObjects.EntitySystems /// Whether the examiner is within the 'Details' range, allowing you to show information logically only availabe when close to the examined entity. void Examine(FormattedMessage message, bool inDetailsRange); } + public abstract class ExamineSystemShared : EntitySystem { public const float ExamineRange = 16f; diff --git a/Content.Shared/GameObjects/EntitySystems/SharedMetabolismSystem.cs b/Content.Shared/GameObjects/EntitySystems/SharedMetabolismSystem.cs new file mode 100644 index 0000000000..827f933877 --- /dev/null +++ b/Content.Shared/GameObjects/EntitySystems/SharedMetabolismSystem.cs @@ -0,0 +1,11 @@ +using JetBrains.Annotations; +using Robust.Shared.GameObjects.Systems; + +namespace Content.Shared.GameObjects.EntitySystems +{ + [UsedImplicitly] + public class SharedMetabolismSystem : EntitySystem + { + // TODO move metabolism updates here from body entity system + } +} diff --git a/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs b/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs index 9de3c6ca93..ce61bfc392 100644 --- a/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs +++ b/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs @@ -1,4 +1,5 @@ using System; +using Content.Shared.GameObjects.Components.Body.Part; using Robust.Shared.Serialization; namespace Content.Shared.Preferences.Appearance @@ -23,4 +24,46 @@ namespace Content.Shared.Preferences.Appearance StencilMask, Fire, } + + public static class HumanoidVisualLayersExtension + { + public static HumanoidVisualLayers? ToHumanoidLayer(this IBodyPart part) + { + return part.PartType switch + { + BodyPartType.Other => null, + BodyPartType.Torso => HumanoidVisualLayers.Chest, + BodyPartType.Head => HumanoidVisualLayers.Head, + BodyPartType.Arm => part.Symmetry switch + { + BodyPartSymmetry.None => null, + BodyPartSymmetry.Left => HumanoidVisualLayers.LArm, + BodyPartSymmetry.Right => HumanoidVisualLayers.RArm, + _ => throw new ArgumentOutOfRangeException() + }, + BodyPartType.Hand => part.Symmetry switch + { + BodyPartSymmetry.None => null, + BodyPartSymmetry.Left => HumanoidVisualLayers.LHand, + BodyPartSymmetry.Right => HumanoidVisualLayers.RHand, + _ => throw new ArgumentOutOfRangeException() + }, + BodyPartType.Leg => part.Symmetry switch + { + BodyPartSymmetry.None => null, + BodyPartSymmetry.Left => HumanoidVisualLayers.LLeg, + BodyPartSymmetry.Right => HumanoidVisualLayers.RLeg, + _ => throw new ArgumentOutOfRangeException() + }, + BodyPartType.Foot => part.Symmetry switch + { + BodyPartSymmetry.None => null, + BodyPartSymmetry.Left => HumanoidVisualLayers.LFoot, + BodyPartSymmetry.Right => HumanoidVisualLayers.RFoot, + _ => throw new ArgumentOutOfRangeException() + }, + _ => throw new ArgumentOutOfRangeException() + }; + } + } } diff --git a/Content.Shared/Preferences/HumanoidCharacterAppearance.cs b/Content.Shared/Preferences/HumanoidCharacterAppearance.cs index 7b1001304a..41f83c5476 100644 --- a/Content.Shared/Preferences/HumanoidCharacterAppearance.cs +++ b/Content.Shared/Preferences/HumanoidCharacterAppearance.cs @@ -69,7 +69,7 @@ namespace Content.Shared.Preferences "Shaved", Color.Black, Color.Black, - Color.Black + Color.FromHex("#C0967F") ); } diff --git a/Content.Shared/Utility/TransformExtensions.cs b/Content.Shared/Utility/TransformExtensions.cs new file mode 100644 index 0000000000..d126dd5bba --- /dev/null +++ b/Content.Shared/Utility/TransformExtensions.cs @@ -0,0 +1,26 @@ +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.GameObjects.Components; + +namespace Content.Shared.Utility +{ + public static class TransformExtensions + { + public static void AttachToGrandparent(this ITransformComponent transform) + { + var grandParent = transform.Parent?.Parent; + + if (grandParent == null) + { + transform.AttachToGridOrMap(); + return; + } + + transform.AttachParent(grandParent); + } + + public static void AttachToGrandparent(this IEntity entity) + { + AttachToGrandparent(entity.Transform); + } + } +} diff --git a/Content.Tests/Shared/BodySystem/BodyTemplateTest.cs b/Content.Tests/Shared/BodySystem/BodyTemplateTest.cs deleted file mode 100644 index df0240b8d4..0000000000 --- a/Content.Tests/Shared/BodySystem/BodyTemplateTest.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using Content.Server.Body; -using Content.Shared.GameObjects.Components.Body; -using NUnit.Framework; -using Robust.UnitTesting; - -namespace Content.Tests.Shared.BodySystem -{ - [TestFixture, Parallelizable, TestOf(typeof(BodyTemplate))] - public class BodyTemplateTest : RobustUnitTest - { - [Test] - public void BodyTemplateHashCodeTest() - { - var a = new BodyTemplate(); - var b = new BodyTemplate(); - var c = new BodyTemplate(); - var d = new BodyTemplate(); - var e = new BodyTemplate(); - - a.Slots.Add("torso", BodyPartType.Torso); - a.Slots.Add("left arm", BodyPartType.Arm); - a.Connections.Add("torso", new List() { "left arm" }); - a.CenterSlot = "torso"; - - b.Slots.Add("left arm", BodyPartType.Arm); - b.Slots.Add("torso", BodyPartType.Torso); - b.Connections.Add("left arm", new List() { "torso" }); - b.CenterSlot = "torso"; - - c.Slots.Add("torso", BodyPartType.Head); - c.Slots.Add("left arm", BodyPartType.Arm); - c.Connections.Add("torso", new List() { "left arm" }); - a.CenterSlot = "torso"; - - d.Slots.Add("torso", BodyPartType.Torso); - d.Slots.Add("left arm", BodyPartType.Arm); - d.Slots.Add("left hand", BodyPartType.Hand); - d.Connections.Add("left arm", new List() { "left hand" }); - d.CenterSlot = "torso"; - - e.Slots.Add("torso", BodyPartType.Torso); - e.Slots.Add("left arm", BodyPartType.Arm); - e.Slots.Add("left hand", BodyPartType.Hand); - e.Connections.Add("left arm", new List() { "torso" }); - e.CenterSlot = "left hand"; - - Assert.That(a.Equals(b) && a.GetHashCode() != 0 && b.GetHashCode() != 0); - Assert.That(!a.Equals(c) && a.GetHashCode() != 0 && c.GetHashCode() != 0); - Assert.That(!d.Equals(e) && d.GetHashCode() != 0 && e.GetHashCode() != 0); - } - } -} diff --git a/Resources/Prototypes/Body/Mechanisms/basic_human_organs.yml b/Resources/Prototypes/Body/Mechanisms/basic_human_organs.yml index d7ec211e93..e85f15d2f1 100644 --- a/Resources/Prototypes/Body/Mechanisms/basic_human_organs.yml +++ b/Resources/Prototypes/Body/Mechanisms/basic_human_organs.yml @@ -1,90 +1,101 @@ - type: entity - parent: BaseDroppedMechanism - id: HeartMechanismDebug - name: "human heart (debug)" + id: BrainHuman + name: "human brain" + description: "The source of incredible, unending intelligence. Honk." components: - type: Sprite + netsync: false + sprite: Mobs/Species/Human/organs.rsi + state: brain + - type: Mechanism + durability: 10 + size: 1 + compatibility: Biological + - type: Brain + +- type: entity + id: EyesHuman + name: "human eyes" + description: "Ocular organ capable of turning light into a colorful visual." + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/organs.rsi + state: eyeballs + - type: Mechanism + durability: 10 + size: 1 + compatibility: Biological + +- type: entity + id: HeartHuman + name: "human heart" + description: "Pumps blood throughout a body. Essential for any entity with blood." + components: + - type: Sprite + netsync: false sprite: Mobs/Species/Human/organs.rsi state: heart-on - - type: DroppedMechanism - debugLoadMechanismData: mechanism.Heart.BasicHuman - behaviors: - - Content.Server.Body.Mechanisms.Behaviors.HeartBehavior + - type: Mechanism + durability: 10 + size: 1 + compatibility: Biological + - type: Heart -- type: mechanism - id: mechanism.Brain.BasicHuman - name: "human brain" - rsiPath: Mobs/Species/Human/organs.rsi - rsiState: "brain_human" - description: "The source of incredible, unending intelligence. Honk." - durability: 10 - size: 1 - compatibility: Biological - behaviors: - - Content.Server.Body.Mechanisms.Behaviors.BrainBehavior - -- type: mechanism - id: mechanism.Eyes.BasicHuman - name: "human eyes" - rsiPath: Mobs/Species/Human/organs.rsi - rsiState: "eyeballs" - description: "Ocular organ capable of turning light into a colorful visual." - durability: 10 - size: 1 - compatibility: Biological - -- type: mechanism - id: mechanism.Heart.BasicHuman - name: "human heart" - rsiPath: Mobs/Species/Human/organs.rsi - rsiState: "heart-on" - description: "Pumps blood throughout a body. Essential for any entity with blood." - durability: 10 - size: 1 - compatibility: Biological - behaviors: - - Content.Server.Body.Mechanisms.Behaviors.HeartBehavior - -- type: mechanism - id: mechanism.Lungs.BasicHuman +- type: entity + id: LungsHuman name: "human lungs" - rsiPath: Mobs/Species/Human/organs.rsi - rsiState: "lungs" description: "Filters oxygen from an atmosphere, which is then sent into the bloodstream to be used as an electron carrier." - durability: 13 - size: 1 - compatibility: Biological - behaviors: - - Content.Server.Body.Mechanisms.Behaviors.LungBehavior + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/organs.rsi + state: lungs + - type: Mechanism + durability: 13 + size: 1 + compatibility: Biological + - type: Lung -- type: mechanism - id: mechanism.Stomach.BasicHuman +- type: entity + id: StomachHuman name: "human stomach" - rsiPath: Mobs/Species/Human/organs.rsi - rsiState: "stomach" description: "Gross. This is hard to stomach." - durability: 13 - size: 1 - compatibility: Biological - behaviors: - - Content.Server.Body.Mechanisms.Behaviors.StomachBehavior + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/organs.rsi + state: stomach + - type: Mechanism + durability: 13 + size: 1 + compatibility: Biological + - type: Stomach -- type: mechanism - id: mechanism.Liver.BasicHuman +- type: entity + id: LiverHuman name: "human liver" - rsiPath: Mobs/Species/Human/organs.rsi - rsiState: "liver" description: "Filters impurities out of a bloodstream and provides other important functionality to a human." - durability: 15 - size: 1 - compatibility: Biological + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/organs.rsi + state: liver + - type: Mechanism + durability: 15 + size: 1 + compatibility: Biological -- type: mechanism - id: mechanism.Kidneys.BasicHuman +- type: entity + id: KidneysHuman name: "human kidneys" - rsiPath: Mobs/Species/Human/organs.rsi - rsiState: "kidneys" description: "Filters toxins out of a bloodstream." - durability: 20 - size: 1 - compatibility: Biological + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/organs.rsi + state: kidneys + - type: Mechanism + durability: 20 + size: 1 + compatibility: Biological diff --git a/Resources/Prototypes/Body/Mechanisms/basic_tests.yml b/Resources/Prototypes/Body/Mechanisms/basic_tests.yml index 2c591bbfcd..b5503bc1a8 100644 --- a/Resources/Prototypes/Body/Mechanisms/basic_tests.yml +++ b/Resources/Prototypes/Body/Mechanisms/basic_tests.yml @@ -1,18 +1,26 @@ -- type: mechanism - id: mechanism.EMPStriker +- type: entity + id: MechanismEMPStriker name: "EMP striker" description: "When activated, this arm implant will apply a small EMP on the target of a physical strike for 10 watts per use." - durability: 80 - size: 4 - compatibility: Universal - implantableParts: - - Arm - - Hand + components: + - type: Sprite + - type: Icon + - type: Mechanism + durability: 80 + size: 4 + compatibility: Universal + implantableParts: + - Arm + - Hand -- type: mechanism - id: mechanism.HonkModule +- type: entity + id: MechanismHonkModule name: "HONK module 3000" description: "Mandatory implant for all clowns after the Genevo Convention of 2459." - durability: 50 - size: 3 - compatibility: Universal + components: + - type: Sprite + - type: Icon + - type: Mechanism + durability: 50 + size: 3 + compatibility: Universal diff --git a/Resources/Prototypes/Body/Parts/humanoid_parts.yml b/Resources/Prototypes/Body/Parts/humanoid_parts.yml index a9964674e2..eda82f12f1 100644 --- a/Resources/Prototypes/Body/Parts/humanoid_parts.yml +++ b/Resources/Prototypes/Body/Parts/humanoid_parts.yml @@ -1,193 +1,281 @@ -- type: bodyPart - id: bodyPart.Torso.BasicHuman +- type: entity + id: PartHuman + name: "human body part" + abstract: true + +- type: entity + id: TorsoHuman name: "human torso" - plural: "human torsos" - rsiPath: Mobs/Species/Human/parts.rsi - rsiState: "torso_m" - partType: Torso - durability: 100 - destroyThreshold: -150 - size: 14 - compatibility: Biological - damageContainer: biologicalDamageContainer - resistances: defaultResistances - surgeryDataType: Content.Server.Body.Surgery.BiologicalSurgeryData - mechanisms: - - mechanism.Heart.BasicHuman - - mechanism.Lungs.BasicHuman - - mechanism.Stomach.BasicHuman - - mechanism.Liver.BasicHuman - - mechanism.Kidneys.BasicHuman + parent: PartHuman + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/parts.rsi + state: "torso_m" + - type: Icon + sprite: Mobs/Species/Human/parts.rsi + state: "torso_m" + - type: BodyPart + plural: "human torsos" + partType: Torso + size: 14 + compatibility: Biological + mechanisms: + - HeartHuman + - LungsHuman + - StomachHuman + - LiverHuman + - KidneysHuman + - type: BiologicalSurgeryData + - type: Damageable + # TODO BODY DettachableDamageableComponent? + damageContainer: biologicalDamageContainer + resistances: defaultResistances + criticalThreshold: 100 + deadThreshold: 150 -- type: bodyPart - id: bodyPart.Head.BasicHuman +- type: entity + id: HeadHuman name: "human head" - plural: "human heads" - rsiPath: Mobs/Species/Human/parts.rsi - rsiState: head_m - partType: Head - durability: 50 - destroyThreshold: -120 - size: 7 - compatibility: Biological - damageContainer: biologicalDamageContainer - resistances: defaultResistances - surgeryDataType: Content.Server.Body.Surgery.BiologicalSurgeryData - isVital: true - mechanisms: - - mechanism.Brain.BasicHuman - - mechanism.Eyes.BasicHuman + parent: PartHuman + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/parts.rsi + state: "head_m" + - type: Icon + sprite: Mobs/Species/Human/parts.rsi + state: "head_m" + - type: BodyPart + plural: "human heads" + partType: Head + size: 7 + compatibility: Biological + isVital: true + mechanisms: + - BrainHuman + - EyesHuman + - type: BiologicalSurgeryData + - type: Damageable + damageContainer: biologicalDamageContainer + resistances: defaultResistances + criticalThreshold: 50 + deadThreshold: 120 -- type: bodyPart - id: bodyPart.LArm.BasicHuman +- type: entity + id: LeftArmHuman name: "left human arm" - plural: "left human arms" - rsiPath: Mobs/Species/Human/parts.rsi - rsiState: l_arm - partType: Arm - durability: 40 - destroyThreshold: -80 - size: 5 - compatibility: Biological - damageContainer: biologicalDamageContainer - resistances: defaultResistances - surgeryDataType: Content.Server.Body.Surgery.BiologicalSurgeryData - properties: - - !type:ExtensionProperty - active: true - reachDistance: 2.4 + parent: PartHuman + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/parts.rsi + state: "l_arm" + - type: Icon + sprite: Mobs/Species/Human/parts.rsi + state: "l_arm" + - type: BodyPart + plural: "left human arms" + partType: Arm + size: 5 + compatibility: Biological + symmetry: Left + - type: BiologicalSurgeryData + - type: Damageable + damageContainer: biologicalDamageContainer + resistances: defaultResistances + criticalThreshold: 40 + deadThreshold: 80 + - type: Extension + distance: 2.4 -- type: bodyPart - id: bodyPart.RArm.BasicHuman +- type: entity + id: RightArmHuman name: "right human arm" - plural: "right human arms" - rsiPath: Mobs/Species/Human/parts.rsi - rsiState: r_arm - partType: Arm - durability: 40 - destroyThreshold: -80 - size: 5 - compatibility: Biological - damageContainer: biologicalDamageContainer - resistances: defaultResistances - surgeryDataType: Content.Server.Body.Surgery.BiologicalSurgeryData - properties: - - !type:ExtensionProperty - active: true - reachDistance: 2.4 + parent: PartHuman + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/parts.rsi + state: "r_arm" + - type: Icon + sprite: Mobs/Species/Human/parts.rsi + state: "r_arm" + - type: BodyPart + plural: "right human arms" + partType: Arm + size: 5 + compatibility: Biological + symmetry: Right + - type: BiologicalSurgeryData + - type: Damageable + damageContainer: biologicalDamageContainer + resistances: defaultResistances + criticalThreshold: 40 + deadThreshold: 80 + - type: Extension + distance: 2.4 -- type: bodyPart - id: bodyPart.LHand.BasicHuman +- type: entity + id: LeftHandHuman name: "left human hand" - plural: "left human hands" - rsiPath: Mobs/Species/Human/parts.rsi - rsiState: l_hand - partType: Hand - durability: 30 - destroyThreshold: -60 - size: 3 - compatibility: Biological - damageContainer: biologicalDamageContainer - resistances: defaultResistances - surgeryDataType: Content.Server.Body.Surgery.BiologicalSurgeryData - properties: - - !type:GraspProperty - active: true + parent: PartHuman + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/parts.rsi + state: "l_hand" + - type: Icon + sprite: Mobs/Species/Human/parts.rsi + state: "l_hand" + - type: BodyPart + plural: "left human hands" + partType: Hand + size: 3 + compatibility: Biological + symmetry: Left + - type: BiologicalSurgeryData + - type: Damageable + damageContainer: biologicalDamageContainer + resistances: defaultResistances + criticalThreshold: 30 + deadThreshold: 60 + - type: Grasp -- type: bodyPart - id: bodyPart.RHand.BasicHuman +- type: entity + id: RightHandHuman name: "right human hand" - plural: "right human hands" - rsiPath: Mobs/Species/Human/parts.rsi - rsiState: r_hand - partType: Hand - durability: 30 - destroyThreshold: -60 - size: 3 - compatibility: Biological - damageContainer: biologicalDamageContainer - resistances: defaultResistances - surgeryDataType: Content.Server.Body.Surgery.BiologicalSurgeryData - properties: - - !type:GraspProperty - active: true + parent: PartHuman + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/parts.rsi + state: "r_hand" + - type: Icon + sprite: Mobs/Species/Human/parts.rsi + state: "r_hand" + - type: BodyPart + plural: "right human hands" + partType: Hand + durability: 30 + destroyThreshold: -60 + size: 3 + compatibility: Biological + symmetry: Right + - type: BiologicalSurgeryData + - type: Damageable + damageContainer: biologicalDamageContainer + resistances: defaultResistances + criticalThreshold: 30 + deadThreshold: 60 + - type: Grasp -- type: bodyPart - id: bodyPart.LLeg.BasicHuman +- type: entity + id: LeftLegHuman name: "left human leg" - plural: "left human legs" - rsiPath: Mobs/Species/Human/parts.rsi - rsiState: l_leg - partType: Leg - durability: 45 - destroyThreshold: -90 - size: 6 - compatibility: Biological - damageContainer: biologicalDamageContainer - resistances: defaultResistances - surgeryDataType: Content.Server.Body.Surgery.BiologicalSurgeryData - properties: - - !type:ExtensionProperty - active: true - reachDistance: 3.0 - - !type:LegProperty - active: true + parent: PartHuman + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/parts.rsi + state: "l_leg" + - type: Icon + sprite: Mobs/Species/Human/parts.rsi + state: "l_leg" + - type: BodyPart + plural: "left human legs" + partType: Leg + size: 6 + compatibility: Biological + symmetry: Left + - type: BiologicalSurgeryData + - type: Damageable + damageContainer: biologicalDamageContainer + resistances: defaultResistances + criticalThreshold: 45 + deadThreshold: 90 + - type: Leg speed: 2.6 + - type: Extension + distance: 3.0 -- type: bodyPart - id: bodyPart.RLeg.BasicHuman +- type: entity + id: RightLegHuman name: "right human leg" - plural: "right human legs" - rsiPath: Mobs/Species/Human/parts.rsi - rsiState: r_leg - partType: Leg - durability: 45 - destroyThreshold: -90 - size: 6 - compatibility: Biological - damageContainer: biologicalDamageContainer - resistances: defaultResistances - surgeryDataType: Content.Server.Body.Surgery.BiologicalSurgeryData - properties: - - !type:ExtensionProperty - active: true - reachDistance: 3.0 - - !type:LegProperty - active: true + parent: PartHuman + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/parts.rsi + state: "r_leg" + - type: Icon + sprite: Mobs/Species/Human/parts.rsi + state: "r_leg" + - type: BodyPart + plural: "right human legs" + partType: Leg + durability: 45 + destroyThreshold: -90 + size: 6 + compatibility: Biological + symmetry: Right + - type: BiologicalSurgeryData + - type: Damageable + damageContainer: biologicalDamageContainer + resistances: defaultResistances + criticalThreshold: 45 + deadThreshold: 90 + - type: Leg speed: 2.6 + - type: Extension + distance: 3.0 -- type: bodyPart - id: bodyPart.LFoot.BasicHuman +- type: entity + id: LeftFootHuman name: "left human foot" - plural: "left human feet" - rsiPath: Mobs/Species/Human/parts.rsi - rsiState: l_foot - partType: Foot - durability: 30 - destroyThreshold: -60 - size: 2 - compatibility: Biological - damageContainer: biologicalDamageContainer - resistances: defaultResistances - surgeryDataType: Content.Server.Body.Surgery.BiologicalSurgeryData - properties: - - !type:FootProperty - active: true + parent: PartHuman + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/parts.rsi + state: "l_foot" + - type: Icon + sprite: Mobs/Species/Human/parts.rsi + state: "l_foot" + - type: BodyPart + plural: "left human feet" + partType: Foot + size: 2 + compatibility: Biological + symmetry: Left + - type: BiologicalSurgeryData + - type: Damageable + damageContainer: biologicalDamageContainer + resistances: defaultResistances + criticalThreshold: 30 + deadThreshold: 60 -- type: bodyPart - id: bodyPart.RFoot.BasicHuman +- type: entity + id: RightFootHuman name: "right human foot" - plural: "right human feet" - rsiPath: Mobs/Species/Human/parts.rsi - rsiState: r_foot - partType: Foot - durability: 30 - destroyThreshold: -60 - size: 2 - compatibility: Biological - damageContainer: biologicalDamageContainer - resistances: defaultResistances - surgeryDataType: Content.Server.Body.Surgery.BiologicalSurgeryData - properties: - - !type:FootProperty - active: true + parent: PartHuman + components: + - type: Sprite + netsync: false + sprite: Mobs/Species/Human/parts.rsi + state: "r_foot" + - type: Icon + sprite: Mobs/Species/Human/parts.rsi + state: "r_foot" + - type: BodyPart + plural: "right human feet" + partType: Foot + size: 2 + compatibility: Biological + symmetry: Right + - type: BiologicalSurgeryData + - type: Damageable + damageContainer: biologicalDamageContainer + resistances: defaultResistances + criticalThreshold: 30 + deadThreshold: 60 diff --git a/Resources/Prototypes/Body/Presets/basic_human.yml b/Resources/Prototypes/Body/Presets/basic_human.yml index 38e65813df..d4619f693d 100644 --- a/Resources/Prototypes/Body/Presets/basic_human.yml +++ b/Resources/Prototypes/Body/Presets/basic_human.yml @@ -1,14 +1,14 @@ - type: bodyPreset - name: "basic human" - id: bodyPreset.BasicHuman + name: "human" + id: HumanPreset partIDs: - head: bodyPart.Head.BasicHuman - torso: bodyPart.Torso.BasicHuman - right arm: bodyPart.RArm.BasicHuman - left arm: bodyPart.LArm.BasicHuman - right hand: bodyPart.RHand.BasicHuman - left hand: bodyPart.LHand.BasicHuman - right leg: bodyPart.RLeg.BasicHuman - left leg: bodyPart.LLeg.BasicHuman - right foot: bodyPart.RFoot.BasicHuman - left foot: bodyPart.LFoot.BasicHuman + head: HeadHuman + torso: TorsoHuman + right arm: RightArmHuman + left arm: LeftArmHuman + right hand: RightHandHuman + left hand: LeftHandHuman + right leg: RightLegHuman + left leg: LeftLegHuman + right foot: RightFootHuman + left foot: LeftFootHuman diff --git a/Resources/Prototypes/Body/Templates/humanoid.yml b/Resources/Prototypes/Body/Templates/humanoid.yml index 856c995139..f840fd681a 100644 --- a/Resources/Prototypes/Body/Templates/humanoid.yml +++ b/Resources/Prototypes/Body/Templates/humanoid.yml @@ -1,5 +1,5 @@ - type: bodyTemplate - id: bodyTemplate.Humanoid + id: HumanoidTemplate name: "humanoid" centerSlot: "torso" slots: @@ -29,16 +29,3 @@ - left foot right leg: - right foot - layers: - head: "enum.HumanoidVisualLayers.Head" - torso: "enum.HumanoidVisualLayers.Chest" - left arm: "enum.HumanoidVisualLayers.LArm" - left hand: "enum.HumanoidVisualLayers.LHand" - right arm: "enum.HumanoidVisualLayers.RArm" - right hand: "enum.HumanoidVisualLayers.RHand" - left leg: "enum.HumanoidVisualLayers.LLeg" - left foot: "enum.HumanoidVisualLayers.LFoot" - right leg: "enum.HumanoidVisualLayers.RLeg" - right foot: "enum.HumanoidVisualLayers.RFoot" - mechanismLayers: - mechanism.Eyes.BasicHuman: "enum.HumanoidVisualLayers.Eyes" diff --git a/Resources/Prototypes/Body/Templates/quadrupedal.yml b/Resources/Prototypes/Body/Templates/quadrupedal.yml index 2eec9a12b6..6d32ea27bd 100644 --- a/Resources/Prototypes/Body/Templates/quadrupedal.yml +++ b/Resources/Prototypes/Body/Templates/quadrupedal.yml @@ -1,5 +1,5 @@ - type: bodyTemplate - id: bodyTemplate.Quadrupedal + id: QuadrupedalTemplate name: "quadrupedal" centerSlot: "torso" slots: diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 48db003704..31478ee875 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -39,31 +39,31 @@ netsync: false drawdepth: Mobs layers: - - map: ["enum.HumanoidVisualLayers.Chest"] + - map: [ "enum.HumanoidVisualLayers.Chest" ] color: "#e8b59b" sprite: Mobs/Species/Human/parts.rsi state: torso_m - - map: ["enum.HumanoidVisualLayers.Head"] + - map: [ "enum.HumanoidVisualLayers.Head" ] color: "#e8b59b" sprite: Mobs/Species/Human/parts.rsi state: head_m - - map: ["enum.HumanoidVisualLayers.Eyes"] + - map: [ "enum.HumanoidVisualLayers.Eyes" ] color: "#008800" sprite: Mobs/Customization/eyes.rsi state: eyes - - map: ["enum.HumanoidVisualLayers.RArm"] + - map: [ "enum.HumanoidVisualLayers.RArm" ] color: "#e8b59b" sprite: Mobs/Species/Human/parts.rsi state: r_arm - - map: ["enum.HumanoidVisualLayers.LArm"] + - map: [ "enum.HumanoidVisualLayers.LArm" ] color: "#e8b59b" sprite: Mobs/Species/Human/parts.rsi state: l_arm - - map: ["enum.HumanoidVisualLayers.RLeg"] + - map: [ "enum.HumanoidVisualLayers.RLeg" ] color: "#e8b59b" sprite: Mobs/Species/Human/parts.rsi state: r_leg - - map: ["enum.HumanoidVisualLayers.LLeg"] + - map: [ "enum.HumanoidVisualLayers.LLeg" ] color: "#e8b59b" sprite: Mobs/Species/Human/parts.rsi state: l_leg @@ -71,58 +71,50 @@ sprite: Mobs/Species/Human/parts.rsi state: l_leg - shader: StencilMask - map: ["enum.HumanoidVisualLayers.StencilMask"] + map: [ "enum.HumanoidVisualLayers.StencilMask" ] sprite: Mobs/Customization/masking_helpers.rsi state: female_full visible: false - - map: ["enum.Slots.INNERCLOTHING"] + - map: [ "enum.Slots.INNERCLOTHING" ] shader: StencilDraw - - map: ["enum.HumanoidVisualLayers.LHand"] + - map: [ "enum.HumanoidVisualLayers.LHand" ] color: "#e8b59b" sprite: Mobs/Species/Human/parts.rsi state: l_hand - - map: ["enum.HumanoidVisualLayers.RHand"] + - map: [ "enum.HumanoidVisualLayers.RHand" ] color: "#e8b59b" sprite: Mobs/Species/Human/parts.rsi state: r_hand - - map: ["enum.HumanoidVisualLayers.LFoot"] + - map: [ "enum.HumanoidVisualLayers.LFoot" ] color: "#e8b59b" sprite: Mobs/Species/Human/parts.rsi state: l_foot - - map: ["enum.HumanoidVisualLayers.RFoot"] + - map: [ "enum.HumanoidVisualLayers.RFoot" ] color: "#e8b59b" sprite: Mobs/Species/Human/parts.rsi state: r_foot - - map: ["enum.HumanoidVisualLayers.Handcuffs"] + - map: [ "enum.HumanoidVisualLayers.Handcuffs" ] color: "#ffffff" sprite: Objects/Misc/handcuffs.rsi state: body-overlay-2 visible: false - - map: ["enum.Slots.IDCARD"] - - map: ["enum.Slots.GLOVES"] - - map: ["enum.Slots.SHOES"] - - map: ["enum.Slots.EARS"] - - map: ["enum.Slots.OUTERCLOTHING"] - - map: ["enum.Slots.EYES"] - - map: ["enum.Slots.BELT"] - - map: ["enum.Slots.NECK"] - - map: ["enum.Slots.BACKPACK"] - - map: ["enum.HumanoidVisualLayers.FacialHair"] + - map: [ "enum.Slots.IDCARD" ] + - map: [ "enum.Slots.GLOVES" ] + - map: [ "enum.Slots.SHOES" ] + - map: [ "enum.Slots.EARS" ] + - map: [ "enum.Slots.OUTERCLOTHING" ] + - map: [ "enum.Slots.EYES" ] + - map: [ "enum.Slots.BELT" ] + - map: [ "enum.Slots.NECK" ] + - map: [ "enum.Slots.BACKPACK" ] + - map: [ "enum.HumanoidVisualLayers.FacialHair" ] state: shaved sprite: Mobs/Customization/human_facial_hair.rsi - - map: ["enum.HumanoidVisualLayers.Hair"] + - map: [ "enum.HumanoidVisualLayers.Hair" ] state: bald sprite: Mobs/Customization/human_hair.rsi - - map: ["enum.Slots.MASK"] - - map: ["enum.Slots.HEAD"] - - map: ["enum.CreamPiedVisualLayers.Pie"] - sprite: Effects/creampie.rsi - state: creampie_human - visible: false - - map: ["enum.FireVisualLayers.Fire"] - sprite: Mobs/Effects/onfire.rsi - state: Generic_mob_burning - visible: false + - map: [ "enum.Slots.MASK" ] + - map: [ "enum.Slots.HEAD" ] - type: Physics mass: 85 @@ -147,11 +139,13 @@ currentTemperature: 310.15 specificHeat: 42 tempDamageCoefficient: 0.1 - - type: BodyManager + - type: HumanoidAppearance + - type: Body criticalThreshold: 100 deadThreshold: 200 - baseTemplate: bodyTemplate.Humanoid - basePreset: bodyPreset.BasicHuman + template: HumanoidTemplate + preset: HumanPreset + centerSlot: torso - type: Metabolism metabolismHeat: 5000 radiatedHeat: 400 @@ -182,7 +176,6 @@ - type: Teleportable - type: CharacterInfo - type: FootstepSound - - type: HumanoidAppearance - type: AnimationPlayer - type: Buckle - type: UnarmedCombat @@ -197,11 +190,10 @@ - type: Strippable - type: UserInterface interfaces: - - key: enum.StrippingUiKey.Key - type: StrippableBoundUserInterface - - key: enum.AcceptCloningUiKey.Key - type: AcceptCloningBoundUserInterface - + - key: enum.StrippingUiKey.Key + type: StrippableBoundUserInterface + - key: enum.AcceptCloningUiKey.Key + type: AcceptCloningBoundUserInterface - type: entity save: false @@ -218,6 +210,7 @@ - type: Sprite netsync: false drawdepth: Mobs + # TODO BODY Turn these into individual body parts? layers: - map: ["enum.HumanoidVisualLayers.Chest"] color: "#e8b59b" @@ -292,24 +285,25 @@ mass: 85 - type: Collidable shapes: - - !type:PhysShapeAabb - bounds: "-0.35,-0.35,0.35,0.35" - mask: - - Impassable - - MobImpassable - - VaultImpassable - - SmallImpassable - layer: - - MobImpassable - - type: BodyManager + - !type:PhysShapeAabb + bounds: "-0.35,-0.35,0.35,0.35" + mask: + - Impassable + - MobImpassable + - VaultImpassable + - SmallImpassable + layer: + - MobImpassable + - type: HumanoidAppearance + - type: Body criticalThreshold: 100 deadThreshold: 200 - baseTemplate: bodyTemplate.Humanoid - basePreset: bodyPreset.BasicHuman + template: HumanoidTemplate + preset: HumanPreset + centerSlot: torso - type: MobStateManager - type: Appearance visuals: - - type: RotationVisualizer - - type: HumanoidAppearance + - type: RotationVisualizer - type: Grammar proper: true diff --git a/Resources/Prototypes/Entities/Mobs/body_system_dropped_abstract.yml b/Resources/Prototypes/Entities/Mobs/body_system_dropped_abstract.yml deleted file mode 100644 index 53d16d4d35..0000000000 --- a/Resources/Prototypes/Entities/Mobs/body_system_dropped_abstract.yml +++ /dev/null @@ -1,27 +0,0 @@ -- type: entity - name: "basedroppedbodypart" - parent: BaseItem - id: BaseDroppedBodyPart - abstract: true - components: - - type: DroppedBodyPart - - type: Sprite - texture: Mobs/Species/Human/parts.rsi/torso_m.png - - type: UserInterface - interfaces: - - key: enum.GenericSurgeryUiKey.Key - type: GenericSurgeryBoundUserInterface - -- type: entity - name: "basedroppedmechanism" - parent: BaseItem - id: BaseDroppedMechanism - abstract: true - components: - - type: DroppedMechanism - - type: Sprite - texture: Mobs/Species/Human/parts.rsi/torso_m.png - - type: UserInterface - interfaces: - - key: enum.GenericSurgeryUiKey.Key - type: GenericSurgeryBoundUserInterface diff --git a/Resources/Prototypes/Entities/Objects/Tools/surgery.yml b/Resources/Prototypes/Entities/Objects/Tools/surgery.yml index 6f4e6a3f71..45474372e2 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/surgery.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/surgery.yml @@ -8,8 +8,8 @@ baseOperateTime: 5 - type: UserInterface interfaces: - - key: enum.GenericSurgeryUiKey.Key - type: GenericSurgeryBoundUserInterface + - key: enum.SurgeryUIKey.Key + type: SurgeryBoundUserInterface - type: entity name: scalpel