Predict helmet toggles (#22089)

This commit is contained in:
metalgearsloth
2023-12-06 17:59:31 +11:00
committed by GitHub
parent 7cd5bf33aa
commit 8583f0d565
4 changed files with 40 additions and 64 deletions

View File

@@ -1,4 +1,5 @@
using Content.Shared.Clothing.EntitySystems; using Content.Shared.Clothing.EntitySystems;
using Robust.Shared.GameStates;
namespace Content.Shared.Clothing.Components; namespace Content.Shared.Clothing.Components;
@@ -9,12 +10,12 @@ namespace Content.Shared.Clothing.Components;
/// hardsuit helmets. /// hardsuit helmets.
/// </summary> /// </summary>
[Access(typeof(ToggleableClothingSystem))] [Access(typeof(ToggleableClothingSystem))]
[RegisterComponent] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class AttachedClothingComponent : Component public sealed partial class AttachedClothingComponent : Component
{ {
/// <summary> /// <summary>
/// The Id of the piece of clothing that this entity belongs to. /// The Id of the piece of clothing that this entity belongs to.
/// </summary> /// </summary>
[DataField("AttachedUid")] [DataField, AutoNetworkedField]
public EntityUid AttachedUid = default!; public EntityUid AttachedUid;
} }

View File

@@ -1,17 +1,17 @@
using Content.Shared.Clothing.EntitySystems; using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Clothing.Components; namespace Content.Shared.Clothing.Components;
/// <summary> /// <summary>
/// This component gives an item an action that will equip or un-equip some clothing. Intended for use with /// This component gives an item an action that will equip or un-equip some clothing e.g. hardsuits and hardsuit helmets.
/// hardsuits and hardsuit helmets.
/// </summary> /// </summary>
[Access(typeof(ToggleableClothingSystem))] [Access(typeof(ToggleableClothingSystem))]
[RegisterComponent] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ToggleableClothingComponent : Component public sealed partial class ToggleableClothingComponent : Component
{ {
public const string DefaultClothingContainerId = "toggleable-clothing"; public const string DefaultClothingContainerId = "toggleable-clothing";
@@ -19,35 +19,35 @@ public sealed partial class ToggleableClothingComponent : Component
/// <summary> /// <summary>
/// Action used to toggle the clothing on or off. /// Action used to toggle the clothing on or off.
/// </summary> /// </summary>
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] [DataField, AutoNetworkedField]
public string Action = "ActionToggleSuitPiece"; public EntProtoId Action = "ActionToggleSuitPiece";
[DataField("actionEntity")] [DataField, AutoNetworkedField]
public EntityUid? ActionEntity; public EntityUid? ActionEntity;
/// <summary> /// <summary>
/// Default clothing entity prototype to spawn into the clothing container. /// Default clothing entity prototype to spawn into the clothing container.
/// </summary> /// </summary>
[DataField("clothingPrototype", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))] [DataField(required: true), AutoNetworkedField]
public string ClothingPrototype = default!; public EntProtoId ClothingPrototype = default!;
/// <summary> /// <summary>
/// The inventory slot that the clothing is equipped to. /// The inventory slot that the clothing is equipped to.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("slot")] [DataField, AutoNetworkedField]
public string Slot = "head"; public string Slot = "head";
/// <summary> /// <summary>
/// The inventory slot flags required for this component to function. /// The inventory slot flags required for this component to function.
/// </summary> /// </summary>
[DataField("requiredSlot")] [DataField("requiredSlot"), AutoNetworkedField]
public SlotFlags RequiredFlags = SlotFlags.OUTERCLOTHING; public SlotFlags RequiredFlags = SlotFlags.OUTERCLOTHING;
/// <summary> /// <summary>
/// The container that the clothing is stored in when not equipped. /// The container that the clothing is stored in when not equipped.
/// </summary> /// </summary>
[DataField("containerId")] [DataField, AutoNetworkedField]
public string ContainerId = DefaultClothingContainerId; public string ContainerId = DefaultClothingContainerId;
[ViewVariables] [ViewVariables]
@@ -57,18 +57,18 @@ public sealed partial class ToggleableClothingComponent : Component
/// The Id of the piece of clothing that belongs to this component. Required for map-saving if the clothing is /// 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. /// currently not inside of the container.
/// </summary> /// </summary>
[DataField("clothingUid")] [DataField, AutoNetworkedField]
public EntityUid? ClothingUid; public EntityUid? ClothingUid;
/// <summary> /// <summary>
/// Time it takes for this clothing to be toggled via the stripping menu verbs. Null prevents the verb from even showing up. /// Time it takes for this clothing to be toggled via the stripping menu verbs. Null prevents the verb from even showing up.
/// </summary> /// </summary>
[DataField("stripDelay")] [DataField, AutoNetworkedField]
public TimeSpan? StripDelay = TimeSpan.FromSeconds(3); public TimeSpan? StripDelay = TimeSpan.FromSeconds(3);
/// <summary> /// <summary>
/// Text shown in the toggle-clothing verb. Defaults to using the name of the <see cref="ActionEntity"/> action. /// Text shown in the toggle-clothing verb. Defaults to using the name of the <see cref="ActionEntity"/> action.
/// </summary> /// </summary>
[DataField("verbText")] [DataField, AutoNetworkedField]
public string? VerbText; public string? VerbText;
} }

View File

@@ -9,13 +9,17 @@ using Content.Shared.Popups;
using Content.Shared.Strip; using Content.Shared.Strip;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Shared.Clothing.EntitySystems; namespace Content.Shared.Clothing.EntitySystems;
public sealed class ToggleableClothingSystem : EntitySystem 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 SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!; [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!; [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
@@ -24,8 +28,6 @@ public sealed class ToggleableClothingSystem : EntitySystem
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedStrippableSystem _strippable = default!; [Dependency] private readonly SharedStrippableSystem _strippable = default!;
private Queue<EntityUid> _toInsert = new();
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -128,18 +130,6 @@ public sealed class ToggleableClothingSystem : EntitySystem
ToggleClothing(args.User, uid, component); 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) private void OnInteractHand(EntityUid uid, AttachedClothingComponent component, InteractHandEvent args)
{ {
if (args.Handled) if (args.Handled)
@@ -182,7 +172,7 @@ public sealed class ToggleableClothingSystem : EntitySystem
_actionsSystem.RemoveAction(action.AttachedEntity.Value, component.ActionEntity); _actionsSystem.RemoveAction(action.AttachedEntity.Value, component.ActionEntity);
} }
if (component.ClothingUid != null) if (component.ClothingUid != null && !_netMan.IsClient)
QueueDel(component.ClothingUid.Value); QueueDel(component.ClothingUid.Value);
} }
@@ -214,6 +204,10 @@ public sealed class ToggleableClothingSystem : EntitySystem
/// </summary> /// </summary>
private void OnAttachedUnequip(EntityUid uid, AttachedClothingComponent component, GotUnequippedEvent args) private void OnAttachedUnequip(EntityUid uid, AttachedClothingComponent component, GotUnequippedEvent args)
{ {
// Let containers worry about it.
if (_timing.ApplyingState)
return;
if (component.LifeStage > ComponentLifeStage.Running) if (component.LifeStage > ComponentLifeStage.Running)
return; return;
@@ -225,7 +219,8 @@ public sealed class ToggleableClothingSystem : EntitySystem
// As unequipped gets called in the middle of container removal, we cannot call a container-insert without causing issues. // 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: // 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> /// <summary>
@@ -250,7 +245,7 @@ public sealed class ToggleableClothingSystem : EntitySystem
_inventorySystem.TryUnequip(user, parent, component.Slot); _inventorySystem.TryUnequip(user, parent, component.Slot);
else if (_inventorySystem.TryGetSlotEntity(parent, component.Slot, out var existing)) 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); user, user);
} }
else else
@@ -294,8 +289,11 @@ public sealed class ToggleableClothingSystem : EntitySystem
{ {
var xform = Transform(uid); var xform = Transform(uid);
component.ClothingUid = Spawn(component.ClothingPrototype, xform.Coordinates); 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); component.Container.Insert(component.ClothingUid.Value, EntityManager, ownerTransform: xform);
Dirty(uid, component);
} }
if (_actionContainer.EnsureAction(uid, ref component.ActionEntity, out var action, component.Action)) if (_actionContainer.EnsureAction(uid, ref component.ActionEntity, out var action, component.Action))

View File

@@ -198,21 +198,8 @@ public abstract partial class InventorySystem
return false; 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); _audio.PlayPredicted(clothing.EquipSound, target, actor);
} }
@@ -385,23 +372,13 @@ public abstract partial class InventorySystem
if (!slotContainer.Remove(removedItem.Value, force: force)) if (!slotContainer.Remove(removedItem.Value, force: force))
return false; 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); _audio.PlayPredicted(clothing.UnequipSound, target, actor);
} }