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

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

@@ -0,0 +1,329 @@
#nullable enable
using System;
using System.Linq;
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;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
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} <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 without arguments.");
return;
}
if (player.AttachedEntity == null)
{
shell.SendText(player, "You don't have an entity to add a hand to.");
return;
}
entity = player.AttachedEntity;
hand = entityManager.SpawnEntity(DefaultHandPrototype, entity.Transform.Coordinates);
break;
}
case 1:
{
if (EntityUid.TryParse(args[0], out var uid))
{
if (!entityManager.TryGetEntity(uid, out var parsedEntity))
{
shell.SendText(player, $"No entity found with uid {uid}");
return;
}
entity = parsedEntity;
hand = entityManager.SpawnEntity(DefaultHandPrototype, entity.Transform.Coordinates);
}
else
{
if (player == null)
{
shell.SendText(player,
"You must specify an entity to add a hand to when using this command from the server terminal.");
return;
}
if (player.AttachedEntity == null)
{
shell.SendText(player, "You don't have an entity to add a hand to.");
return;
}
entity = player.AttachedEntity;
hand = entityManager.SpawnEntity(args[0], entity.Transform.Coordinates);
}
break;
}
case 2:
{
if (!EntityUid.TryParse(args[0], out var uid))
{
shell.SendText(player, $"{args[0]} is not a valid entity uid.");
return;
}
if (!entityManager.TryGetEntity(uid, out var parsedEntity))
{
shell.SendText(player, $"No entity exists with uid {uid}.");
return;
}
entity = parsedEntity;
if (!prototypeManager.HasIndex<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." : ".")}";
shell.SendText(player, text);
return;
}
if (!hand.TryGetComponent(out IBodyPart? part))
{
shell.SendText(player, $"Hand entity {hand} does not have a {nameof(IBodyPart)} component.");
return;
}
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}";
shell.SendText(player, response);
}
}
class RemoveHandCommand : IClientCommand
{
public string Command => "removehand";
public string Description => "Removes a hand from your entity.";
public string Help => $"Usage: {Command}";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "Only a player can run this command.");
return;
}
if (player.AttachedEntity == null)
{
shell.SendText(player, "You have no entity.");
return;
}
if (!player.AttachedEntity.TryGetBody(out var body))
{
var random = IoCManager.Resolve<IRobustRandom>();
var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
shell.SendText(player, text);
return;
}
var hand = body.Parts.FirstOrDefault(x => x.Value.PartType == BodyPartType.Hand);
if (hand.Value.Equals(default))
{
shell.SendText(player, "You have no hands.");
}
else
{
body.RemovePart(hand.Value, true);
}
}
}
class DestroyMechanismCommand : IClientCommand
{
public string Command => "destroymechanism";
public string Description => "Destroys a mechanism from your entity";
public string Help => $"Usage: {Command} <mechanism>";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "Only a player can run this command.");
return;
}
if (args.Length == 0)
{
shell.SendText(player, Help);
return;
}
if (player.AttachedEntity == null)
{
shell.SendText(player, "You have no entity.");
return;
}
if (!player.AttachedEntity.TryGetBody(out var body))
{
var random = IoCManager.Resolve<IRobustRandom>();
var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
shell.SendText(player, text);
return;
}
var mechanismName = string.Join(" ", args).ToLowerInvariant();
foreach (var part in body.Parts.Values)
foreach (var mechanism in part.Mechanisms)
{
if (mechanism.Name.ToLowerInvariant() == mechanismName)
{
part.DeleteMechanism(mechanism);
shell.SendText(player, $"Mechanism with name {mechanismName} has been destroyed.");
return;
}
}
shell.SendText(player, $"No mechanism was found with name {mechanismName}.");
}
}
class HurtCommand : IClientCommand
{
public string Command => "hurt";
public string Description => "Ouch";
public string Help => $"Usage: {Command} <type> <amount> (<entity uid/_>) (<ignoreResistance>)";
private void SendDamageTypes(IConsoleShell shell, IPlayerSession? player)
{
var msg = "";
foreach (var dClass in Enum.GetNames(typeof(DamageClass)))
{
msg += $"\n{dClass}";
var types = Enum.Parse<DamageClass>(dClass).ToTypes();
foreach (var dType in types)
{
msg += $"\n - {dType}";
}
}
shell.SendText(player, $"Damage Types:{msg}");
}
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
// Check if we have enough for the dmg types to show
if (args.Length > 0 && args[0] == "?")
{
SendDamageTypes(shell, player);
return;
}
// Not enough args
if (args.Length < 2)
{
shell.SendText(player, Help);
return;
}
var ignoreResistance = false;
var entityUid = player != null && player.AttachedEntityUid.HasValue ? player.AttachedEntityUid.Value : EntityUid.Invalid;
if (!int.TryParse(args[1], out var amount) ||
args.Length >= 3 && args[2] != "_" && !EntityUid.TryParse(args[2], out entityUid) ||
args.Length >= 4 && !bool.TryParse(args[3], out ignoreResistance))
{
shell.SendText(player, Help);
return;
}
if (entityUid == EntityUid.Invalid)
{
shell.SendText(player, "Not a valid entity.");
return;
}
if (!IoCManager.Resolve<IEntityManager>().TryGetEntity(entityUid, out var ent))
{
shell.SendText(player, "Entity couldn't be found.");
return;
}
if (!ent.TryGetComponent(out IDamageableComponent? damageable))
{
shell.SendText(player, "Entity can't be damaged.");
return;
}
if (Enum.TryParse<DamageClass>(args[0], true, out var dmgClass))
{
if (!damageable.ChangeDamage(dmgClass, amount, ignoreResistance))
shell.SendText(player, "Something went wrong!");
return;
}
// Fall back to DamageType
else if (Enum.TryParse<DamageType>(args[0], true, out var dmgType))
{
if (!damageable.ChangeDamage(dmgType, amount, ignoreResistance))
shell.SendText(player, "Something went wrong!");
return;
}
else
{
SendDamageTypes(shell, player);
}
}
}
}

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,162 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Nutrition;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Digestive
{
/// <summary>
/// Where reagents go when ingested. Tracks ingested reagents over time, and
/// eventually transfers them to <see cref="BloodstreamComponent"/> once digested.
/// </summary>
[RegisterComponent]
public class StomachComponent : SharedStomachComponent
{
/// <summary>
/// Max volume of internal solution storage
/// </summary>
public ReagentUnit MaxVolume
{
get => Owner.TryGetComponent(out SolutionContainerComponent? solution) ? solution.MaxVolume : ReagentUnit.Zero;
set
{
if (Owner.TryGetComponent(out SolutionContainerComponent? solution))
{
solution.MaxVolume = value;
}
}
}
/// <summary>
/// Initial internal solution storage volume
/// </summary>
[ViewVariables]
private ReagentUnit _initialMaxVolume;
/// <summary>
/// Time in seconds between reagents being ingested and them being transferred
/// to <see cref="BloodstreamComponent"/>
/// </summary>
[ViewVariables]
private float _digestionDelay;
/// <summary>
/// Used to track how long each reagent has been in the stomach
/// </summary>
[ViewVariables]
private readonly List<ReagentDelta> _reagentDeltas = new List<ReagentDelta>();
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _initialMaxVolume, "maxVolume", ReagentUnit.New(100));
serializer.DataField(ref _digestionDelay, "digestionDelay", 20);
}
protected override void Startup()
{
base.Startup();
if (!Owner.EnsureComponent(out SolutionContainerComponent solution))
{
Logger.Warning($"Entity {Owner} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionContainerComponent)}");
}
solution.MaxVolume = _initialMaxVolume;
}
public bool CanTransferSolution(Solution solution)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent))
{
return false;
}
// TODO: For now no partial transfers. Potentially change by design
if (!solutionComponent.CanAddSolution(solution))
{
return false;
}
return true;
}
public bool TryTransferSolution(Solution solution)
{
if (!CanTransferSolution(solution))
return false;
var solutionComponent = Owner.GetComponent<SolutionContainerComponent>();
// Add solution to _stomachContents
solutionComponent.TryAddSolution(solution, false, true);
// Add each reagent to _reagentDeltas. Used to track how long each reagent has been in the stomach
foreach (var reagent in solution.Contents)
{
_reagentDeltas.Add(new ReagentDelta(reagent.ReagentId, reagent.Quantity));
}
return true;
}
/// <summary>
/// Updates digestion status of ingested reagents.
/// Once reagents surpass _digestionDelay they are moved to the bloodstream,
/// where they are then metabolized.
/// </summary>
/// <param name="frameTime">The time since the last update in seconds.</param>
public void Update(float frameTime)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent) ||
!Owner.TryGetComponent(out BloodstreamComponent? bloodstream))
{
return;
}
// Add reagents ready for transfer to bloodstream to transferSolution
var transferSolution = new Solution();
// Use ToList here to remove entries while iterating
foreach (var delta in _reagentDeltas.ToList())
{
//Increment lifetime of reagents
delta.Increment(frameTime);
if (delta.Lifetime > _digestionDelay)
{
solutionComponent.TryRemoveReagent(delta.ReagentId, delta.Quantity);
transferSolution.AddReagent(delta.ReagentId, delta.Quantity);
_reagentDeltas.Remove(delta);
}
}
// Transfer digested reagents to bloodstream
bloodstream.TryTransferSolution(transferSolution);
}
/// <summary>
/// Used to track quantity changes when ingesting & digesting reagents
/// </summary>
private class ReagentDelta
{
public readonly string ReagentId;
public readonly ReagentUnit Quantity;
public float Lifetime { get; private set; }
public ReagentDelta(string reagentId, ReagentUnit quantity)
{
ReagentId = reagentId;
Quantity = quantity;
Lifetime = 0.0f;
}
public void Increment(float delta) => Lifetime += delta;
}
}
}

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

@@ -1,154 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Body;
using Content.Shared.GameObjects.Components.Body;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects.Components.Body
{
public interface IBodyPartManager : IComponent
{
/// <summary>
/// The <see cref="BodyPreset"/> that this
/// <see cref="BodyManagerComponent"/>
/// is adhering to.
/// </summary>
public BodyPreset Preset { get; }
/// <summary>
/// Installs the given <see cref="DroppedBodyPartComponent"/> into the
/// given slot, deleting the <see cref="IEntity"/> afterwards.
/// </summary>
/// <returns>True if successful, false otherwise.</returns>
bool TryAddPart(string slot, DroppedBodyPartComponent part, bool force = false);
bool TryAddPart(string slot, IBodyPart part, bool force = false);
bool HasPart(string slot);
/// <summary>
/// Removes the given <see cref="IBodyPart"/> reference, potentially
/// dropping other <see cref="IBodyPart">BodyParts</see> if they
/// were hanging off of it.
/// </summary>
void RemovePart(IBodyPart part, bool drop);
/// <summary>
/// Removes the body part in slot <see cref="slot"/> from this body,
/// if one exists.
/// </summary>
/// <param name="slot">The slot to remove it from.</param>
/// <param name="drop">
/// Whether or not to drop the removed <see cref="IBodyPart"/>.
/// </param>
/// <returns>True if the part was removed, false otherwise.</returns>
bool RemovePart(string slot, bool drop);
/// <summary>
/// Removes the body part from this body, if one exists.
/// </summary>
/// <param name="part">The part to remove from this body.</param>
/// <param name="slotName">The slot that the part was in, if any.</param>
/// <returns>True if <see cref="part"/> was removed, false otherwise.</returns>
bool RemovePart(IBodyPart part, [NotNullWhen(true)] out string? slotName);
/// <summary>
/// Disconnects the given <see cref="IBodyPart"/> reference, potentially
/// dropping other <see cref="IBodyPart">BodyParts</see> if they were hanging
/// off of it.
/// </summary>
/// <returns>
/// The <see cref="IEntity"/> representing the dropped
/// <see cref="IBodyPart"/>, or null if none was dropped.
/// </returns>
IEntity? DropPart(IBodyPart part);
/// <summary>
/// Recursively searches for if <see cref="part"/> is connected to
/// the center.
/// </summary>
/// <param name="part">The body part to find the center for.</param>
/// <returns>True if it is connected to the center, false otherwise.</returns>
bool ConnectedToCenter(IBodyPart part);
/// <summary>
/// Finds the central <see cref="IBodyPart"/>, if any, of this body based on
/// the <see cref="BodyTemplate"/>. For humans, this is the torso.
/// </summary>
/// <returns>The <see cref="BodyPart"/> if one exists, null otherwise.</returns>
IBodyPart? CenterPart();
/// <summary>
/// Returns whether the given part slot name exists within the current
/// <see cref="BodyTemplate"/>.
/// </summary>
/// <param name="slot">The slot to check for.</param>
/// <returns>True if the slot exists in this body, false otherwise.</returns>
bool HasSlot(string slot);
/// <summary>
/// Finds the <see cref="IBodyPart"/> in the given <see cref="slot"/> if
/// one exists.
/// </summary>
/// <param name="slot">The part slot to search in.</param>
/// <param name="result">The body part in that slot, if any.</param>
/// <returns>True if found, false otherwise.</returns>
bool TryGetPart(string slot, [NotNullWhen(true)] out IBodyPart? result);
/// <summary>
/// Finds the slotName that the given <see cref="IBodyPart"/> resides in.
/// </summary>
/// <param name="part">The <see cref="IBodyPart"/> to find the slot for.</param>
/// <param name="slot">The slot found, if any.</param>
/// <returns>True if a slot was found, false otherwise</returns>
bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out string? slot);
/// <summary>
/// Finds the <see cref="BodyPartType"/> in the given
/// <see cref="slot"/> if one exists.
/// </summary>
/// <param name="slot">The slot to search in.</param>
/// <param name="result">
/// The <see cref="BodyPartType"/> of that slot, if any.
/// </param>
/// <returns>True if found, false otherwise.</returns>
bool TryGetSlotType(string slot, out BodyPartType result);
/// <summary>
/// Finds the names of all slots connected to the given
/// <see cref="slot"/> for the template.
/// </summary>
/// <param name="slot">The slot to search in.</param>
/// <param name="connections">The connections found, if any.</param>
/// <returns>True if the connections are found, false otherwise.</returns>
bool TryGetSlotConnections(string slot, [NotNullWhen(true)] out List<string>? connections);
/// <summary>
/// Grabs all occupied slots connected to the given slot,
/// regardless of whether the given <see cref="slot"/> is occupied.
/// </summary>
/// <param name="slot">The slot name to find connections from.</param>
/// <param name="connections">The connected body parts, if any.</param>
/// <returns>
/// True if successful, false if the slot couldn't be found on this body.
/// </returns>
bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List<IBodyPart>? connections);
/// <summary>
/// Grabs all parts connected to the given <see cref="part"/>, regardless
/// of whether the given <see cref="part"/> is occupied.
/// </summary>
/// <param name="part">The part to find connections from.</param>
/// <param name="connections">The connected body parts, if any.</param>
/// <returns>
/// True if successful, false if the part couldn't be found on this body.
/// </returns>
bool TryGetPartConnections(IBodyPart part, [NotNullWhen(true)] out List<IBodyPart>? connections);
/// <summary>
/// Grabs all <see cref="IBodyPart"/> of the given type in this body.
/// </summary>
List<IBodyPart> GetPartsOfType(BodyPartType type);
}
}

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;
protected override void OnRemoveMechanism(IMechanism mechanism)
{
base.OnRemoveMechanism(mechanism);
if (eventArgs.Target.TryGetComponent(out BodyManagerComponent? bodyManager))
if (mechanism.Owner.TryGetComponent(out SpriteComponent? sprite))
{
SendBodySlotListToUser(eventArgs, bodyManager);
sprite.Visible = true;
}
}
@@ -56,49 +59,77 @@ namespace Content.Server.GameObjects.Components.Body
{
base.Initialize();
// This is ran in Startup as entities spawned in Initialize
// are not synced to the client since they are assumed to be
// identical on it
foreach (var mechanismId in MechanismIds)
{
var entity = Owner.EntityManager.SpawnEntity(mechanismId, Owner.Transform.MapPosition);
if (!entity.TryGetComponent(out IMechanism? mechanism))
{
Logger.Error($"Entity {mechanismId} does not have a {nameof(IMechanism)} component.");
continue;
}
TryAddMechanism(mechanism, true);
}
}
protected override void Startup()
{
base.Startup();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
UserInterface.OnReceiveMessage += OnUIMessage;
}
}
public void TransferBodyPartData(BodyPart data)
{
ContainedBodyPart = data;
Owner.Name = Loc.GetString(ContainedBodyPart.Name);
if (Owner.TryGetComponent(out SpriteComponent? component))
foreach (var mechanism in Mechanisms)
{
component.LayerSetRSI(0, data.RSIPath);
component.LayerSetState(0, data.RSIState);
if (data.RSIColor.HasValue)
{
component.LayerSetColor(0, data.RSIColor.Value);
}
mechanism.Dirty();
}
}
private void SendBodySlotListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager)
public void AfterInteract(AfterInteractEventArgs eventArgs)
{
// TODO BODY
if (eventArgs.Target == null)
{
return;
}
CloseAllSurgeryUIs();
_optionsCache.Clear();
_surgeonCache = null;
_owningBodyCache = null;
if (eventArgs.Target.TryGetBody(out var body))
{
SendSlots(eventArgs, body);
}
}
private void SendSlots(AfterInteractEventArgs eventArgs, IBody body)
{
// Create dictionary to send to client (text to be shown : data sent back if selected)
var toSend = new Dictionary<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

@@ -101,7 +101,7 @@ namespace Content.Server.GameObjects.Components.Doors
/// Whether something is currently using a welder on this so DoAfter isn't spammed.
/// </summary>
private bool _beingWelded = false;
[ViewVariables(VVAccess.ReadWrite)]
private bool _canCrush = true;
@@ -147,7 +147,7 @@ namespace Content.Server.GameObjects.Components.Doors
// Disabled because it makes it suck hard to walk through double doors.
if (entity.HasComponent<ISharedBodyManagerComponent>())
if (entity.HasComponent<IBody>())
{
if (!entity.TryGetComponent<IMoverComponent>(out var mover)) return;
@@ -315,7 +315,7 @@ namespace Content.Server.GameObjects.Components.Doors
damage.ChangeDamage(DamageType.Blunt, DoorCrushDamage, false, Owner);
stun.Paralyze(DoorStunTime);
// If we hit someone, open up after stun (opens right when stun ends)
Timer.Spawn(TimeSpan.FromSeconds(DoorStunTime) - OpenTimeOne - OpenTimeTwo, Open);
break;
@@ -479,7 +479,7 @@ namespace Content.Server.GameObjects.Components.Doors
if (_beingWelded)
return false;
_beingWelded = true;
if (!await tool.UseTool(eventArgs.User, Owner, 3f, ToolQuality.Welding, 3f, () => _canWeldShut))
@@ -487,7 +487,7 @@ namespace Content.Server.GameObjects.Components.Doors
_beingWelded = false;
return false;
}
_beingWelded = false;
IsWeldedShut ^= true;
return true;

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);
headCount++;
var droppedHeads = dropped.Where(p => p.PartType == BodyPartType.Head);
foreach (var droppedHead in droppedHeads)
{
_storage.Insert(droppedHead.Owner);
headCount++;
}
}
}
var othersMessage = Loc.GetString("{0:theName} is trying to cook {0:their} head!", victim);
var othersMessage = headCount > 1
? Loc.GetString("{0:theName} is trying to cook {0:their} heads!", victim)
: Loc.GetString("{0:theName} is trying to cook {0:their} head!", victim);
victim.PopupMessageOtherClients(othersMessage);
var selfMessage = Loc.GetString("You cook your head!");
var selfMessage = headCount > 1
? Loc.GetString("You cook your heads!")
: Loc.GetString("You cook your head!");
victim.PopupMessage(selfMessage);
_currentCookTimerTime = 10;

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))
{
lung.Gasp();
foreach (var lung in lungs)
{
lung.Gasp();
}
bloodstreamAmount = bloodstream.Air.GetMoles(gas);
}
@@ -341,7 +346,7 @@ namespace Content.Server.GameObjects.Components.Metabolism
public void Update(float frameTime)
{
if (!Owner.TryGetComponent<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,42 +151,43 @@ 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))
// All stomach are full or can't handle whatever solution we have.
if (firstStomach == null)
{
if (_useSound == null)
{
return false;
}
EntitySystem.Get<AudioSystem>().PlayFromEntity(_useSound, target, AudioParams.Default.WithVolume(-2f));
target.PopupMessage(Loc.GetString("Slurp"));
UpdateAppearance();
// TODO: Account for partial transfer.
foreach (var (reagentId, quantity) in split.Contents)
{
if (!_prototypeManager.TryIndex(reagentId, out ReagentPrototype reagent)) continue;
split.RemoveReagent(reagentId, reagent.ReactionEntity(target, ReactionMethod.Ingestion, quantity));
}
stomachComponent.TryTransferSolution(split);
return true;
_contents.TryAddSolution(split);
target.PopupMessage(Loc.GetString("You've had enough {0:theName}!", Owner));
return false;
}
// Stomach was full or can't handle whatever solution we have.
_contents.TryAddSolution(split);
target.PopupMessage(Loc.GetString("You've had enough {0:theName}!", Owner));
return false;
if (_useSound != null)
{
EntitySystem.Get<AudioSystem>().PlayFromEntity(_useSound, target, AudioParams.Default.WithVolume(-2f));
}
target.PopupMessage(Loc.GetString("Slurp"));
UpdateAppearance();
// TODO: Account for partial transfer.
foreach (var (reagentId, quantity) in split.Contents)
{
if (!_prototypeManager.TryIndex(reagentId, out ReagentPrototype reagent)) continue;
split.RemoveReagent(reagentId, reagent.ReactionEntity(target, ReactionMethod.Ingestion, quantity));
}
firstStomach.TryTransferSolution(split);
return true;
}
void ILand.Land(LandEventArgs eventArgs)

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