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:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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++);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -469,19 +469,26 @@ namespace Content.Server.GameObjects.Components.Kitchen
|
||||
|
||||
if (victim.TryGetComponent<IBody>(out var body))
|
||||
{
|
||||
var heads = body.GetPartsOfType(BodyPartType.Head);
|
||||
foreach (var head in heads)
|
||||
var headSlots = body.GetSlotsOfType(BodyPartType.Head);
|
||||
|
||||
foreach (var slot in headSlots)
|
||||
{
|
||||
if (!body.TryDropPart(head, out var dropped))
|
||||
var part = slot.Part;
|
||||
|
||||
if (part == null ||
|
||||
!body.TryDropPart(slot, out var dropped))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var droppedHeads = dropped.Where(p => p.PartType == BodyPartType.Head);
|
||||
|
||||
foreach (var droppedHead in droppedHeads)
|
||||
foreach (var droppedPart in dropped.Values)
|
||||
{
|
||||
_storage.Insert(droppedHead.Owner);
|
||||
if (droppedPart.PartType != BodyPartType.Head)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_storage.Insert(droppedPart.Owner);
|
||||
headCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Content.Server.GameObjects.Components.Mobs
|
||||
|
||||
if (Owner.TryGetComponent(out IBody? body))
|
||||
{
|
||||
foreach (var part in body.Parts.Values)
|
||||
foreach (var (part, _) in body.Parts)
|
||||
{
|
||||
if (!part.Owner.TryGetComponent(out SpriteComponent? sprite))
|
||||
{
|
||||
@@ -37,7 +37,7 @@ namespace Content.Server.GameObjects.Components.Mobs
|
||||
|
||||
if (Appearance != null! && Owner.TryGetComponent(out IBody? body))
|
||||
{
|
||||
foreach (var part in body.Parts.Values)
|
||||
foreach (var (part, _) in body.Parts)
|
||||
{
|
||||
if (!part.Owner.TryGetComponent(out SpriteComponent? sprite))
|
||||
{
|
||||
|
||||
@@ -80,8 +80,8 @@ namespace Content.Server.GameObjects.Components.Movement
|
||||
return false;
|
||||
}
|
||||
|
||||
if (body.GetPartsOfType(BodyPartType.Leg).Count == 0 ||
|
||||
body.GetPartsOfType(BodyPartType.Foot).Count == 0)
|
||||
if (!body.HasPartOfType(BodyPartType.Leg) ||
|
||||
!body.HasPartOfType(BodyPartType.Foot))
|
||||
{
|
||||
reason = Loc.GetString("comp-climbable-cant-climb");
|
||||
return false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Interactable;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.Components.Strap;
|
||||
@@ -14,15 +15,14 @@ using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Watercloset
|
||||
{
|
||||
@@ -144,7 +144,7 @@ namespace Content.Server.GameObjects.Components.Watercloset
|
||||
{
|
||||
// check that victim even have head
|
||||
if (victim.TryGetComponent<IBody>(out var body) &&
|
||||
body.GetPartsOfType(BodyPartType.Head).Count != 0)
|
||||
body.HasPartOfType(BodyPartType.Head))
|
||||
{
|
||||
var othersMessage = Loc.GetString("{0:theName} sticks their head into {1:theName} and flushes it!", victim, Owner);
|
||||
victim.PopupMessageOtherClients(othersMessage);
|
||||
|
||||
Reference in New Issue
Block a user