Refactor body component to use slots instead of an army of dictionaries (#3749)

* Refactor body component to use slots instead of an army of dictionaries

* Update vox

* Replace static method call with extension

* Add setpart method, replace dispose with shutdown

* Fix tests, fix not listening to slot events when setting a part
This commit is contained in:
DrSmugleaf
2021-04-05 14:54:51 +02:00
committed by GitHub
parent 5387f87608
commit 677706b117
30 changed files with 602 additions and 466 deletions

View File

@@ -12,7 +12,7 @@ namespace Content.Server.GameObjects.Components.Body.Behavior
{
public static bool HasMechanismBehavior<T>(this IBody body) where T : IMechanismBehavior
{
return body.Parts.Values.Any(p => p.HasMechanismBehavior<T>());
return body.Parts.Any(p => p.Key.HasMechanismBehavior<T>());
}
public static bool HasMechanismBehavior<T>(this IBodyPart part) where T : IMechanismBehavior
@@ -22,7 +22,7 @@ namespace Content.Server.GameObjects.Components.Body.Behavior
public static IEnumerable<IMechanismBehavior> GetMechanismBehaviors(this IBody body)
{
foreach (var part in body.Parts.Values)
foreach (var (part, _) in body.Parts)
foreach (var mechanism in part.Mechanisms)
foreach (var behavior in mechanism.Behaviors.Values)
{
@@ -46,7 +46,7 @@ namespace Content.Server.GameObjects.Components.Body.Behavior
public static IEnumerable<T> GetMechanismBehaviors<T>(this IBody body) where T : class, IMechanismBehavior
{
foreach (var part in body.Parts.Values)
foreach (var (part, _) in body.Parts)
foreach (var mechanism in part.Mechanisms)
foreach (var behavior in mechanism.Behaviors.Values)
{

View File

@@ -5,11 +5,11 @@ using Content.Server.GameObjects.Components.Observer;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Body.Slot;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.Utility;
using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Console;
using Robust.Shared.Containers;
@@ -29,20 +29,20 @@ namespace Content.Server.GameObjects.Components.Body
{
private Container _partContainer = default!;
protected override bool CanAddPart(string slot, IBodyPart part)
protected override bool CanAddPart(string slotId, IBodyPart part)
{
return base.CanAddPart(slot, part) &&
return base.CanAddPart(slotId, part) &&
_partContainer.CanInsert(part.Owner);
}
protected override void OnAddPart(string slot, IBodyPart part)
protected override void OnAddPart(BodyPartSlot slot, IBodyPart part)
{
base.OnAddPart(slot, part);
_partContainer.Insert(part.Owner);
}
protected override void OnRemovePart(string slot, IBodyPart part)
protected override void OnRemovePart(BodyPartSlot slot, IBodyPart part)
{
base.OnRemovePart(slot, part);
@@ -54,21 +54,25 @@ namespace Content.Server.GameObjects.Components.Body
{
base.Initialize();
_partContainer = ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-{nameof(BodyComponent)}");
_partContainer = Owner.EnsureContainer<Container>($"{Name}-{nameof(BodyComponent)}");
var preset = Preset;
foreach (var (slot, partId) in PartIds)
if (preset != null)
{
// 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))
foreach (var slot in Slots)
{
Logger.Error($"Entity {partId} does not have a {nameof(IBodyPart)} component.");
continue;
}
// Using MapPosition instead of Coordinates here prevents
// a crash within the character preview menu in the lobby
var entity = Owner.EntityManager.SpawnEntity(preset.PartIDs[slot.Id], Owner.Transform.MapPosition);
TryAddPart(slot, part, true);
if (!entity.TryGetComponent(out IBodyPart? part))
{
Logger.Error($"Entity {slot.Id} does not have a {nameof(IBodyPart)} component.");
continue;
}
SetPart(slot.Id, part);
}
}
}
@@ -79,7 +83,7 @@ namespace Content.Server.GameObjects.Components.Body
// 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)
foreach (var (part, _) in Parts)
{
part.Dirty();
}

View File

@@ -68,13 +68,13 @@ namespace Content.Server.GameObjects.Components.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)
foreach (var (part, slot) in body.Parts)
{
// For each limb in the target, add it to our cache if it is a valid option.
if (value.CanAddMechanism(this))
if (part.CanAddMechanism(this))
{
OptionsCache.Add(IdHash, value);
toSend.Add(key + ": " + value.Name, IdHash++);
OptionsCache.Add(IdHash, slot);
toSend.Add(part + ": " + part.Name, IdHash++);
}
}

View File

@@ -1,6 +1,5 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Body;
@@ -61,7 +60,7 @@ namespace Content.Server.GameObjects.Components.Body.Part
{
base.Initialize();
_mechanismContainer = ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-{nameof(BodyPartComponent)}");
_mechanismContainer = Owner.EnsureContainer<Container>($"{Name}-{nameof(BodyPartComponent)}");
// This is ran in Startup as entities spawned in Initialize
// are not synced to the client since they are assumed to be
@@ -123,25 +122,23 @@ namespace Content.Server.GameObjects.Components.Body.Part
// 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 = body.Slots.Keys.ToList().Except(body.Parts.Keys.ToList()).ToList();
foreach (var slot in unoccupiedSlots)
foreach (var slot in body.EmptySlots)
{
if (!body.TryGetSlotType(slot, out var typeResult) ||
typeResult != PartType ||
!body.TryGetPartConnections(slot, out var parts))
if (slot.PartType != PartType)
{
continue;
}
foreach (var connectedPart in parts)
foreach (var connection in slot.Connections)
{
if (!connectedPart.CanAttachPart(this))
if (connection.Part == null ||
!connection.Part.CanAttachPart(this))
{
continue;
}
_optionsCache.Add(_idHash, slot);
toSend.Add(slot, _idHash++);
toSend.Add(slot.Id, _idHash++);
}
}
@@ -269,7 +266,7 @@ namespace Content.Server.GameObjects.Components.Body.Part
return;
}
body.TryAddPart($"{nameof(AttachBodyPartVerb)}-{component.Owner.Uid}", component, true);
body.SetPart($"{nameof(AttachBodyPartVerb)}-{component.Owner.Uid}", component);
}
}
}

View File

@@ -69,13 +69,13 @@ namespace Content.Server.GameObjects.Components.Body.Surgery
// 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)
foreach (var (part, slot) in body.Parts)
{
// For each limb in the target, add it to our cache if it is a valid option.
if (value.SurgeryCheck(_surgeryType))
if (part.SurgeryCheck(_surgeryType))
{
_optionsCache.Add(_idHash, value);
toSend.Add(key + ": " + value.Name, _idHash++);
_optionsCache.Add(_idHash, part);
toSend.Add(slot.Id + ": " + part.Name, _idHash++);
}
}