Change all of body system to use entities and components (#2074)

* Early commit

* Early commit 2

* merging master broke my git

* does anyone even read these

* life is fleeting

* it just works

* this time passing integration tests

* Remove hashset yaml serialization for now

* You got a license for those nullables?

* No examine, no context menu, part and mechanism parenting and visibility

* Fix wrong brain sprite state

* Removing layers was a mistake

* just tear body system a new one and see if it still breathes

* Remove redundant code

* Add that comment back

* Separate damage and body, component states, stomach rework

* Add containers for body parts

* Bring layers back pls

* Fix parts magically changing color

* Reimplement sprite layer visibility

* Fix tests

* Add leg test

* Active legs is gone

Crab rave

* Merge fixes, rename DamageState to CurrentState

* Remove IShowContextMenu and ICanExamine
This commit is contained in:
DrSmugleaf
2020-10-10 15:25:13 +02:00
committed by GitHub
parent 73c730d06c
commit dd385a0511
165 changed files with 4232 additions and 4650 deletions

View File

@@ -63,9 +63,6 @@ namespace Content.Client
factory.Register<SharedResearchConsoleComponent>();
factory.Register<SharedLatheComponent>();
factory.Register<SharedSpawnPointComponent>();
factory.Register<SharedSolutionContainerComponent>();
factory.Register<SharedVendingMachineComponent>();
factory.Register<SharedWiresComponent>();
factory.Register<SharedCargoConsoleComponent>();

View File

@@ -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<DisposalUnitComponent>() ||
eventArgs.Target.HasComponent<MedicalScannerComponent>())
{
return true;
}
return false;
}
public bool ClientCanDrag(CanDragEventArgs eventArgs)
{
return true;
}
}
}

View File

@@ -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<DisposalUnitComponent>()||
eventArgs.Target.HasComponent<MedicalScannerComponent>())
{
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;
}
}
}
}

View File

@@ -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) { }
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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
{
}
}

View File

@@ -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<string, BodyScannerBodyPartData> _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();
}
}
}

View File

@@ -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<string, BodyScannerBodyPartData> _parts;
private List<string> _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<string, BodyScannerBodyPartData> parts)
public void UpdateDisplay(IEntity entity)
{
_template = template;
_parts = parts;
_slots = new List<string>();
_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("");

View File

@@ -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.
/// <summary>
/// Generic client-side UI list popup that allows users to choose from an option
/// of limbs or organs to operate on.
/// </summary>
[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;

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -3,11 +3,9 @@ using Robust.Shared.GameObjects;
namespace Content.Client.GameObjects.Components.MedicalScanner
{
[RegisterComponent]
[ComponentReference(typeof(SharedMedicalScannerComponent))]
public class MedicalScannerComponent : SharedMedicalScannerComponent
{
}
}

View File

@@ -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<SpriteComponent>();
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);
}
}
}

View File

@@ -182,6 +182,7 @@
"BreakableConstruction",
"GasCanister",
"GasCanisterPort",
"Lung",
};
}
}

View File

@@ -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<IMapManager>();
var mapId = new MapId(0);
mapManager.CreateNewMapEntity(mapId);
var entityManager = IoCManager.Resolve<IEntityManager>();
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));
});
}
}
}

View File

@@ -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<LungBehaviorComponent> 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<LungBehaviorComponent>());
Assert.True(human.TryGetComponent(out metabolism));
Assert.False(metabolism.Suffocating);
});

View File

@@ -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);

View File

@@ -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<IPrototypeManager>();
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<IConsoleShell>();
shell.ExecuteCommand($"addhand {to.Uid}");
}
}
}

View File

@@ -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));

View File

@@ -151,7 +151,7 @@ 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)
{

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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
{
/// <summary>
/// Data class representing a singular limb such as an arm or a leg.
/// Typically held within either a <see cref="BodyManagerComponent"/>,
/// which coordinates functions between BodyParts, or a
/// <see cref="DroppedBodyPartComponent"/>.
/// </summary>
public class BodyPart : IBodyPart
{
private IBodyManagerComponent? _body;
private readonly HashSet<IMechanism> _mechanisms = new HashSet<IMechanism>();
public BodyPart(BodyPartPrototype data)
{
SurgeryData = null!;
Properties = new HashSet<IExposeData>();
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();
}
}
}
}
/// <summary>
/// The <see cref="Surgery.SurgeryData"/> class currently representing this BodyPart's
/// surgery status.
/// </summary>
[ViewVariables] private SurgeryData SurgeryData { get; set; }
/// <summary>
/// How much space is currently taken up by Mechanisms in this BodyPart.
/// </summary>
[ViewVariables] private int SizeUsed { get; set; }
/// <summary>
/// List of <see cref="IExposeData"/> properties, allowing for additional
/// data classes to be attached to a limb, such as a "length" class to an arm.
/// </summary>
[ViewVariables]
private HashSet<IExposeData> 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
/// <summary>
/// Current damage dealt to this <see cref="IBodyPart"/>.
/// </summary>
[ViewVariables]
public DamageContainer Damage { get; private set; }
/// <summary>
/// Armor of this <see cref="IBodyPart"/> against damages.
/// </summary>
[ViewVariables]
public ResistanceSet Resistances { get; private set; }
/// <summary>
/// At what HP this <see cref="IBodyPart"/> destroyed.
/// </summary>
[ViewVariables]
public int DestroyThreshold { get; private set; }
/// <summary>
/// What types of BodyParts this <see cref="IBodyPart"/> can easily attach to.
/// For the most part, most limbs aren't universal and require extra work to
/// attach between types.
/// </summary>
[ViewVariables]
public BodyPartCompatibility Compatibility { get; private set; }
/// <summary>
/// Set of all <see cref="IMechanism"/> currently inside this
/// <see cref="IBodyPart"/>.
/// </summary>
[ViewVariables]
public IReadOnlyCollection<IMechanism> Mechanisms => _mechanisms;
/// <summary>
/// Represents if body part is vital for creature.
/// If the last vital body part is removed creature dies
/// </summary>
[ViewVariables]
public bool IsVital { get; private set; }
/// <summary>
/// This method is called by
/// <see cref="IBodyManagerComponent.PreMetabolism"/> before
/// <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public void PreMetabolism(float frameTime)
{
foreach (var mechanism in Mechanisms)
{
mechanism.PreMetabolism(frameTime);
}
}
/// <summary>
/// This method is called by
/// <see cref="IBodyManagerComponent.PostMetabolism"/> after
/// <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public void PostMetabolism(float frameTime)
{
foreach (var mechanism in Mechanisms)
{
mechanism.PreMetabolism(frameTime);
}
}
/// <summary>
/// Attempts to add the given <see cref="BodyPartProperty"/>.
/// </summary>
/// <returns>
/// True if a <see cref="BodyPartProperty"/> of that type doesn't exist,
/// false otherwise.
/// </returns>
public bool TryAddProperty(BodyPartProperty property)
{
if (HasProperty(property.GetType()))
{
return false;
}
Properties.Add(property);
return true;
}
/// <summary>
/// Attempts to retrieve the given <see cref="BodyPartProperty"/> type.
/// The resulting <see cref="BodyPartProperty"/> will be null if unsuccessful.
/// </summary>
/// <param name="property">The property if found, null otherwise.</param>
/// <typeparam name="T">The type of the property to find.</typeparam>
/// <returns>True if successful, false otherwise.</returns>
public bool TryGetProperty<T>([NotNullWhen(true)] out T? property) where T : BodyPartProperty
{
property = (T?) Properties.FirstOrDefault(x => x.GetType() == typeof(T));
return property != null;
}
/// <summary>
/// Attempts to retrieve the given <see cref="BodyPartProperty"/> type.
/// The resulting <see cref="BodyPartProperty"/> will be null if unsuccessful.
/// </summary>
/// <returns>True if successful, false otherwise.</returns>
public bool TryGetProperty(Type propertyType, [NotNullWhen(true)] out BodyPartProperty? property)
{
property = (BodyPartProperty?) Properties.First(x => x.GetType() == propertyType);
return property != null;
}
/// <summary>
/// Checks if the given type <see cref="T"/> is on this <see cref="IBodyPart"/>.
/// </summary>
/// <typeparam name="T">
/// The subtype of <see cref="BodyPartProperty"/> to look for.
/// </typeparam>
/// <returns>
/// True if this <see cref="IBodyPart"/> has a property of type
/// <see cref="T"/>, false otherwise.
/// </returns>
public bool HasProperty<T>() where T : BodyPartProperty
{
return Properties.Count(x => x.GetType() == typeof(T)) > 0;
}
/// <summary>
/// Checks if a subtype of <see cref="BodyPartProperty"/> is on this
/// <see cref="IBodyPart"/>.
/// </summary>
/// <param name="propertyType">
/// The subtype of <see cref="BodyPartProperty"/> to look for.
/// </param>
/// <returns>
/// True if this <see cref="IBodyPart"/> has a property of type
/// <see cref="propertyType"/>, false otherwise.
/// </returns>
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);
}
/// <summary>
/// Tries to install a mechanism onto this body part.
/// Call <see cref="TryInstallDroppedMechanism"/> instead if you want to
/// easily install an <see cref="IEntity"/> with a
/// <see cref="DroppedMechanismComponent"/>.
/// </summary>
/// <param name="mechanism">The mechanism to try to install.</param>
/// <returns>
/// True if successful, false if there was an error
/// (e.g. not enough room in <see cref="IBodyPart"/>).
/// </returns>
private bool TryInstallMechanism(IMechanism mechanism)
{
if (!CanInstallMechanism(mechanism))
{
return false;
}
AddMechanism(mechanism);
return true;
}
/// <summary>
/// Tries to install a <see cref="DroppedMechanismComponent"/> into this
/// <see cref="IBodyPart"/>, potentially deleting the dropped
/// <see cref="IEntity"/>.
/// </summary>
/// <param name="droppedMechanism">The mechanism to install.</param>
/// <returns>
/// True if successful, false if there was an error
/// (e.g. not enough room in <see cref="BodyPart"/>).
/// </returns>
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<IEntityManager>();
var position = dropLocation.Transform.Coordinates;
var mechanismEntity = entityManager.SpawnEntity("BaseDroppedMechanism", position);
dropped = mechanismEntity.GetComponent<DroppedMechanismComponent>();
dropped.InitializeDroppedMechanism(mechanismTarget);
return true;
}
/// <summary>
/// Tries to destroy the given <see cref="IMechanism"/> in this
/// <see cref="IBodyPart"/>. Does NOT spawn a dropped entity.
/// </summary>
/// <summary>
/// Tries to destroy the given <see cref="IMechanism"/> in this
/// <see cref="IBodyPart"/>.
/// </summary>
/// <param name="mechanismTarget">The mechanism to destroy.</param>
/// <returns>True if successful, false otherwise.</returns>
public bool DestroyMechanism(IMechanism mechanismTarget)
{
if (!RemoveMechanism(mechanismTarget))
{
return false;
}
return true;
}
public bool SurgeryCheck(SurgeryType surgery)
{
return SurgeryData.CheckSurgery(surgery);
}
/// <summary>
/// Attempts to perform surgery on this <see cref="IBodyPart"/> with the given
/// tool.
/// </summary>
/// <returns>True if successful, false if there was an error.</returns>
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<IReflectionManager>().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);
}
/// <summary>
/// Tries to remove the given <see cref="mechanism"/> from this
/// <see cref="IBodyPart"/>.
/// </summary>
/// <param name="mechanism">The mechanism to remove.</param>
/// <returns>True if it was removed, false otherwise.</returns>
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<IReflectionManager>().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;
}
/// <summary>
/// Loads the given <see cref="BodyPartPrototype"/>.
/// Current data on this <see cref="IBodyPart"/> will be overwritten!
/// </summary>
protected virtual void LoadFromPrototype(BodyPartPrototype data)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
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<IDynamicTypeFactory>().CreateInstance<SurgeryData>(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<HealthChangeData> changes)
{
// TODO
}
public bool SpawnDropped([NotNullWhen(true)] out IEntity dropped)
{
dropped = default!;
if (Body == null)
{
return false;
}
dropped = IoCManager.Resolve<IEntityManager>().SpawnEntity("BaseDroppedBodyPart", Body.Owner.Transform.Coordinates);
dropped.GetComponent<DroppedBodyPartComponent>().TransferBodyPartData(this);
return true;
}
}
}

View File

@@ -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
{
/// <summary>
/// Stores data on what <see cref="BodyPartPrototype"></see> should
/// fill a BodyTemplate.
/// Used for loading complete body presets, like a "basic human" with all
/// human limbs.
/// </summary>
public class BodyPreset
{
[ViewVariables] public bool Initialized { get; private set; }
[ViewVariables] public string Name { get; protected set; }
/// <summary>
/// Maps a template slot to the ID of the <see cref="IBodyPart"/>
/// that should fill it. E.g. "right arm" : "BodyPart.arm.basic_human".
/// </summary>
[ViewVariables]
public Dictionary<string, string> 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;
}
}
}

View File

@@ -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
{
/// <summary>
/// This class is a data capsule representing the standard format of a
/// <see cref="BodyManagerComponent"/>.
/// For instance, the "humanoid" BodyTemplate defines two arms, each
/// connected to a torso and so on.
/// Capable of loading data from a <see cref="BodyTemplatePrototype"/>.
/// </summary>
public class BodyTemplate
{
[ViewVariables] public bool Initialized { get; private set; }
[ViewVariables] public string Name { get; private set; } = "";
/// <summary>
/// The name of the center BodyPart. For humans, this is set to "torso".
/// Used in many calculations.
/// </summary>
[ViewVariables]
public string CenterSlot { get; set; } = "";
/// <summary>
/// Maps all parts on this template to its BodyPartType.
/// For instance, "right arm" is mapped to "BodyPartType.arm" on the humanoid
/// template.
/// </summary>
[ViewVariables]
public Dictionary<string, BodyPartType> Slots { get; private set; } = new Dictionary<string, BodyPartType>();
/// <summary>
/// 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".
/// </summary>
[ViewVariables]
public Dictionary<string, List<string>> Connections { get; private set; } = new Dictionary<string, List<string>>();
[ViewVariables]
public Dictionary<string, string> Layers { get; private set; } = new Dictionary<string, string>();
[ViewVariables]
public Dictionary<string, string> MechanismLayers { get; private set; } = new Dictionary<string, string>();
public bool Equals(BodyTemplate other)
{
return GetHashCode() == other.GetHashCode();
}
/// <summary>
/// Checks if the given slot exists in this <see cref="BodyTemplate"/>.
/// </summary>
/// <returns>True if it does, false otherwise.</returns>
public bool HasSlot(string slotName)
{
return Slots.Keys.Any(slot => slot == slotName);
}
/// <summary>
/// Calculates the hash code for this instance of <see cref="BodyTemplate"/>.
/// It does not matter in which order the Connections or Slots are defined.
/// </summary>
/// <returns>
/// An integer unique to this <see cref="BodyTemplate"/>'s layout.
/// </returns>
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<int>();
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<string, BodyPartType>(prototype.Slots);
Connections = new Dictionary<string, List<string>>(prototype.Connections);
Layers = new Dictionary<string, string>(prototype.Layers);
MechanismLayers = new Dictionary<string, string>(prototype.MechanismLayers);
Initialized = true;
}
}
}

View File

@@ -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
{
/// <summary>
/// The body that this body part is currently in, if any.
/// </summary>
IBodyManagerComponent? Body { get; set; }
/// <summary>
/// <see cref="BodyPartType"/> that this <see cref="IBodyPart"/> is considered
/// to be.
/// For example, <see cref="BodyPartType.Arm"/>.
/// </summary>
BodyPartType PartType { get; }
/// <summary>
/// The name of this <see cref="IBodyPart"/>, often displayed to the user.
/// For example, it could be named "advanced robotic arm".
/// </summary>
public string Name { get; }
/// <summary>
/// Plural version of this <see cref="IBodyPart"/> name.
/// </summary>
public string Plural { get; }
/// <summary>
/// Determines many things: how many mechanisms can be fit inside this
/// <see cref="IBodyPart"/>, whether a body can fit through tiny crevices,
/// etc.
/// </summary>
int Size { get; }
/// <summary>
/// Max HP of this <see cref="IBodyPart"/>.
/// </summary>
int MaxDurability { get; }
/// <summary>
/// Current HP of this <see cref="IBodyPart"/> based on sum of all damage types.
/// </summary>
int CurrentDurability { get; }
/// <summary>
/// Collection of all <see cref="IMechanism"/>s currently inside this
/// <see cref="IBodyPart"/>.
/// To add and remove from this list see <see cref="AddMechanism"/> and
/// <see cref="RemoveMechanism"/>
/// </summary>
IReadOnlyCollection<IMechanism> Mechanisms { get; }
/// <summary>
/// Path to the RSI that represents this <see cref="IBodyPart"/>.
/// </summary>
public string RSIPath { get; }
/// <summary>
/// RSI state that represents this <see cref="IBodyPart"/>.
/// </summary>
public string RSIState { get; }
/// <summary>
/// RSI map keys that this body part changes on the sprite.
/// </summary>
public Enum? RSIMap { get; set; }
/// <summary>
/// RSI color of this body part.
/// </summary>
// TODO: SpriteComponent rework
public Color? RSIColor { get; set; }
/// <summary>
/// If body part is vital
/// </summary>
public bool IsVital { get; }
bool HasProperty<T>() where T : BodyPartProperty;
bool HasProperty(Type type);
bool TryGetProperty<T>([NotNullWhen(true)] out T? property) where T : BodyPartProperty;
void PreMetabolism(float frameTime);
void PostMetabolism(float frameTime);
bool SpawnDropped([NotNullWhen(true)] out IEntity? dropped);
/// <summary>
/// Checks if the given <see cref="SurgeryType"/> can be used on
/// the current state of this <see cref="IBodyPart"/>.
/// </summary>
/// <returns>True if it can be used, false otherwise.</returns>
bool SurgeryCheck(SurgeryType surgery);
/// <summary>
/// Checks if another <see cref="IBodyPart"/> can be connected to this one.
/// </summary>
/// <param name="part">The part to connect.</param>
/// <returns>True if it can be connected, false otherwise.</returns>
bool CanAttachPart(IBodyPart part);
/// <summary>
/// Checks if a <see cref="IMechanism"/> can be installed on this
/// <see cref="IBodyPart"/>.
/// </summary>
/// <returns>True if it can be installed, false otherwise.</returns>
bool CanInstallMechanism(IMechanism mechanism);
/// <summary>
/// Tries to remove the given <see cref="IMechanism"/> reference from
/// this <see cref="IBodyPart"/>.
/// </summary>
/// <returns>
/// The newly spawned <see cref="DroppedMechanismComponent"/>, or null
/// if there was an error in spawning the entity or removing the mechanism.
/// </returns>
bool TryDropMechanism(IEntity dropLocation, IMechanism mechanismTarget,
[NotNullWhen(true)] out DroppedMechanismComponent dropped);
bool DestroyMechanism(IMechanism mechanism);
}
}

View File

@@ -1,9 +0,0 @@
namespace Content.Server.Body.Mechanisms.Behaviors
{
/// <summary>
/// The behaviors of a brain, inhabitable by a player.
/// </summary>
public class BrainBehavior : MechanismBehavior
{
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
{
/// <summary>
/// The behaviors a mechanism performs.
/// </summary>
public abstract class MechanismBehavior
{
private bool Initialized { get; set; }
private bool Removed { get; set; }
/// <summary>
/// The network, if any, that this behavior forms when its mechanism is
/// added and destroys when its mechanism is removed.
/// </summary>
protected virtual Type? Network { get; } = null;
/// <summary>
/// Upward reference to the parent <see cref="Mechanisms.Mechanism"/> that this
/// behavior is attached to.
/// </summary>
protected Mechanism Mechanism { get; private set; } = null!;
/// <summary>
/// Called by a <see cref="Mechanism"/> to initialize this behavior.
/// </summary>
/// <param name="mechanism">The mechanism that owns this behavior.</param>
/// <exception cref="InvalidOperationException">
/// If the mechanism has already been initialized.
/// </exception>
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;
}
/// <summary>
/// Called when a behavior is removed from a <see cref="Mechanism"/>.
/// </summary>
public void Remove()
{
OnRemove();
TryRemoveNetwork(Mechanism.Body);
Mechanism = null!;
Removed = true;
}
/// <summary>
/// Called when the containing <see cref="IBodyPart"/> is attached to a
/// <see cref="BodyManagerComponent"/>.
/// For instance, attaching a head to a body will call this on the brain inside.
/// </summary>
public void InstalledIntoBody()
{
TryAddNetwork();
OnInstalledIntoBody();
}
/// <summary>
/// Called when the parent <see cref="Mechanisms.Mechanism"/> is
/// installed into a <see cref="IBodyPart"/>.
/// For instance, putting a brain into an empty head.
/// </summary>
public void InstalledIntoPart()
{
TryAddNetwork();
OnInstalledIntoPart();
}
/// <summary>
/// Called when the containing <see cref="IBodyPart"/> is removed from a
/// <see cref="BodyManagerComponent"/>.
/// For instance, cutting off ones head will call this on the brain inside.
/// </summary>
public void RemovedFromBody(IBodyManagerComponent old)
{
OnRemovedFromBody(old);
TryRemoveNetwork(old);
}
/// <summary>
/// Called when the parent <see cref="Mechanisms.Mechanism"/> is
/// removed from a <see cref="IBodyPart"/>.
/// For instance, taking a brain out of ones head.
/// </summary>
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);
}
}
/// <summary>
/// Called by <see cref="Initialize"/> when this behavior is first initialized.
/// </summary>
protected virtual void Initialize() { }
protected virtual void OnRemove() { }
/// <summary>
/// Called when the containing <see cref="IBodyPart"/> is attached to a
/// <see cref="BodyManagerComponent"/>.
/// For instance, attaching a head to a body will call this on the brain inside.
/// </summary>
protected virtual void OnInstalledIntoBody() { }
/// <summary>
/// Called when the parent <see cref="Mechanisms.Mechanism"/> is
/// installed into a <see cref="IBodyPart"/>.
/// For instance, putting a brain into an empty head.
/// </summary>
protected virtual void OnInstalledIntoPart() { }
/// <summary>
/// Called when the containing <see cref="IBodyPart"/> is removed from a
/// <see cref="BodyManagerComponent"/>.
/// For instance, cutting off ones head will call this on the brain inside.
/// </summary>
protected virtual void OnRemovedFromBody(IBodyManagerComponent old) { }
/// <summary>
/// Called when the parent <see cref="Mechanisms.Mechanism"/> is
/// removed from a <see cref="IBodyPart"/>.
/// For instance, taking a brain out of ones head.
/// </summary>
protected virtual void OnRemovedFromPart(IBodyPart old) { }
/// <summary>
/// Called every update when this behavior is connected to a
/// <see cref="BodyManagerComponent"/>, but not while in a
/// <see cref="DroppedMechanismComponent"/> or
/// <see cref="DroppedBodyPartComponent"/>,
/// before <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public virtual void PreMetabolism(float frameTime) { }
/// <summary>
/// Called every update when this behavior is connected to a
/// <see cref="BodyManagerComponent"/>, but not while in a
/// <see cref="DroppedMechanismComponent"/> or
/// <see cref="DroppedBodyPartComponent"/>,
/// after <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public virtual void PostMetabolism(float frameTime) { }
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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; }
/// <summary>
/// Professional description of the <see cref="IMechanism"/>.
/// </summary>
string Description { get; set; }
/// <summary>
/// The message to display upon examining a mob with this Mechanism installed.
/// If the string is empty (""), no message will be displayed.
/// </summary>
string ExamineMessage { get; set; }
// TODO: Make RSI properties sane
/// <summary>
/// Path to the RSI that represents this <see cref="IMechanism"/>.
/// </summary>
string RSIPath { get; set; }
/// <summary>
/// RSI state that represents this <see cref="IMechanism"/>.
/// </summary>
string RSIState { get; set; }
/// <summary>
/// Max HP of this <see cref="IMechanism"/>.
/// </summary>
int MaxDurability { get; set; }
/// <summary>
/// Current HP of this <see cref="IMechanism"/>.
/// </summary>
int CurrentDurability { get; set; }
/// <summary>
/// At what HP this <see cref="IMechanism"/> is completely destroyed.
/// </summary>
int DestroyThreshold { get; set; }
/// <summary>
/// Armor of this <see cref="IMechanism"/> against attacks.
/// </summary>
int Resistance { get; set; }
/// <summary>
/// Determines a handful of things - mostly whether this
/// <see cref="IMechanism"/> can fit into a <see cref="IBodyPart"/>.
/// </summary>
// TODO: OnSizeChanged
int Size { get; set; }
/// <summary>
/// What kind of <see cref="IBodyPart"/> this <see cref="IMechanism"/> can be
/// easily installed into.
/// </summary>
BodyPartCompatibility Compatibility { get; set; }
IReadOnlyList<MechanismBehavior> Behaviors { get; }
IBodyManagerComponent? Body { get; }
IBodyPart? Part { get; set; }
void EnsureInitialize();
void InstalledIntoBody();
void RemovedFromBody(IBodyManagerComponent old);
/// <summary>
/// This method is called by <see cref="IBodyPart.PreMetabolism"/> before
/// <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
void PreMetabolism(float frameTime);
/// <summary>
/// This method is called by <see cref="IBodyPart.PostMetabolism"/> after
/// <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
void PostMetabolism(float frameTime);
void AddBehavior(MechanismBehavior behavior);
/// <summary>
/// Removes a behavior from this mechanism.
/// </summary>
/// <param name="behavior">The behavior to remove.</param>
/// <returns>True if it was removed, false otherwise.</returns>
bool RemoveBehavior(MechanismBehavior behavior);
}
}

View File

@@ -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
{
/// <summary>
/// Data class representing a persistent item inside a <see cref="IBodyPart"/>.
/// This includes livers, eyes, cameras, brains, explosive implants,
/// binary communicators, and other things.
/// </summary>
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<MechanismBehavior>();
}
[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<MechanismBehavior> _behaviors;
[ViewVariables] public IReadOnlyList<MechanismBehavior> 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;
}
/// <summary>
/// Loads the given <see cref="MechanismPrototype"/>.
/// Current data on this <see cref="Mechanism"/> will be overwritten!
/// </summary>
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<IDynamicTypeFactory>().CreateInstance<MechanismBehavior>(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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents a "network" such as a bloodstream or electrical power that
/// is coordinated throughout an entire <see cref="BodyManagerComponent"/>.
/// </summary>
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() { }
/// <summary>
/// Called every update by
/// <see cref="BodyManagerComponent.PreMetabolism"/>.
/// </summary>
public virtual void PreMetabolism(float frameTime) { }
/// <summary>
/// Called every update by
/// <see cref="BodyManagerComponent.PostMetabolism"/>.
/// </summary>
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<T>(this IEntity entity) where T : BodyNetwork
{
if (!entity.TryGetComponent(out BodyManagerComponent body))
{
return;
}
body.EnsureNetwork<T>();
}
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<T>(this IEntity entity, out T network) where T : BodyNetwork
{
entity.TryGetBodyNetwork(typeof(T), out var unCastNetwork);
network = (T) unCastNetwork;
return network != null;
}
}
}

View File

@@ -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!;
/// <summary>
/// Mapping of body network names to their types.
/// </summary>
private readonly Dictionary<string, Type> _names = new Dictionary<string, Type>();
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<BodyNetwork>(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<BodyNetwork>(type);
}
public BodyNetwork GetNetwork(Type type)
{
if (!_names.ContainsValue(type))
{
throw new ArgumentException($"{type} is not registered.");
}
return _typeFactory.CreateInstance<BodyNetwork>(type);
}
}
}

View File

@@ -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<BloodstreamComponent>();
}
public override void OnRemove()
{
if (Owner.HasComponent<BloodstreamComponent>())
{
Owner.RemoveComponent<BloodstreamComponent>();
}
}
}
}

View File

@@ -1,28 +0,0 @@
using Content.Server.GameObjects.Components.Body.Digestive;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Body.Network
{
/// <summary>
/// Represents the system that processes food, liquids, and the reagents inside them.
/// </summary>
[UsedImplicitly]
public class DigestiveNetwork : BodyNetwork
{
public override string Name => "Digestive";
protected override void OnAdd()
{
Owner.EnsureComponent<StomachComponent>();
}
public override void OnRemove()
{
if (Owner.HasComponent<StomachComponent>())
{
Owner.RemoveComponent<StomachComponent>();
}
}
}
}

View File

@@ -1,13 +0,0 @@
using System;
namespace Content.Server.Body.Network
{
public interface IBodyNetworkFactory
{
void DoAutoRegistrations();
BodyNetwork GetNetwork(string name);
BodyNetwork GetNetwork(Type type);
}
}

View File

@@ -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<LungComponent>();
}
public override void OnRemove()
{
if (Owner.HasComponent<LungComponent>())
{
Owner.RemoveComponent<LungComponent>();
}
}
}
}

View File

@@ -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<IBodyNetworkFactory>().DoAutoRegistrations();
_gameTicker = IoCManager.Resolve<IGameTicker>();
IoCManager.Resolve<IServerNotifyManager>().Initialize();

View File

@@ -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<SharedBloodstreamComponent>())
{
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;
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}
}

View File

@@ -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} <entityUid> <handPrototypeId> / {Command} <entityUid> / {Command} <handPrototypeId> / {Command}";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (args.Length > 1)
{
shell.SendText(player, Help);
return;
}
var entityManager = IoCManager.Resolve<IEntityManager>();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
IEntity entity;
IEntity hand;
switch (args.Length)
{
case 0:
{
if (player == null)
{
shell.SendText(player, "Only a player can run this command.");
shell.SendText(player, "Only a player can run this command without arguments.");
return;
}
if (player.AttachedEntity == null)
{
shell.SendText(player, "You have no entity.");
shell.SendText(player, "You don't have an entity to add a hand to.");
return;
}
if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent? body))
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<EntityPrototype>(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 (!entity.TryGetComponent(out IBody? body))
{
var random = IoCManager.Resolve<IRobustRandom>();
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<IPrototypeManager>();
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<IRobustRandom>();
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<IRobustRandom>();
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;
}

View File

@@ -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<Container>($"{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);
}
}
}
}

View File

@@ -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<string, IBodyPart> _parts = new Dictionary<string, IBodyPart>();
[ViewVariables] public BodyPreset Preset { get; private set; } = default!;
/// <summary>
/// All <see cref="IBodyPart"></see> with <see cref="LegProperty"></see>
/// that are currently affecting move speed, mapped to how big that leg
/// they're on is.
/// </summary>
[ViewVariables]
private readonly Dictionary<IBodyPart, float> _activeLegs = new Dictionary<IBodyPart, float>();
/// <summary>
/// Maps <see cref="BodyTemplate"/> slot name to the <see cref="IBodyPart"/>
/// object filling it (if there is one).
/// </summary>
[ViewVariables]
public IReadOnlyDictionary<string, IBodyPart> Parts => _parts;
/// <summary>
/// List of all occupied slots in this body, taken from the values of
/// <see cref="Parts"/>.
/// </summary>
public IEnumerable<string> OccupiedSlots => Parts.Keys;
/// <summary>
/// List of all slots in this body, taken from the keys of
/// <see cref="Template"/> slots.
/// </summary>
public IEnumerable<string> 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<IBodyPartAdded>().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<IBodyPartRemoved>())
{
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<StandingStateSystem>().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<string>();
return TryGetSlot(part, out var result) &&
ConnectedToCenterPartRecursion(searchedSlots, result);
}
private bool ConnectedToCenterPartRecursion(ICollection<string> 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<string>? connections)
{
return Template.Connections.TryGetValue(slot, out connections);
}
public bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List<IBodyPart>? result)
{
result = null;
if (!Template.Connections.TryGetValue(slot, out var connections))
{
return false;
}
var toReturn = new List<IBodyPart>();
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<IBodyPart>? connections)
{
connections = null;
return TryGetSlot(part, out var slotName) &&
TryGetPartConnections(slotName, out connections);
}
public List<IBodyPart> GetPartsOfType(BodyPartType type)
{
var toReturn = new List<IBodyPart>();
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<LegProperty>())
{
_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;
}
}
/// <summary>
/// Called when the layout of this body changes.
/// </summary>
private void OnBodyChanged()
{
// Calculate move speed based on this body.
if (Owner.HasComponent<MovementSpeedModifierComponent>())
{
_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();
}
}
/// <summary>
/// Returns the combined length of the distance to the nearest <see cref="BodyPart"/> with a
/// <see cref="FootProperty"/>. Returns <see cref="float.MinValue"/>
/// if there is no foot found. If you consider a <see cref="BodyManagerComponent"/> a node map, then it will look for
/// a foot node from the given node. It can
/// only search through BodyParts with <see cref="ExtensionProperty"/>.
/// </summary>
public float DistanceToNearestFoot(IBodyPart source)
{
if (source.HasProperty<FootProperty>() && source.TryGetProperty<ExtensionProperty>(out var property))
{
return property.ReachDistance;
}
return LookForFootRecursion(source, new List<BodyPart>());
}
private float LookForFootRecursion(IBodyPart current,
ICollection<BodyPart> searchedParts)
{
if (!current.TryGetProperty<ExtensionProperty>(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<FootProperty>())
{
return extProperty.ReachDistance;
}
}
// Otherwise, get the recursion values of all connected BodyParts and
// store them in a list.
var distances = new List<float>();
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<float>() + extProperty.ReachDistance;
}
return float.MinValue;
// No extension property, no go.
}
}
}

View File

@@ -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
{
/// <summary>
/// Component representing a collection of <see cref="IBodyPart"></see>
/// attached to each other.
/// </summary>
[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<Type, BodyNetwork> _networks = new Dictionary<Type, BodyNetwork>();
[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
}
// /// <summary>
// /// Changes the current <see cref="BodyTemplate"/> to the given
// /// <see cref="BodyTemplate"/>.
// /// Attempts to keep previous <see cref="IBodyPart"/> if there is a
// /// slot for them in both <see cref="BodyTemplate"/>.
// /// </summary>
// public void ChangeBodyTemplate(BodyTemplatePrototype newTemplate)
// {
// foreach (var part in Parts)
// {
// // TODO: Make this work.
// }
//
// OnBodyChanged();
// }
/// <summary>
/// This method is called by <see cref="BodySystem.Update"/> before
/// <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
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);
}
}
/// <summary>
/// This method is called by <see cref="BodySystem.Update"/> after
/// <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
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;
}
/// <summary>
/// Attempts to add a <see cref="BodyNetwork"/> of the given type to this body.
/// </summary>
/// <returns>
/// True if successful, false if there was an error
/// (such as passing in an invalid type or a network of that type already
/// existing).
/// </returns>
public bool EnsureNetwork(Type networkType)
{
DebugTools.Assert(networkType.IsSubclassOf(typeof(BodyNetwork)));
var network = _bodyNetworkFactory.GetNetwork(networkType);
return EnsureNetwork(network);
}
/// <summary>
/// Attempts to add a <see cref="BodyNetwork"/> of the given type to
/// this body.
/// </summary>
/// <typeparam name="T">The type of network to add.</typeparam>
/// <returns>
/// True if successful, false if there was an error
/// (such as passing in an invalid type or a network of that type already
/// existing).
/// </returns>
public bool EnsureNetwork<T>() 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<T>() where T : BodyNetwork
{
RemoveNetwork(typeof(T));
}
/// <summary>
/// Attempts to get the <see cref="BodyNetwork"/> of the given type in this body.
/// </summary>
/// <param name="networkType">The type to search for.</param>
/// <param name="result">
/// The <see cref="BodyNetwork"/> if found, null otherwise.
/// </param>
/// <returns>True if found, false otherwise.</returns>
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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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
/// <summary>
/// Copy BodyTemplate and BodyPart data into a common data class that the client can read.
/// </summary>
private BodyScannerInterfaceState InterfaceState(BodyTemplate template, IReadOnlyDictionary<string, IBodyPart> bodyParts)
private BodyScannerUIState InterfaceState(IBody body)
{
var partsData = new Dictionary<string, BodyScannerBodyPartData>();
foreach (var (slotName, part) in bodyParts)
{
var mechanismData = new List<BodyScannerMechanismData>();
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);
}
}
}

View File

@@ -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
/// </summary>
/// <param name="solution">Solution to be transferred</param>
/// <returns>Whether or not transfer was a success</returns>
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)

View File

@@ -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
{
/// <summary>
/// Component representing a dropped, tangible <see cref="Mechanism"/> entity.
/// </summary>
[RegisterComponent]
public class DroppedMechanismComponent : Component, IAfterInteract
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public sealed override string Name => "DroppedMechanism";
private readonly Dictionary<int, object> _optionsCache = new Dictionary<int, object>();
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<BodyManagerComponent>(out var bodyManager))
{
SendBodyPartListToUser(eventArgs, bodyManager);
}
else if (eventArgs.Target.TryGetComponent<DroppedBodyPartComponent>(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<string, int>();
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<BasicActorComponent>().playerSession);
UpdateSurgeryUIBodyPartRequest(eventArgs.User.GetComponent<BasicActorComponent>().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));
}
}
/// <summary>
/// Called after the client chooses from a list of possible BodyParts that can be operated on.
/// </summary>
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<string, int> 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;
}
}
}
}

View File

@@ -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
{
/// <summary>
/// The <see cref="BodyTemplate"/> that this
/// <see cref="BodyManagerComponent"/> is adhering to.
/// </summary>
public BodyTemplate Template { get; }
/// <summary>
/// Installs the given <see cref="IBodyPart"/> into the given slot.
/// </summary>
/// <returns>True if successful, false otherwise.</returns>
bool TryAddPart(string slot, IBodyPart part, bool force = false);
bool HasPart(string slot);
/// <summary>
/// Ensures that this body has the specified network.
/// </summary>
/// <typeparam name="T">The type of the network to ensure.</typeparam>
/// <returns>
/// True if the network already existed, false if it had to be created.
/// </returns>
bool EnsureNetwork<T>() where T : BodyNetwork;
/// <summary>
/// Ensures that this body has the specified network.
/// </summary>
/// <param name="networkType">The type of the network to ensure.</param>
/// <returns>
/// True if the network already existed, false if it had to be created.
/// </returns>
bool EnsureNetwork(Type networkType);
/// <summary>
/// Removes the <see cref="BodyNetwork"/> of the given type in this body,
/// if one exists.
/// </summary>
/// <typeparam name="T">The type of the network to remove.</typeparam>
void RemoveNetwork<T>() where T : BodyNetwork;
/// <summary>
/// Removes the <see cref="BodyNetwork"/> of the given type in this body,
/// if there is one.
/// </summary>
/// <param name="networkType">The type of the network to remove.</param>
void RemoveNetwork(Type networkType);
void PreMetabolism(float frameTime);
void PostMetabolism(float frameTime);
}
}

View File

@@ -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<IBodyPart>(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<string, int>();
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));
}
}
/// <summary>
/// Called after the client chooses from a list of possible BodyParts that can be operated on.
/// </summary>
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<string, int> 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;
}
}
}
}

View File

@@ -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
{
/// <summary>
/// Component representing a dropped, tangible <see cref="BodyPart"/> entity.
/// </summary>
[RegisterComponent]
public class DroppedBodyPartComponent : Component, IAfterInteract, IBodyPartContainer
[ComponentReference(typeof(SharedBodyPartComponent))]
[ComponentReference(typeof(IBodyPart))]
public class BodyPartComponent : SharedBodyPartComponent, IAfterInteract
{
private readonly Dictionary<int, object> _optionsCache = new Dictionary<int, object>();
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;
if (eventArgs.Target.TryGetComponent(out BodyManagerComponent? bodyManager))
protected override void OnRemoveMechanism(IMechanism mechanism)
{
SendBodySlotListToUser(eventArgs, bodyManager);
base.OnRemoveMechanism(mechanism);
if (mechanism.Owner.TryGetComponent(out SpriteComponent? sprite))
{
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)
foreach (var mechanism in Mechanisms)
{
ContainedBodyPart = data;
Owner.Name = Loc.GetString(ContainedBodyPart.Name);
mechanism.Dirty();
}
}
if (Owner.TryGetComponent(out SpriteComponent? component))
public void AfterInteract(AfterInteractEventArgs eventArgs)
{
component.LayerSetRSI(0, data.RSIPath);
component.LayerSetState(0, data.RSIState);
if (data.RSIColor.HasValue)
// TODO BODY
if (eventArgs.Target == null)
{
component.LayerSetColor(0, data.RSIColor.Value);
return;
}
CloseAllSurgeryUIs();
_optionsCache.Clear();
_surgeonCache = null;
_owningBodyCache = null;
if (eventArgs.Target.TryGetBody(out var body))
{
SendSlots(eventArgs, body);
}
}
private void SendBodySlotListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager)
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<string, int>();
// 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<BasicActorComponent>().playerSession);
UpdateSurgeryUIBodyPartSlotRequest(eventArgs.User.GetComponent<BasicActorComponent>().playerSession,
BodyPartSlotRequest(eventArgs.User.GetComponent<BasicActorComponent>().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
}
/// <summary>
/// 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.
/// </summary>
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<string, int> options)
private void BodyPartSlotRequest(IPlayerSession session, Dictionary<string, int> 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;
}
}

View File

@@ -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.
/// <summary>
/// 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<string, int>();
@@ -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<DroppedBodyPartComponent>(out var droppedBodyPart))
else if (eventArgs.Target.TryGetComponent<IBodyPart>(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
/// <summary>
/// Called after the client chooses from a list of possible
/// <see cref="BodyPart"/> that can be operated on.
/// <see cref="IBodyPart"/> that can be operated on.
/// </summary>
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));
}

View File

@@ -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<StomachBehaviorComponent>(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)
{

View File

@@ -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
{

View File

@@ -27,6 +27,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// ECS component that manages a liquid solution of reagents.
/// </summary>
[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;
/// <summary>
/// The total volume of all the of the reagents in the container.
/// </summary>
[ViewVariables]
public ReagentUnit CurrentVolume => Solution.TotalVolume;
/// <summary>
/// The volume without reagents remaining in the container.
/// </summary>
[ViewVariables]
public ReagentUnit EmptyVolume => MaxVolume - CurrentVolume;
/// <summary>
/// The current blended color of all the reagents in the container.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Color SubstanceColor { get; private set; }
/// <summary>
/// The current capabilities of this container (is the top open to pour? can I inject it into another object?).
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public SolutionContainerCaps Capabilities { get; set; }
/// <summary>
/// The contained solution.
/// </summary>
[ViewVariables]
public Solution Solution { get; set; }
/// <summary>
/// The maximum volume of the container.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit MaxVolume { get; set; }
public IReadOnlyList<Solution.ReagentQuantity> 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;

View File

@@ -27,13 +27,10 @@ namespace Content.Server.GameObjects.Components.Damage
public override string Name => "Breakable";
private ActSystem _actSystem;
private DamageState _currentDamageState;
public override List<DamageState> SupportedDamageStates =>
new List<DamageState> {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()

View File

@@ -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;
/// <summary>
/// Sound played upon destruction.
/// </summary>
[ViewVariables]
protected string DestroySound { get; private set; }
public override List<DamageState> SupportedDamageStates =>
new List<DamageState> {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
/// <summary>
/// Destroys the Owner <see cref="IEntity"/>, setting
/// <see cref="IDamageableComponent.CurrentDamageState"/> to
/// <see cref="DamageState.Dead"/>
/// <see cref="IDamageableComponent.CurrentState"/> to
/// <see cref="Shared.GameObjects.Components.Damage.DamageState.Dead"/>
/// </summary>
protected void PerformDestruction()
{
_currentDamageState = DamageState.Dead;
CurrentState = DamageState.Dead;
if (!Owner.Deleted && DestroySound != string.Empty)
{

View File

@@ -63,7 +63,7 @@ namespace Content.Server.GameObjects.Components.Disposal
}
return entity.HasComponent<ItemComponent>() ||
entity.HasComponent<ISharedBodyManagerComponent>();
entity.HasComponent<IBody>();
}
public bool TryInsert(IEntity entity)

View File

@@ -145,7 +145,7 @@ namespace Content.Server.GameObjects.Components.Disposal
}
if (!entity.HasComponent<ItemComponent>() &&
!entity.HasComponent<ISharedBodyManagerComponent>())
!entity.HasComponent<IBody>())
{
return false;
}

View File

@@ -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<ISharedBodyManagerComponent>())
if (entity.HasComponent<IBody>())
{
if (!entity.TryGetComponent<IMoverComponent>(out var mover)) return;

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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<StorableComponent>() &&
!entity.HasComponent<BodyManagerComponent>())
!entity.HasComponent<IBody>())
continue;
if (!AddToContents(entity))

View File

@@ -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<BodyManagerComponent>(out var bodyManagerComponent))
if (victim.TryGetComponent<IBody>(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);
var droppedHeads = dropped.Where(p => p.PartType == BodyPartType.Head);
foreach (var droppedHead in droppedHeads)
{
_storage.Insert(droppedHead.Owner);
headCount++;
}
}
}
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);
var othersMessage = 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;

View File

@@ -163,7 +163,7 @@ namespace Content.Server.GameObjects.Components.Medical
var dead =
mind.OwnedEntity.TryGetComponent<IDamageableComponent>(out var damageable) &&
damageable.CurrentDamageState == DamageState.Dead;
damageable.CurrentState == DamageState.Dead;
if (!dead) return;

View File

@@ -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);
}
}
}

View File

@@ -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<IDamageableComponent>().CurrentDamageState);
: GetStatusFromDamageState(body.GetComponent<IDamageableComponent>().CurrentState);
}
return MedicalScannerStatus.Off;
@@ -249,7 +250,7 @@ namespace Content.Server.GameObjects.Components.Medical
public bool CanDragDropOn(DragDropEventArgs eventArgs)
{
return eventArgs.Dropped.HasComponent<BodyManagerComponent>();
return eventArgs.Dropped.HasComponent<IBody>();
}
public bool DragDropOn(DragDropEventArgs eventArgs)

View File

@@ -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<LungBehaviorComponent> lungs))
{
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<IDamageableComponent>(out var damageable) ||
damageable.CurrentDamageState == DamageState.Dead)
damageable.CurrentState == DamageState.Dead)
{
return;
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -172,7 +172,7 @@ namespace Content.Server.GameObjects.Components.Mobs
var dead =
Owner.TryGetComponent<IDamageableComponent>(out var damageable) &&
damageable.CurrentDamageState == DamageState.Dead;
damageable.CurrentState == DamageState.Dead;
if (!HasMind)
{

View File

@@ -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;
}
}

View File

@@ -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<ClimbingComponent>())
if (!user.HasComponent<ClimbingComponent>() ||
!user.TryGetComponent(out IBody body))
{
reason = Loc.GetString("You are incapable of climbing!");
return false;
}
var bodyManager = user.GetComponent<BodyManagerComponent>();
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;

View File

@@ -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,22 +151,29 @@ namespace Content.Server.GameObjects.Components.Nutrition
return false;
}
if (!target.TryGetComponent(out StomachComponent stomachComponent))
if (!target.TryGetMechanismBehaviors<StomachBehaviorComponent>(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))
{
if (_useSound == null)
// All stomach are full or can't handle whatever solution we have.
if (firstStomach == null)
{
_contents.TryAddSolution(split);
target.PopupMessage(Loc.GetString("You've had enough {0:theName}!", Owner));
return false;
}
if (_useSound != null)
{
EntitySystem.Get<AudioSystem>().PlayFromEntity(_useSound, target, AudioParams.Default.WithVolume(-2f));
}
target.PopupMessage(Loc.GetString("Slurp"));
UpdateAppearance();
@@ -176,17 +185,11 @@ namespace Content.Server.GameObjects.Components.Nutrition
split.RemoveReagent(reagentId, reagent.ReactionEntity(target, ReactionMethod.Ingestion, quantity));
}
stomachComponent.TryTransferSolution(split);
firstStomach.TryTransferSolution(split);
return true;
}
// 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;
}
void ILand.Land(LandEventArgs eventArgs)
{
if (_pressurized &&

View File

@@ -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<SharedStomachBehaviorComponent>(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<AudioSystem>()
.PlayFromEntity(_useSound, trueTarget, AudioParams.Default.WithVolume(-1f));

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -66,7 +66,7 @@ namespace Content.Server.GameObjects.Components.Recycling
private bool CanGib(IEntity entity)
{
return entity.HasComponent<ISharedBodyManagerComponent>() && !_safe && Powered;
return entity.HasComponent<IBody>() && !_safe && Powered;
}
private bool CanRecycle(IEntity entity, [MaybeNullWhen(false)] out ConstructionPrototype prototype)

View File

@@ -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()

View File

@@ -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<BodyManagerComponent>())
{
body.PreMetabolism(frameTime);
}
foreach (var metabolism in ComponentManager.EntityQuery<MetabolismComponent>())
{
metabolism.Update(frameTime);
}
foreach (var body in ComponentManager.EntityQuery<BodyManagerComponent>())
{
body.PostMetabolism(frameTime);
}
}
}
}

View File

@@ -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<SharedHeartBehaviorComponent>())
{
heart.Update(frameTime);
}
}
}
}

View File

@@ -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<SharedLungBehaviorComponent>())
{
lung.Update(frameTime);
}
}
}
}

View File

@@ -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;

View File

@@ -68,7 +68,7 @@ namespace Content.Server.GameTicking.GameRules
continue;
}
if (damageable.CurrentDamageState != DamageState.Alive)
if (damageable.CurrentState != DamageState.Alive)
{
continue;
}

View File

@@ -73,7 +73,7 @@ namespace Content.Server.GameTicking.GameRules
continue;
}
if (damageable.CurrentDamageState != DamageState.Alive)
if (damageable.CurrentState != DamageState.Alive)
{
continue;
}

View File

@@ -1,49 +0,0 @@
using System;
using Content.Server.Body;
namespace Content.Server.Interfaces.GameObjects.Components.Interaction
{
/// <summary>
/// This interface gives components behavior when a body part
/// is added to their owning entity.
/// </summary>
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; }
}
/// <summary>
/// This interface gives components behavior when a body part
/// is removed from their owning entity.
/// </summary>
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; }
}
}

View File

@@ -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;

View File

@@ -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<IPowerNetManager, PowerNetManager>();
IoCManager.Register<BlackboardManager, BlackboardManager>();
IoCManager.Register<ConsiderationsManager, ConsiderationsManager>();
IoCManager.Register<IBodyNetworkFactory, BodyNetworkFactory>();
IoCManager.Register<IAccentManager, AccentManager>();
IoCManager.Register<IConnectionManager, ConnectionManager>();
}

View File

@@ -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<IDamageableComponent>(out var damageable)
&& damageable.CurrentDamageState == DamageState.Dead) return;
&& damageable.CurrentState == DamageState.Dead) return;
var entityManager = IoCManager.Resolve<IEntityManager>();
var playerPos = player.AttachedEntity.Transform.Coordinates;

View File

@@ -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
{
/// <summary>
/// Prototype for the Mechanism class.
/// </summary>
[Prototype("mechanism")]
[Serializable, NetSerializable]
public class MechanismPrototype : IPrototype, IIndexedPrototype
{
private List<string> _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<string> 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<string>());
}
}
}

View File

@@ -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
{
/// <summary>
/// Prototype for the BodyPart class.
/// </summary>
[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<string> _mechanisms;
private string _name;
private BodyPartType _partType;
private string _plural;
private List<IExposeData> _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<IExposeData> Properties => _properties;
[ViewVariables] public List<string> 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<IExposeData>());
serializer.DataField(ref _mechanisms, "mechanisms", new List<string>());
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}");
}
}
}
}
}

View File

@@ -1,23 +0,0 @@
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Serialization;
namespace Content.Shared.Body.Part.Properties
{
/// <summary>
/// Property attachable to a <see cref="BodyPart"/>.
/// For example, this is used to define the speed capabilities of a
/// leg. The movement system will look for a LegProperty on all BodyParts.
/// </summary>
public abstract class BodyPartProperty : IExposeData
{
/// <summary>
/// Whether this property is currently active.
/// </summary>
public bool Active;
public virtual void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(ref Active, "active", true);
}
}
}

View File

@@ -1,11 +0,0 @@
namespace Content.Shared.Body.Part.Properties.Movement
{
/// <summary>
/// Defines the leg-based locomotion ability of a BodyPart. Required for humanoid-like movement. Must be connected to a
/// <see cref="BodyPart" /> with
/// <see cref="LegProperty" /> and <see cref="ExtensionProperty" /> to work.
/// </summary>
public class FootProperty : BodyPartProperty
{
}
}

View File

@@ -1,23 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Body.Part.Properties.Movement
{
/// <summary>
/// Defines the speed of humanoid-like movement. Must be connected to a
/// <see cref="BodyPart" /> with <see cref="FootProperty" /> and have
/// <see cref="ExtensionProperty" /> on the same organ and down to the foot to work.
/// </summary>
public class LegProperty : BodyPartProperty
{
/// <summary>
/// Speed (in tiles per second).
/// </summary>
public float Speed;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref Speed, "speed", 1f);
}
}
}

View File

@@ -1,21 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Body.Part.Properties.Other
{
/// <summary>
/// Defines the extension ability of a BodyPart. Used to determine things like reach distance and running speed.
/// </summary>
public class ExtensionProperty : BodyPartProperty
{
/// <summary>
/// Current reach distance (in tiles).
/// </summary>
public float ReachDistance;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref ReachDistance, "reachDistance", 2f);
}
}
}

View File

@@ -1,10 +0,0 @@
namespace Content.Shared.Body.Part.Properties.Other
{
/// <summary>
/// Defines the item grasping ability of a BodyPart. Distance is determined by the
/// <see cref="ExtensionProperty">ExtensionProperties</see> of the current BodyPart or attached BodyParts.
/// </summary>
public class GraspProperty : BodyPartProperty
{
}
}

View File

@@ -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<string, BodyScannerBodyPartData> Parts;
public readonly BodyScannerTemplateData Template;
public BodyScannerInterfaceState(Dictionary<string, BodyScannerBodyPartData> parts,
BodyScannerTemplateData template)
{
Template = template;
Parts = parts;
}
}
[Serializable, NetSerializable]
public class BodyScannerBodyPartData
{
public readonly int CurrentDurability;
public readonly int MaxDurability;
public readonly List<BodyScannerMechanismData> 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<BodyScannerMechanismData> 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<string, BodyPartType> Slots;
public BodyScannerTemplateData(string name, Dictionary<string, BodyPartType> slots)
{
Name = name;
Slots = slots;
}
}
}

Some files were not shown because too many files have changed in this diff Show More