Bodysystem and damagesystem rework (#1544)

* Things and stuff with grids, unfinished w/ code debug changes.

* Updated submodule and also lost some progress cause I fucked it up xd

* First unfinished draft of the BodySystem. Doesn't compile.

* More changes to make it compile, but still just a framework. Doesn't do anything at the moment.

* Many cleanup changes.

* Revert "Merge branch 'master' of https://github.com/GlassEclipse/space-station-14 into body_system"

This reverts commit ddd4aebbc76cf2a0b7b102f72b93d55a0816c88c, reversing
changes made to 12d0dd752706bdda8879393bd8191a1199a0c978.

* Commit human.yml

* Updated a lot of things to be more classy, more progress overall, etc. etc.

* Latest update with many changes

* Minor changes

* Fixed Travis build bug

* Adds first draft of Body Scanner console, apparently I also forgot to tie Mechanisms into body parts so now a heart just sits in the Torso like a good boy :)

* Commit rest of stuff

* Latest changes

* Latest changes again

* 14 naked cowboys

* Yay!

* Latest changes (probably doesnt compile)

* Surgery!!!!!!!!!~1116y

* Cleaned some stuff up

* More cleanup

* Refactoring of code. Basic surgery path now done.

* Removed readme, has been added to HackMD

* Fixes typo (and thus test errors)

* WIP changes, committing so I can pull latest master changes

* Still working on that god awful merge

* Latest changes

* Latest changes!!

* Beginning of refactor to BoundUserInterface

* Surgery!

* Latest changes - fixes pr change requests and random fixes

* oops

* Fixes bodypart recursion

* Beginning of work on revamping the damage system.

* More latest changes

* Latest changes

* Finished merge

* Commit before removing old healthcode

* Almost done with removing speciescomponent...

* It compiles!!!

* yahoo more work

* Fixes to make it work

* Merge conflict fixes

* Deleting species visualizer was a mistake

* IDE warnings are VERBOTEN

* makes the server not kill itself on startup, some cleanup (#1)

* Namespaces, comments and exception fixes

* Fix conveyor and conveyor switch serialization

SS14 in reactive when

* Move damage, acts and body to shared

Damage cleanup
Comment cleanup

* Rename SpeciesComponent to RotationComponent and cleanup

Damage cleanup
Comment cleanup

* Fix nullable warnings

* Address old reviews

Fix off welder suicide damage type, deathmatch and suspicion

* Fix new test fail with units being able to accept items when unpowered

* Remove RotationComponent, change references to IBodyManagerComponent

* Add a bloodstream to humans

* More cleanups

* Add body conduits, connections, connectors substances and valves

* Revert "Add body conduits, connections, connectors substances and valves"

This reverts commit 9ab0b50e6b15fe98852d7b0836c0cdbf4bd76d20.

* Implement the heart mechanism behavior with the circulatory network

* Added network property to mechanism behaviors

* Changed human organ sprites and added missing ones

* Fix tests

* Add individual body part sprite rendering

* Fix error where dropped mechanisms are not initialized

* Implement client/server body damage

* Make DamageContainer take care of raising events

* Reimplement medical scanner with the new body system

* Improve the medical scanner ui

* Merge conflict fixes

* Fix crash when colliding with something

* Fix microwave suicides and eyes sprite rendering

* Fix nullable reference error

* Fix up surgery client side

* Fix missing using from merge conflict

* Add breathing

*inhale

* Merge conflict fixes

* Fix accumulatedframetime being reset to 0 instead of decreased by the threshold

https://github.com/space-wizards/space-station-14/pull/1617

* Use and add to the new AtmosHelpers

* Fix feet

* Add proper coloring to dropped body parts

* Fix Urist's lungs being too strong

* Merge conflict fixes

* Merge conflict fixes

* Merge conflict fixes

Co-authored-by: GlassEclipse <tsymall5@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: AJCM-git <60196617+AJCM-git@users.noreply.github.com>
This commit is contained in:
DrSmugleaf
2020-08-17 01:42:42 +02:00
committed by GitHub
parent c17dd97383
commit b051261485
276 changed files with 7853 additions and 4737 deletions

View File

@@ -0,0 +1,147 @@
#nullable enable
using System.Linq;
using Content.Server.GameObjects.Components.Body;
using Content.Shared.Body.Part;
using Content.Shared.GameObjects.Components.Body;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Body
{
class AddHandCommand : IClientCommand
{
public string Command => "addhand";
public string Description => "Adds a hand to your entity.";
public string Help => $"Usage: {Command}";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "Only a player can run this command.");
return;
}
if (player.AttachedEntity == null)
{
shell.SendText(player, "You have no entity.");
return;
}
if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent body))
{
var random = IoCManager.Resolve<IRobustRandom>();
var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
shell.SendText(player, text);
return;
}
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.TryIndex("bodyPart.Hand.BasicHuman", out BodyPartPrototype prototype);
var part = new BodyPart(prototype);
var slot = part.GetHashCode().ToString();
body.Template.Slots.Add(slot, BodyPartType.Hand);
body.InstallBodyPart(part, slot);
}
}
class RemoveHandCommand : IClientCommand
{
public string Command => "removehand";
public string Description => "Removes a hand from your entity.";
public string Help => $"Usage: {Command}";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "Only a player can run this command.");
return;
}
if (player.AttachedEntity == null)
{
shell.SendText(player, "You have no entity.");
return;
}
if (!player.AttachedEntity.TryGetComponent(out BodyManagerComponent body))
{
var random = IoCManager.Resolve<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 == null)
{
shell.SendText(player, "You have no hands.");
}
else
{
body.DisconnectBodyPart(hand.Value, true);
}
}
}
class DestroyMechanismCommand : IClientCommand
{
public string Command => "destroymechanism";
public string Description => "Destroys a mechanism from your entity";
public string Help => $"Usage: {Command} <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.TryGetComponent(out BodyManagerComponent 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.DestroyMechanism(mechanism);
shell.SendText(player, $"Mechanism with name {mechanismName} has been destroyed.");
return;
}
}
shell.SendText(player, $"No mechanism was found with name {mechanismName}.");
}
}
}

View File

@@ -0,0 +1,602 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Body.Mechanisms;
using Content.Server.Body.Surgery;
using Content.Server.GameObjects.Components.Body;
using Content.Server.GameObjects.Components.Metabolism;
using Content.Shared.Body.Mechanism;
using Content.Shared.Body.Part;
using Content.Shared.Body.Part.Properties;
using Content.Shared.Damage.DamageContainer;
using Content.Shared.Damage.ResistanceSet;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body
{
/// <summary>
/// Data class representing a singular limb such as an arm or a leg.
/// Typically held within either a <see cref="BodyManagerComponent"/>,
/// which coordinates functions between BodyParts, or a
/// <see cref="DroppedBodyPartComponent"/>.
/// </summary>
public class BodyPart
{
/// <summary>
/// The body that this body part is in, if any.
/// </summary>
private BodyManagerComponent? _body;
/// <summary>
/// Set of all <see cref="Mechanism"/> currently inside this
/// <see cref="BodyPart"/>.
/// To add and remove from this list see <see cref="AddMechanism"/> and
/// <see cref="RemoveMechanism"/>
/// </summary>
private readonly HashSet<Mechanism> _mechanisms = new HashSet<Mechanism>();
public BodyPart(BodyPartPrototype data)
{
SurgeryData = null!;
Properties = new HashSet<IExposeData>();
Name = null!;
Plural = null!;
RSIPath = null!;
RSIState = null!;
RSIMap = null!;
Damage = null!;
Resistances = null!;
LoadFromPrototype(data);
}
/// <summary>
/// The body that this body part is in, if any.
/// </summary>
[ViewVariables]
public BodyManagerComponent? Body
{
get => _body;
set
{
var old = _body;
_body = value;
if (value == null && old != null)
{
foreach (var mechanism in Mechanisms)
{
mechanism.RemovedFromBody(old);
}
}
else
{
foreach (var mechanism in Mechanisms)
{
mechanism.InstalledIntoBody();
}
}
}
}
/// <summary>
/// The <see cref="Surgery.SurgeryData"/> class currently representing this BodyPart's
/// surgery status.
/// </summary>
[ViewVariables] private SurgeryData SurgeryData { get; set; }
/// <summary>
/// How much space is currently taken up by Mechanisms in this BodyPart.
/// </summary>
[ViewVariables] private int SizeUsed { get; set; }
/// <summary>
/// List of <see cref="IExposeData"/> properties, allowing for additional
/// data classes to be attached to a limb, such as a "length" class to an arm.
/// </summary>
[ViewVariables]
private HashSet<IExposeData> Properties { get; }
/// <summary>
/// The name of this <see cref="BodyPart"/>, often displayed to the user.
/// For example, it could be named "advanced robotic arm".
/// </summary>
[ViewVariables]
public string Name { get; private set; }
/// <summary>
/// Plural version of this <see cref="BodyPart"/> name.
/// </summary>
[ViewVariables]
public string Plural { get; private set; }
/// <summary>
/// Path to the RSI that represents this <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public string RSIPath { get; private set; }
/// <summary>
/// RSI state that represents this <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public string RSIState { get; private set; }
/// <summary>
/// RSI map keys that this body part changes on the sprite.
/// </summary>
[ViewVariables]
public Enum? RSIMap { get; set; }
/// <summary>
/// RSI color of this body part.
/// </summary>
// TODO: SpriteComponent rework
public Color? RSIColor { get; set; }
/// <summary>
/// <see cref="BodyPartType"/> that this <see cref="BodyPart"/> is considered
/// to be.
/// For example, <see cref="BodyPartType.Arm"/>.
/// </summary>
[ViewVariables]
public BodyPartType PartType { get; private set; }
/// <summary>
/// Determines many things: how many mechanisms can be fit inside this
/// <see cref="BodyPart"/>, whether a body can fit through tiny crevices, etc.
/// </summary>
[ViewVariables]
private int Size { get; set; }
/// <summary>
/// Max HP of this <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public int MaxDurability { get; private set; }
/// <summary>
/// Current HP of this <see cref="BodyPart"/> based on sum of all damage types.
/// </summary>
[ViewVariables]
public int CurrentDurability => MaxDurability - Damage.TotalDamage;
// TODO: Individual body part damage
/// <summary>
/// Current damage dealt to this <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public DamageContainer Damage { get; private set; }
/// <summary>
/// Armor of this <see cref="BodyPart"/> against damages.
/// </summary>
[ViewVariables]
public ResistanceSet Resistances { get; private set; }
/// <summary>
/// At what HP this <see cref="BodyPart"/> destroyed.
/// </summary>
[ViewVariables]
public int DestroyThreshold { get; private set; }
/// <summary>
/// What types of BodyParts this <see cref="BodyPart"/> can easily attach to.
/// For the most part, most limbs aren't universal and require extra work to
/// attach between types.
/// </summary>
[ViewVariables]
public BodyPartCompatibility Compatibility { get; private set; }
/// <summary>
/// Set of all <see cref="Mechanism"/> currently inside this
/// <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public IReadOnlyCollection<Mechanism> Mechanisms => _mechanisms;
/// <summary>
/// This method is called by <see cref="BodyManagerComponent.Update"/>
/// before <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public void PreMetabolism(float frameTime)
{
foreach (var mechanism in Mechanisms)
{
mechanism.PreMetabolism(frameTime);
}
}
/// <summary>
/// This method is called by <see cref="BodyManagerComponent.Update"/>
/// after <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public void PostMetabolism(float frameTime)
{
foreach (var mechanism in Mechanisms)
{
mechanism.PreMetabolism(frameTime);
}
}
/// <summary>
/// Attempts to add the given <see cref="BodyPartProperty"/>.
/// </summary>
/// <returns>
/// True if a <see cref="BodyPartProperty"/> of that type doesn't exist,
/// false otherwise.
/// </returns>
public bool TryAddProperty(BodyPartProperty property)
{
if (HasProperty(property.GetType()))
{
return false;
}
Properties.Add(property);
return true;
}
/// <summary>
/// Attempts to retrieve the given <see cref="BodyPartProperty"/> type.
/// The resulting <see cref="BodyPartProperty"/> will be null if unsuccessful.
/// </summary>
/// <param name="property">The property if found, null otherwise.</param>
/// <typeparam name="T">The type of the property to find.</typeparam>
/// <returns>True if successful, false otherwise.</returns>
public bool TryGetProperty<T>(out T property)
{
property = (T) Properties.First(x => x.GetType() == typeof(T));
return property != null;
}
/// <summary>
/// Attempts to retrieve the given <see cref="BodyPartProperty"/> type.
/// The resulting <see cref="BodyPartProperty"/> will be null if unsuccessful.
/// </summary>
/// <returns>True if successful, false otherwise.</returns>
public bool TryGetProperty(Type propertyType, out BodyPartProperty property)
{
property = (BodyPartProperty) Properties.First(x => x.GetType() == propertyType);
return property != null;
}
/// <summary>
/// Checks if the given type <see cref="T"/> is on this <see cref="BodyPart"/>.
/// </summary>
/// <typeparam name="T">
/// The subtype of <see cref="BodyPartProperty"/> to look for.
/// </typeparam>
/// <returns>
/// True if this <see cref="BodyPart"/> has a property of type
/// <see cref="T"/>, false otherwise.
/// </returns>
public bool HasProperty<T>() where T : BodyPartProperty
{
return Properties.Count(x => x.GetType() == typeof(T)) > 0;
}
/// <summary>
/// Checks if a subtype of <see cref="BodyPartProperty"/> is on this
/// <see cref="BodyPart"/>.
/// </summary>
/// <param name="propertyType">
/// The subtype of <see cref="BodyPartProperty"/> to look for.
/// </param>
/// <returns>
/// True if this <see cref="BodyPart"/> has a property of type
/// <see cref="propertyType"/>, false otherwise.
/// </returns>
public bool HasProperty(Type propertyType)
{
return Properties.Count(x => x.GetType() == propertyType) > 0;
}
/// <summary>
/// Checks if another <see cref="BodyPart"/> can be connected to this one.
/// </summary>
/// <param name="toBeConnected">The part to connect.</param>
/// <returns>True if it can be connected, false otherwise.</returns>
public bool CanAttachBodyPart(BodyPart toBeConnected)
{
return SurgeryData.CanAttachBodyPart(toBeConnected);
}
/// <summary>
/// Checks if a <see cref="Mechanism"/> can be installed on this
/// <see cref="BodyPart"/>.
/// </summary>
/// <returns>True if it can be installed, false otherwise.</returns>
public bool CanInstallMechanism(Mechanism mechanism)
{
return SizeUsed + mechanism.Size <= Size &&
SurgeryData.CanInstallMechanism(mechanism);
}
/// <summary>
/// Tries to install a mechanism onto this body part.
/// Call <see cref="TryInstallDroppedMechanism"/> instead if you want to
/// easily install an <see cref="IEntity"/> with a
/// <see cref="DroppedMechanismComponent"/>.
/// </summary>
/// <param name="mechanism">The mechanism to try to install.</param>
/// <returns>
/// True if successful, false if there was an error
/// (e.g. not enough room in <see cref="BodyPart"/>).
/// </returns>
private bool TryInstallMechanism(Mechanism mechanism)
{
if (!CanInstallMechanism(mechanism))
{
return false;
}
AddMechanism(mechanism);
return true;
}
/// <summary>
/// Tries to install a <see cref="DroppedMechanismComponent"/> into this
/// <see cref="BodyPart"/>, potentially deleting the dropped
/// <see cref="IEntity"/>.
/// </summary>
/// <param name="droppedMechanism">The mechanism to install.</param>
/// <returns>
/// True if successful, false if there was an error
/// (e.g. not enough room in <see cref="BodyPart"/>).
/// </returns>
public bool TryInstallDroppedMechanism(DroppedMechanismComponent droppedMechanism)
{
if (!TryInstallMechanism(droppedMechanism.ContainedMechanism))
{
return false; //Installing the mechanism failed for some reason.
}
droppedMechanism.Owner.Delete();
return true;
}
/// <summary>
/// Tries to remove the given <see cref="Mechanism"/> reference from
/// this <see cref="BodyPart"/>.
/// </summary>
/// <returns>
/// The newly spawned <see cref="DroppedMechanismComponent"/>, or null
/// if there was an error in spawning the entity or removing the mechanism.
/// </returns>
public bool TryDropMechanism(IEntity dropLocation, Mechanism mechanismTarget,
[NotNullWhen(true)] out DroppedMechanismComponent dropped)
{
dropped = null!;
if (!_mechanisms.Remove(mechanismTarget))
{
return false;
}
SizeUsed -= mechanismTarget.Size;
var entityManager = IoCManager.Resolve<IEntityManager>();
var position = dropLocation.Transform.GridPosition;
var mechanismEntity = entityManager.SpawnEntity("BaseDroppedMechanism", position);
dropped = mechanismEntity.GetComponent<DroppedMechanismComponent>();
dropped.InitializeDroppedMechanism(mechanismTarget);
return true;
}
/// <summary>
/// Tries to destroy the given <see cref="Mechanism"/> in this
/// <see cref="BodyPart"/>. Does NOT spawn a dropped entity.
/// </summary>
/// <summary>
/// Tries to destroy the given <see cref="Mechanism"/> in this
/// <see cref="BodyPart"/>.
/// </summary>
/// <param name="mechanismTarget">The mechanism to destroy.</param>
/// <returns>True if successful, false otherwise.</returns>
public bool DestroyMechanism(Mechanism mechanismTarget)
{
if (!RemoveMechanism(mechanismTarget))
{
return false;
}
return true;
}
/// <summary>
/// Checks if the given <see cref="SurgeryType"/> can be used on
/// the current state of this <see cref="BodyPart"/>.
/// </summary>
/// <returns>True if it can be used, false otherwise.</returns>
public bool SurgeryCheck(SurgeryType toolType)
{
return SurgeryData.CheckSurgery(toolType);
}
/// <summary>
/// Attempts to perform surgery on this <see cref="BodyPart"/> with the given
/// tool.
/// </summary>
/// <returns>True if successful, false if there was an error.</returns>
public bool AttemptSurgery(SurgeryType toolType, IBodyPartContainer target, ISurgeon surgeon, IEntity performer)
{
return SurgeryData.PerformSurgery(toolType, target, surgeon, performer);
}
private void AddMechanism(Mechanism mechanism)
{
DebugTools.AssertNotNull(mechanism);
_mechanisms.Add(mechanism);
SizeUsed += mechanism.Size;
mechanism.Part = this;
mechanism.EnsureInitialize();
if (Body == null)
{
return;
}
if (!Body.Template.MechanismLayers.TryGetValue(mechanism.Id, out var mapString))
{
return;
}
if (!IoCManager.Resolve<IReflectionManager>().TryParseEnumReference(mapString, out var @enum))
{
Logger.Warning($"Template {Body.Template.Name} has an invalid RSI map key {mapString} for mechanism {mechanism.Id}.");
return;
}
var message = new MechanismSpriteAddedMessage(@enum);
Body.Owner.SendNetworkMessage(Body, message);
}
/// <summary>
/// Tries to remove the given <see cref="mechanism"/> from this
/// <see cref="BodyPart"/>.
/// </summary>
/// <param name="mechanism">The mechanism to remove.</param>
/// <returns>True if it was removed, false otherwise.</returns>
private bool RemoveMechanism(Mechanism mechanism)
{
DebugTools.AssertNotNull(mechanism);
if (!_mechanisms.Remove(mechanism))
{
return false;
}
SizeUsed -= mechanism.Size;
mechanism.Part = null;
if (Body == null)
{
return true;
}
if (!Body.Template.MechanismLayers.TryGetValue(mechanism.Id, out var mapString))
{
return true;
}
if (!IoCManager.Resolve<IReflectionManager>().TryParseEnumReference(mapString, out var @enum))
{
Logger.Warning($"Template {Body.Template.Name} has an invalid RSI map key {mapString} for mechanism {mechanism.Id}.");
return true;
}
var message = new MechanismSpriteRemovedMessage(@enum);
Body.Owner.SendNetworkMessage(Body, message);
return true;
}
/// <summary>
/// Loads the given <see cref="BodyPartPrototype"/>.
/// Current data on this <see cref="BodyPart"/> will be overwritten!
/// </summary>
protected virtual void LoadFromPrototype(BodyPartPrototype data)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
Name = data.Name;
Plural = data.Plural;
PartType = data.PartType;
RSIPath = data.RSIPath;
RSIState = data.RSIState;
MaxDurability = data.Durability;
if (!prototypeManager.TryIndex(data.DamageContainerPresetId,
out DamageContainerPrototype damageContainerData))
{
throw new InvalidOperationException(
$"No {nameof(DamageContainerPrototype)} found with id {data.DamageContainerPresetId}");
}
Damage = new DamageContainer(OnHealthChanged, damageContainerData);
if (!prototypeManager.TryIndex(data.ResistanceSetId, out ResistanceSetPrototype resistancesData))
{
throw new InvalidOperationException(
$"No {nameof(ResistanceSetPrototype)} found with id {data.ResistanceSetId}");
}
Resistances = new ResistanceSet(resistancesData);
Size = data.Size;
Compatibility = data.Compatibility;
Properties.Clear();
Properties.UnionWith(data.Properties);
var surgeryDataType = Type.GetType(data.SurgeryDataName);
if (surgeryDataType == null)
{
throw new InvalidOperationException($"No {nameof(Surgery.SurgeryData)} found with name {data.SurgeryDataName}");
}
if (!surgeryDataType.IsSubclassOf(typeof(SurgeryData)))
{
throw new InvalidOperationException(
$"Class {data.SurgeryDataName} is not a subtype of {nameof(Surgery.SurgeryData)} with id {data.ID}");
}
SurgeryData = IoCManager.Resolve<IDynamicTypeFactory>().CreateInstance<SurgeryData>(surgeryDataType, new object[] {this});
foreach (var id in data.Mechanisms)
{
if (!prototypeManager.TryIndex(id, out MechanismPrototype mechanismData))
{
throw new InvalidOperationException($"No {nameof(MechanismPrototype)} found with id {id}");
}
var mechanism = new Mechanism(mechanismData);
AddMechanism(mechanism);
}
}
private void OnHealthChanged(List<HealthChangeData> changes)
{
// TODO
}
public bool SpawnDropped([NotNullWhen(true)] out IEntity dropped)
{
dropped = default!;
if (Body == null)
{
return false;
}
dropped = IoCManager.Resolve<IEntityManager>().SpawnEntity("BaseDroppedBodyPart", Body.Owner.Transform.GridPosition);
dropped.GetComponent<DroppedBodyPartComponent>().TransferBodyPartData(this);
return true;
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using Content.Shared.Body.Part;
using Content.Shared.Body.Preset;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body
{
/// <summary>
/// Stores data on what <see cref="BodyPartPrototype"></see> should
/// fill a BodyTemplate.
/// Used for loading complete body presets, like a "basic human" with all
/// human limbs.
/// </summary>
public class BodyPreset
{
public BodyPreset(BodyPresetPrototype data)
{
LoadFromPrototype(data);
}
[ViewVariables] public string Name { get; private set; }
/// <summary>
/// Maps a template slot to the ID of the <see cref="BodyPart"/> that should
/// fill it. E.g. "right arm" : "BodyPart.arm.basic_human".
/// </summary>
[ViewVariables]
public Dictionary<string, string> PartIDs { get; private set; }
protected virtual void LoadFromPrototype(BodyPresetPrototype data)
{
Name = data.Name;
PartIDs = data.PartIDs;
}
}
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Body;
using Content.Shared.Body.Template;
using Content.Shared.GameObjects.Components.Body;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body
{
/// <summary>
/// This class is a data capsule representing the standard format of a
/// <see cref="BodyManagerComponent"/>.
/// For instance, the "humanoid" BodyTemplate defines two arms, each connected to
/// a torso and so on.
/// Capable of loading data from a <see cref="BodyTemplatePrototype"/>.
/// </summary>
public class BodyTemplate
{
public BodyTemplate()
{
Name = "empty";
CenterSlot = "";
Slots = new Dictionary<string, BodyPartType>();
Connections = new Dictionary<string, List<string>>();
Layers = new Dictionary<string, string>();
MechanismLayers = new Dictionary<string, string>();
}
public BodyTemplate(BodyTemplatePrototype data)
{
LoadFromPrototype(data);
}
[ViewVariables] public string Name { get; private set; }
/// <summary>
/// The name of the center BodyPart. For humans, this is set to "torso".
/// Used in many calculations.
/// </summary>
[ViewVariables]
public string CenterSlot { get; set; }
/// <summary>
/// Maps all parts on this template to its BodyPartType.
/// For instance, "right arm" is mapped to "BodyPartType.arm" on the humanoid
/// template.
/// </summary>
[ViewVariables]
public Dictionary<string, BodyPartType> Slots { get; private set; }
/// <summary>
/// Maps limb name to the list of their connections to other limbs.
/// For instance, on the humanoid template "torso" is mapped to a list
/// containing "right arm", "left arm", "left leg", and "right leg".
/// This is mapped both ways during runtime, but in the prototype only one
/// way has to be defined, i.e., "torso" to "left arm" will automatically
/// map "left arm" to "torso".
/// </summary>
[ViewVariables]
public Dictionary<string, List<string>> Connections { get; private set; }
[ViewVariables]
public Dictionary<string, string> Layers { get; private set; }
[ViewVariables]
public Dictionary<string, string> MechanismLayers { get; private set; }
public bool Equals(BodyTemplate other)
{
return GetHashCode() == other.GetHashCode();
}
/// <summary>
/// Checks if the given slot exists in this <see cref="BodyTemplate"/>.
/// </summary>
/// <returns>True if it does, false otherwise.</returns>
public bool SlotExists(string slotName)
{
return Slots.Keys.Any(slot => slot == slotName);
}
/// <summary>
/// Calculates the hash code for this instance of <see cref="BodyTemplate"/>.
/// It does not matter in which order the Connections or Slots are defined.
/// </summary>
/// <returns>
/// An integer unique to this <see cref="BodyTemplate"/>'s layout.
/// </returns>
public override int GetHashCode()
{
var slotsHash = 0;
var connectionsHash = 0;
foreach (var (key, value) in Slots)
{
var slot = key.GetHashCode();
slot = HashCode.Combine(slot, value.GetHashCode());
slotsHash ^= slot;
}
var connections = new List<int>();
foreach (var (key, value) in Connections)
{
foreach (var targetBodyPart in value)
{
var connection = key.GetHashCode() ^ targetBodyPart.GetHashCode();
if (!connections.Contains(connection))
{
connections.Add(connection);
}
}
}
foreach (var connection in connections)
{
connectionsHash ^= connection;
}
// One of the unit tests considers 0 to be an error, but it will be 0 if
// the BodyTemplate is empty, so let's shift that up to 1.
var hash = HashCode.Combine(
CenterSlot.GetHashCode(),
slotsHash,
connectionsHash);
if (hash == 0)
{
hash++;
}
return hash;
}
protected virtual void LoadFromPrototype(BodyTemplatePrototype data)
{
Name = data.Name;
CenterSlot = data.CenterSlot;
Slots = data.Slots;
Connections = data.Connections;
Layers = data.Layers;
MechanismLayers = data.MechanismLayers;
}
}
}

View File

@@ -0,0 +1,19 @@
using Content.Server.Body.Surgery;
using Content.Server.GameObjects.Components.Body;
namespace Content.Server.Body
{
/// <summary>
/// Making a class inherit from this interface allows you to do many things with
/// it in the <see cref="SurgeryData"/> class.
/// This includes passing it as an argument to a
/// <see cref="SurgeryData.SurgeryAction"/> delegate, as to later typecast it back
/// to the original class type.
/// Every BodyPart also needs an <see cref="IBodyPartContainer"/> to be its parent
/// (i.e. the <see cref="BodyManagerComponent"/> holds many <see cref="BodyPart"/>,
/// each of which have an upward reference to it).
/// </summary>
public interface IBodyPartContainer
{
}
}

View File

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

View File

@@ -0,0 +1,38 @@
#nullable enable
using System;
using Content.Server.Body.Network;
using Content.Server.GameObjects.Components.Body.Circulatory;
using JetBrains.Annotations;
namespace Content.Server.Body.Mechanisms.Behaviors
{
[UsedImplicitly]
public class HeartBehavior : MechanismBehavior
{
private float _accumulatedFrameTime;
protected override Type? Network => typeof(CirculatoryNetwork);
public override void PreMetabolism(float frameTime)
{
// TODO do between pre and metabolism
base.PreMetabolism(frameTime);
if (Mechanism.Body == null ||
!Mechanism.Body.Owner.TryGetComponent(out BloodstreamComponent bloodstream))
{
return;
}
// Update at most once per second
_accumulatedFrameTime += frameTime;
// TODO: Move/accept/process bloodstream reagents only when the heart is pumping
if (_accumulatedFrameTime >= 1)
{
// bloodstream.Update(_accumulatedFrameTime);
_accumulatedFrameTime -= 1;
}
}
}
}

View File

@@ -0,0 +1,27 @@
#nullable enable
using System;
using Content.Server.Body.Network;
using Content.Server.GameObjects.Components.Body.Respiratory;
using JetBrains.Annotations;
namespace Content.Server.Body.Mechanisms.Behaviors
{
[UsedImplicitly]
public class LungBehavior : MechanismBehavior
{
protected override Type? Network => typeof(RespiratoryNetwork);
public override void PreMetabolism(float frameTime)
{
base.PreMetabolism(frameTime);
if (Mechanism.Body == null ||
!Mechanism.Body.Owner.TryGetComponent(out LungComponent lung))
{
return;
}
lung.Update(frameTime);
}
}
}

View File

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

View File

@@ -0,0 +1,36 @@
#nullable enable
using System;
using Content.Server.Body.Network;
using Content.Server.GameObjects.Components.Body.Digestive;
using JetBrains.Annotations;
namespace Content.Server.Body.Mechanisms.Behaviors
{
[UsedImplicitly]
public class StomachBehavior : MechanismBehavior
{
private float _accumulatedFrameTime;
protected override Type? Network => typeof(DigestiveNetwork);
public override void PreMetabolism(float frameTime)
{
base.PreMetabolism(frameTime);
if (Mechanism.Body == null ||
!Mechanism.Body.Owner.TryGetComponent(out StomachComponent stomach))
{
return;
}
// Update at most once per second
_accumulatedFrameTime += frameTime;
if (_accumulatedFrameTime >= 1)
{
stomach.Update(_accumulatedFrameTime);
_accumulatedFrameTime -= 1;
}
}
}
}

View File

@@ -0,0 +1,249 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Server.Body.Mechanisms.Behaviors;
using Content.Server.GameObjects.Components.Body;
using Content.Server.GameObjects.Components.Metabolism;
using Content.Shared.Body.Mechanism;
using Content.Shared.GameObjects.Components.Body;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body.Mechanisms
{
/// <summary>
/// Data class representing a persistent item inside a <see cref="BodyPart"/>.
/// This includes livers, eyes, cameras, brains, explosive implants,
/// binary communicators, and other things.
/// </summary>
public class Mechanism
{
private BodyPart? _part;
public Mechanism(MechanismPrototype data)
{
Data = data;
Id = null!;
Name = null!;
Description = null!;
ExamineMessage = null!;
RSIPath = null!;
RSIState = null!;
Behaviors = new List<MechanismBehavior>();
}
[ViewVariables] private bool Initialized { get; set; }
[ViewVariables] private MechanismPrototype Data { get; set; }
[ViewVariables] public string Id { get; private set; }
[ViewVariables] public string Name { get; set; }
/// <summary>
/// Professional description of the <see cref="Mechanism"/>.
/// </summary>
[ViewVariables]
public string Description { get; set; }
/// <summary>
/// The message to display upon examining a mob with this Mechanism installed.
/// If the string is empty (""), no message will be displayed.
/// </summary>
[ViewVariables]
public string ExamineMessage { get; set; }
/// <summary>
/// Path to the RSI that represents this <see cref="Mechanism"/>.
/// </summary>
[ViewVariables]
public string RSIPath { get; set; }
/// <summary>
/// RSI state that represents this <see cref="Mechanism"/>.
/// </summary>
[ViewVariables]
public string RSIState { get; set; }
/// <summary>
/// Max HP of this <see cref="Mechanism"/>.
/// </summary>
[ViewVariables]
public int MaxDurability { get; set; }
/// <summary>
/// Current HP of this <see cref="Mechanism"/>.
/// </summary>
[ViewVariables]
public int CurrentDurability { get; set; }
/// <summary>
/// At what HP this <see cref="Mechanism"/> is completely destroyed.
/// </summary>
[ViewVariables]
public int DestroyThreshold { get; set; }
/// <summary>
/// Armor of this <see cref="Mechanism"/> against attacks.
/// </summary>
[ViewVariables]
public int Resistance { get; set; }
/// <summary>
/// Determines a handful of things - mostly whether this
/// <see cref="Mechanism"/> can fit into a <see cref="BodyPart"/>.
/// </summary>
[ViewVariables]
public int Size { get; set; }
/// <summary>
/// What kind of <see cref="BodyPart"/> this <see cref="Mechanism"/> can be
/// easily installed into.
/// </summary>
[ViewVariables]
public BodyPartCompatibility Compatibility { get; set; }
/// <summary>
/// The behaviors that this <see cref="Mechanism"/> performs.
/// </summary>
[ViewVariables]
private List<MechanismBehavior> Behaviors { get; }
public BodyManagerComponent? Body => Part?.Body;
public BodyPart? Part
{
get => _part;
set
{
var old = _part;
_part = value;
if (value == null && old != null)
{
foreach (var behavior in Behaviors)
{
behavior.RemovedFromPart(old);
}
}
else
{
foreach (var behavior in Behaviors)
{
behavior.InstalledIntoPart();
}
}
}
}
public void EnsureInitialize()
{
if (Initialized)
{
return;
}
LoadFromPrototype(Data);
Initialized = true;
}
/// <summary>
/// Loads the given <see cref="MechanismPrototype"/>.
/// Current data on this <see cref="Mechanism"/> will be overwritten!
/// </summary>
private void LoadFromPrototype(MechanismPrototype data)
{
Data = data;
Id = data.ID;
Name = data.Name;
Description = data.Description;
ExamineMessage = data.ExamineMessage;
RSIPath = data.RSIPath;
RSIState = data.RSIState;
MaxDurability = data.Durability;
CurrentDurability = MaxDurability;
DestroyThreshold = data.DestroyThreshold;
Resistance = data.Resistance;
Size = data.Size;
Compatibility = data.Compatibility;
foreach (var behavior in Behaviors.ToArray())
{
RemoveBehavior(behavior);
}
foreach (var mechanismBehaviorName in data.BehaviorClasses)
{
var mechanismBehaviorType = Type.GetType(mechanismBehaviorName);
if (mechanismBehaviorType == null)
{
throw new InvalidOperationException(
$"No {nameof(MechanismBehavior)} found with name {mechanismBehaviorName}");
}
if (!mechanismBehaviorType.IsSubclassOf(typeof(MechanismBehavior)))
{
throw new InvalidOperationException(
$"Class {mechanismBehaviorName} is not a subtype of {nameof(MechanismBehavior)} for mechanism prototype {data.ID}");
}
var newBehavior = IoCManager.Resolve<IDynamicTypeFactory>().CreateInstance<MechanismBehavior>(mechanismBehaviorType);
AddBehavior(newBehavior);
}
}
public void InstalledIntoBody()
{
foreach (var behavior in Behaviors)
{
behavior.InstalledIntoBody();
}
}
public void RemovedFromBody(BodyManagerComponent old)
{
foreach (var behavior in Behaviors)
{
behavior.RemovedFromBody(old);
}
}
/// <summary>
/// This method is called by <see cref="BodyPart.PreMetabolism"/> before
/// <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public void PreMetabolism(float frameTime)
{
foreach (var behavior in Behaviors)
{
behavior.PreMetabolism(frameTime);
}
}
/// <summary>
/// This method is called by <see cref="BodyPart.PostMetabolism"/> after
/// <see cref="MetabolismComponent.Update"/> is called.
/// </summary>
public void PostMetabolism(float frameTime)
{
foreach (var behavior in Behaviors)
{
behavior.PostMetabolism(frameTime);
}
}
private void AddBehavior(MechanismBehavior behavior)
{
Behaviors.Add(behavior);
behavior.Initialize(this);
}
private bool RemoveBehavior(MechanismBehavior behavior)
{
behavior.Remove();
return Behaviors.Remove(behavior);
}
}
}

View File

@@ -0,0 +1,76 @@
using System;
using Content.Server.GameObjects.Components.Body;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.Body.Network
{
/// <summary>
/// Represents a "network" such as a bloodstream or electrical power that
/// is coordinated throughout an entire <see cref="BodyManagerComponent"/>.
/// </summary>
public abstract class BodyNetwork : IExposeData
{
[ViewVariables]
public abstract string Name { get; }
protected IEntity Owner { get; private set; }
public virtual void ExposeData(ObjectSerializer serializer) { }
public void OnAdd(IEntity entity)
{
Owner = entity;
OnAdd();
}
protected virtual void OnAdd() { }
public virtual void OnRemove() { }
/// <summary>
/// Called every update by <see cref="BodyManagerComponent.Update"/>.
/// </summary>
public virtual void Update(float frameTime) { }
}
public static class BodyNetworkExtensions
{
public static void TryAddNetwork(this IEntity entity, Type type)
{
if (!entity.TryGetComponent(out BodyManagerComponent body))
{
return;
}
body.EnsureNetwork(type);
}
public static void TryAddNetwork<T>(this IEntity entity) where T : BodyNetwork
{
if (!entity.TryGetComponent(out BodyManagerComponent body))
{
return;
}
body.EnsureNetwork<T>();
}
public static bool TryGetBodyNetwork(this IEntity entity, Type type, out BodyNetwork network)
{
network = null;
return entity.TryGetComponent(out BodyManagerComponent body) &&
body.TryGetNetwork(type, out network);
}
public static bool TryGetBodyNetwork<T>(this IEntity entity, out T network) where T : BodyNetwork
{
entity.TryGetBodyNetwork(typeof(T), out var unCastNetwork);
network = (T) unCastNetwork;
return network != null;
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC;
namespace Content.Server.Body.Network
{
public class BodyNetworkFactory : IBodyNetworkFactory
{
[Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
/// <summary>
/// Mapping of body network names to their types.
/// </summary>
private readonly Dictionary<string, Type> _names = new Dictionary<string, Type>();
private void Register(Type type)
{
if (_names.ContainsValue(type))
{
throw new InvalidOperationException($"Type is already registered: {type}");
}
if (!type.IsSubclassOf(typeof(BodyNetwork)))
{
throw new InvalidOperationException($"{type} is not a subclass of {nameof(BodyNetwork)}");
}
var dummy = _typeFactory.CreateInstance<BodyNetwork>(type);
if (dummy == null)
{
throw new NullReferenceException();
}
var name = dummy.Name;
if (name == null)
{
throw new NullReferenceException($"{type}'s name cannot be null.");
}
if (_names.ContainsKey(name))
{
throw new InvalidOperationException($"{name} is already registered.");
}
_names.Add(name, type);
}
public void DoAutoRegistrations()
{
var bodyNetwork = typeof(BodyNetwork);
foreach (var child in _reflectionManager.GetAllChildren(bodyNetwork))
{
Register(child);
}
}
public BodyNetwork GetNetwork(string name)
{
Type type;
try
{
type = _names[name];
}
catch (KeyNotFoundException)
{
throw new ArgumentException($"No {nameof(BodyNetwork)} exists with name {name}");
}
return _typeFactory.CreateInstance<BodyNetwork>(type);
}
public BodyNetwork GetNetwork(Type type)
{
if (!_names.ContainsValue(type))
{
throw new ArgumentException($"{type} is not registered.");
}
return _typeFactory.CreateInstance<BodyNetwork>(type);
}
}
}

View File

@@ -0,0 +1,25 @@
using Content.Server.GameObjects.Components.Body.Circulatory;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Body.Network
{
[UsedImplicitly]
public class CirculatoryNetwork : BodyNetwork
{
public override string Name => "Circulatory";
protected override void OnAdd()
{
Owner.EnsureComponent<BloodstreamComponent>();
}
public override void OnRemove()
{
if (Owner.HasComponent<BloodstreamComponent>())
{
Owner.RemoveComponent<BloodstreamComponent>();
}
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
using Content.Server.GameObjects.Components.Body.Respiratory;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Body.Network
{
[UsedImplicitly]
public class RespiratoryNetwork : BodyNetwork
{
public override string Name => "Respiratory";
protected override void OnAdd()
{
Owner.EnsureComponent<LungComponent>();
}
public override void OnRemove()
{
if (Owner.HasComponent<LungComponent>())
{
Owner.RemoveComponent<LungComponent>();
}
}
}
}

View File

@@ -0,0 +1,250 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.Body.Mechanisms;
using Content.Server.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.Interfaces;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
namespace Content.Server.Body.Surgery
{
/// <summary>
/// Data class representing the surgery state of a biological entity.
/// </summary>
[UsedImplicitly]
public class BiologicalSurgeryData : SurgeryData
{
private readonly List<Mechanism> _disconnectedOrgans = new List<Mechanism>();
private bool _skinOpened;
private bool _skinRetracted;
private bool _vesselsClamped;
public BiologicalSurgeryData(BodyPart parent) : base(parent) { }
protected override SurgeryAction? GetSurgeryStep(SurgeryType toolType)
{
if (toolType == SurgeryType.Amputation)
{
return RemoveBodyPartSurgery;
}
if (!_skinOpened)
{
// Case: skin is normal.
if (toolType == SurgeryType.Incision)
{
return OpenSkinSurgery;
}
}
else if (!_vesselsClamped)
{
// Case: skin is opened, but not clamped.
switch (toolType)
{
case SurgeryType.VesselCompression:
return ClampVesselsSurgery;
case SurgeryType.Cauterization:
return CauterizeIncisionSurgery;
}
}
else if (!_skinRetracted)
{
// Case: skin is opened and clamped, but not retracted.
switch (toolType)
{
case SurgeryType.Retraction:
return RetractSkinSurgery;
case SurgeryType.Cauterization:
return CauterizeIncisionSurgery;
}
}
else
{
// Case: skin is fully open.
if (Parent.Mechanisms.Count > 0 &&
toolType == SurgeryType.VesselCompression)
{
if (_disconnectedOrgans.Except(Parent.Mechanisms).Count() != 0 ||
Parent.Mechanisms.Except(_disconnectedOrgans).Count() != 0)
{
return LoosenOrganSurgery;
}
}
if (_disconnectedOrgans.Count > 0 && toolType == SurgeryType.Incision)
{
return RemoveOrganSurgery;
}
if (toolType == SurgeryType.Cauterization)
{
return CauterizeIncisionSurgery;
}
}
return null;
}
public override string GetDescription(IEntity target)
{
var toReturn = "";
if (_skinOpened && !_vesselsClamped)
{
// Case: skin is opened, but not clamped.
toReturn += Loc.GetString("The skin on {0:their} {1} has an incision, but it is prone to bleeding.\n",
target, Parent.Name);
}
else if (_skinOpened && _vesselsClamped && !_skinRetracted)
{
// Case: skin is opened and clamped, but not retracted.
toReturn += Loc.GetString("The skin on {0:their} {1} has an incision, but it is not retracted.\n",
target, Parent.Name);
}
else if (_skinOpened && _vesselsClamped && _skinRetracted)
{
// Case: skin is fully open.
toReturn += Loc.GetString("There is an incision on {0:their} {1}.\n", target, Parent.Name);
foreach (var mechanism in _disconnectedOrgans)
{
toReturn += Loc.GetString("{0:their} {1} is loose.\n", target, mechanism.Name);
}
}
return toReturn;
}
public override bool CanInstallMechanism(Mechanism mechanism)
{
return _skinOpened && _vesselsClamped && _skinRetracted;
}
public override bool CanAttachBodyPart(BodyPart part)
{
return true;
// TODO: if a bodypart is disconnected, you should have to do some surgery to allow another bodypart to be attached.
}
private void OpenSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
performer.PopupMessage(performer, Loc.GetString("Cut open the skin..."));
// TODO do_after: Delay
_skinOpened = true;
}
private void ClampVesselsSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
performer.PopupMessage(performer, Loc.GetString("Clamp the vessels..."));
// TODO do_after: Delay
_vesselsClamped = true;
}
private void RetractSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
performer.PopupMessage(performer, Loc.GetString("Retract the skin..."));
// TODO do_after: Delay
_skinRetracted = true;
}
private void CauterizeIncisionSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
performer.PopupMessage(performer, Loc.GetString("Cauterize the incision..."));
// TODO do_after: Delay
_skinOpened = false;
_vesselsClamped = false;
_skinRetracted = false;
}
private void LoosenOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
if (Parent.Mechanisms.Count <= 0)
{
return;
}
var toSend = new List<Mechanism>();
foreach (var mechanism in Parent.Mechanisms)
{
if (!_disconnectedOrgans.Contains(mechanism))
{
toSend.Add(mechanism);
}
}
if (toSend.Count > 0)
{
surgeon.RequestMechanism(toSend, LoosenOrganSurgeryCallback);
}
}
private void LoosenOrganSurgeryCallback(Mechanism target, IBodyPartContainer container, ISurgeon surgeon,
IEntity performer)
{
if (target == null || !Parent.Mechanisms.Contains(target))
{
return;
}
performer.PopupMessage(performer, Loc.GetString("Loosen the organ..."));
// TODO do_after: Delay
_disconnectedOrgans.Add(target);
}
private void RemoveOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
if (_disconnectedOrgans.Count <= 0)
{
return;
}
if (_disconnectedOrgans.Count == 1)
{
RemoveOrganSurgeryCallback(_disconnectedOrgans[0], container, surgeon, performer);
}
else
{
surgeon.RequestMechanism(_disconnectedOrgans, RemoveOrganSurgeryCallback);
}
}
private void RemoveOrganSurgeryCallback(Mechanism target, IBodyPartContainer container,
ISurgeon surgeon,
IEntity performer)
{
if (target == null || !Parent.Mechanisms.Contains(target))
{
return;
}
performer.PopupMessage(performer, Loc.GetString("Remove the organ..."));
// TODO do_after: Delay
Parent.TryDropMechanism(performer, target, out _);
_disconnectedOrgans.Remove(target);
}
private void RemoveBodyPartSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
{
// This surgery requires a DroppedBodyPartComponent.
if (!(container is BodyManagerComponent))
{
return;
}
var bmTarget = (BodyManagerComponent) container;
performer.PopupMessage(performer, Loc.GetString("Saw off the limb!"));
// TODO do_after: Delay
bmTarget.DisconnectBodyPart(Parent, true);
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
using Content.Server.Body.Mechanisms;
using Content.Server.GameObjects.Components.Body;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.Body.Surgery
{
/// <summary>
/// Interface representing an entity capable of performing surgery (performing operations on an
/// <see cref="SurgeryData"/> class).
/// For an example see <see cref="SurgeryToolComponent"/>, which inherits from this class.
/// </summary>
public interface ISurgeon
{
public delegate void MechanismRequestCallback(
Mechanism target,
IBodyPartContainer container,
ISurgeon surgeon,
IEntity performer);
/// <summary>
/// How long it takes to perform a single surgery step (in seconds).
/// </summary>
public float BaseOperationTime { get; set; }
/// <summary>
/// When performing a surgery, the <see cref="SurgeryData"/> may sometimes require selecting from a set of Mechanisms
/// to operate on.
/// This function is called in that scenario, and it is expected that you call the callback with one mechanism from the
/// provided list.
/// </summary>
public void RequestMechanism(IEnumerable<Mechanism> options, MechanismRequestCallback callback);
}
}

View File

@@ -0,0 +1,91 @@
#nullable enable
using Content.Server.Body.Mechanisms;
using Content.Shared.GameObjects.Components.Body;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.Body.Surgery
{
/// <summary>
/// This data class represents the state of a <see cref="BodyPart"/> in regards to everything surgery related -
/// whether there's an incision on it, whether the bone is broken, etc.
/// </summary>
public abstract class SurgeryData
{
protected delegate void SurgeryAction(IBodyPartContainer container, ISurgeon surgeon, IEntity performer);
/// <summary>
/// The <see cref="BodyPart"/> this surgeryData is attached to.
/// The <see cref="SurgeryData"/> class should not exist without a
/// <see cref="BodyPart"/> that it represents, and will throw errors if it
/// is null.
/// </summary>
protected readonly BodyPart Parent;
protected SurgeryData(BodyPart parent)
{
Parent = parent;
}
/// <summary>
/// The <see cref="BodyPartType"/> of the parent <see cref="BodyPart"/>.
/// </summary>
protected BodyPartType ParentType => Parent.PartType;
/// <summary>
/// Returns the description of this current <see cref="BodyPart"/> to be shown
/// upon observing the given entity.
/// </summary>
public abstract string GetDescription(IEntity target);
/// <summary>
/// Returns whether a <see cref="Mechanism"/> can be installed into the
/// <see cref="BodyPart"/> this <see cref="SurgeryData"/> represents.
/// </summary>
public abstract bool CanInstallMechanism(Mechanism mechanism);
/// <summary>
/// Returns whether the given <see cref="BodyPart"/> can be connected to the
/// <see cref="BodyPart"/> this <see cref="SurgeryData"/> represents.
/// </summary>
public abstract bool CanAttachBodyPart(BodyPart part);
/// <summary>
/// Gets the delegate corresponding to the surgery step using the given
/// <see cref="SurgeryType"/>.
/// </summary>
/// <returns>
/// The corresponding surgery action or null if no step can be performed.
/// </returns>
protected abstract SurgeryAction? GetSurgeryStep(SurgeryType toolType);
/// <summary>
/// Returns whether the given <see cref="SurgeryType"/> can be used to perform a surgery on the BodyPart this
/// <see cref="SurgeryData"/> represents.
/// </summary>
public bool CheckSurgery(SurgeryType toolType)
{
return GetSurgeryStep(toolType) != null;
}
/// <summary>
/// Attempts to perform surgery of the given <see cref="SurgeryType"/>. Returns whether the operation was successful.
/// </summary>
/// <param name="surgeryType">The <see cref="SurgeryType"/> used for this surgery.</param>
/// <param name="container">The container where the surgery is being done.</param>
/// <param name="surgeon">The entity being used to perform the surgery.</param>
/// <param name="performer">The entity performing the surgery.</param>
public bool PerformSurgery(SurgeryType surgeryType, IBodyPartContainer container, ISurgeon surgeon,
IEntity performer)
{
var step = GetSurgeryStep(surgeryType);
if (step == null)
{
return false;
}
step(container, surgeon, performer);
return true;
}
}
}