using Content.Shared.Clothing.EntitySystems;
+using Robust.Shared.GameStates;
namespace Content.Shared.Clothing.Components;
/// hardsuit helmets.
/// </summary>
[Access(typeof(ToggleableClothingSystem))]
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class AttachedClothingComponent : Component
{
/// <summary>
/// The Id of the piece of clothing that this entity belongs to.
/// </summary>
- [DataField("AttachedUid")]
- public EntityUid AttachedUid = default!;
+ [DataField, AutoNetworkedField]
+ public EntityUid AttachedUid;
}
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Inventory;
using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Clothing.Components;
/// <summary>
-/// This component gives an item an action that will equip or un-equip some clothing. Intended for use with
-/// hardsuits and hardsuit helmets.
+/// This component gives an item an action that will equip or un-equip some clothing e.g. hardsuits and hardsuit helmets.
/// </summary>
[Access(typeof(ToggleableClothingSystem))]
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ToggleableClothingComponent : Component
{
public const string DefaultClothingContainerId = "toggleable-clothing";
/// <summary>
/// Action used to toggle the clothing on or off.
/// </summary>
- [DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
- public string Action = "ActionToggleSuitPiece";
+ [DataField, AutoNetworkedField]
+ public EntProtoId Action = "ActionToggleSuitPiece";
- [DataField("actionEntity")]
+ [DataField, AutoNetworkedField]
public EntityUid? ActionEntity;
/// <summary>
/// Default clothing entity prototype to spawn into the clothing container.
/// </summary>
- [DataField("clothingPrototype", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
- public string ClothingPrototype = default!;
+ [DataField(required: true), AutoNetworkedField]
+ public EntProtoId ClothingPrototype = default!;
/// <summary>
/// The inventory slot that the clothing is equipped to.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
- [DataField("slot")]
+ [DataField, AutoNetworkedField]
public string Slot = "head";
/// <summary>
/// The inventory slot flags required for this component to function.
/// </summary>
- [DataField("requiredSlot")]
+ [DataField("requiredSlot"), AutoNetworkedField]
public SlotFlags RequiredFlags = SlotFlags.OUTERCLOTHING;
/// <summary>
/// The container that the clothing is stored in when not equipped.
/// </summary>
- [DataField("containerId")]
+ [DataField, AutoNetworkedField]
public string ContainerId = DefaultClothingContainerId;
[ViewVariables]
/// The Id of the piece of clothing that belongs to this component. Required for map-saving if the clothing is
/// currently not inside of the container.
/// </summary>
- [DataField("clothingUid")]
+ [DataField, AutoNetworkedField]
public EntityUid? ClothingUid;
/// <summary>
/// Time it takes for this clothing to be toggled via the stripping menu verbs. Null prevents the verb from even showing up.
/// </summary>
- [DataField("stripDelay")]
+ [DataField, AutoNetworkedField]
public TimeSpan? StripDelay = TimeSpan.FromSeconds(3);
/// <summary>
/// Text shown in the toggle-clothing verb. Defaults to using the name of the <see cref="ActionEntity"/> action.
/// </summary>
- [DataField("verbText")]
+ [DataField, AutoNetworkedField]
public string? VerbText;
}
using Content.Shared.Strip;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
+using Robust.Shared.Network;
using Robust.Shared.Serialization;
+using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Clothing.EntitySystems;
public sealed class ToggleableClothingSystem : EntitySystem
{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedStrippableSystem _strippable = default!;
- private Queue<EntityUid> _toInsert = new();
-
public override void Initialize()
{
base.Initialize();
ToggleClothing(args.User, uid, component);
}
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- // process delayed insertions. Avoids doing a container insert during a container removal.
- while (_toInsert.TryDequeue(out var uid))
- {
- if (TryComp(uid, out ToggleableClothingComponent? component) && component.ClothingUid != null)
- component.Container?.Insert(component.ClothingUid.Value);
- }
- }
-
private void OnInteractHand(EntityUid uid, AttachedClothingComponent component, InteractHandEvent args)
{
if (args.Handled)
_actionsSystem.RemoveAction(action.AttachedEntity.Value, component.ActionEntity);
}
- if (component.ClothingUid != null)
+ if (component.ClothingUid != null && !_netMan.IsClient)
QueueDel(component.ClothingUid.Value);
}
/// </summary>
private void OnAttachedUnequip(EntityUid uid, AttachedClothingComponent component, GotUnequippedEvent args)
{
+ // Let containers worry about it.
+ if (_timing.ApplyingState)
+ return;
+
if (component.LifeStage > ComponentLifeStage.Running)
return;
// As unequipped gets called in the middle of container removal, we cannot call a container-insert without causing issues.
// So we delay it and process it during a system update:
- _toInsert.Enqueue(component.AttachedUid);
+ if (toggleComp.ClothingUid != null)
+ toggleComp.Container?.Insert(toggleComp.ClothingUid.Value);
}
/// <summary>
_inventorySystem.TryUnequip(user, parent, component.Slot);
else if (_inventorySystem.TryGetSlotEntity(parent, component.Slot, out var existing))
{
- _popupSystem.PopupEntity(Loc.GetString("toggleable-clothing-remove-first", ("entity", existing)),
+ _popupSystem.PopupClient(Loc.GetString("toggleable-clothing-remove-first", ("entity", existing)),
user, user);
}
else
{
var xform = Transform(uid);
component.ClothingUid = Spawn(component.ClothingPrototype, xform.Coordinates);
- EnsureComp<AttachedClothingComponent>(component.ClothingUid.Value).AttachedUid = uid;
+ var attachedClothing = EnsureComp<AttachedClothingComponent>(component.ClothingUid.Value);
+ attachedClothing.AttachedUid = uid;
+ Dirty(component.ClothingUid.Value, attachedClothing);
component.Container.Insert(component.ClothingUid.Value, EntityManager, ownerTransform: xform);
+ Dirty(uid, component);
}
if (_actionContainer.EnsureAction(uid, ref component.ActionEntity, out var action, component.Action))
return false;
}
- if(!silent && clothing != null && clothing.EquipSound != null && _gameTiming.IsFirstTimePredicted)
+ if (!silent && clothing != null && clothing.EquipSound != null)
{
- Filter filter;
-
- if (_netMan.IsClient)
- filter = Filter.Local();
- else
- {
- filter = Filter.Pvs(target);
-
- // don't play double audio for predicted interactions
- if (predicted)
- filter.RemoveWhereAttachedEntity(entity => entity == actor);
- }
-
_audio.PlayPredicted(clothing.EquipSound, target, actor);
}
if (!slotContainer.Remove(removedItem.Value, force: force))
return false;
- _transform.DropNextTo(removedItem.Value, target);
+ // TODO: Inventory needs a hot cleanup hoo boy
+ // Check if something else (AKA toggleable) dumped it into a container.
+ if (!_containerSystem.IsEntityInContainer(removedItem.Value))
+ _transform.DropNextTo(removedItem.Value, target);
- if (!silent && Resolve(removedItem.Value, ref clothing, false) && clothing.UnequipSound != null && _gameTiming.IsFirstTimePredicted)
+ if (!silent && Resolve(removedItem.Value, ref clothing, false) && clothing.UnequipSound != null)
{
- Filter filter;
-
- if (_netMan.IsClient)
- filter = Filter.Local();
- else
- {
- filter = Filter.Pvs(target);
-
- // don't play double audio for predicted interactions
- if (predicted)
- filter.RemoveWhereAttachedEntity(entity => entity == actor);
- }
-
_audio.PlayPredicted(clothing.UnequipSound, target, actor);
}