diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index dbe6531c1c..fef0f5805b 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -118,7 +118,7 @@ namespace Content.Client.Actions SubscribeLocalEvent(HandleState); } - protected override void Dirty(ActionType action) + public override void Dirty(ActionType action) { // Should only ever receive component states for attached player's component. // --> lets not bother unnecessarily dirtying and prediction-resetting actions for other players. diff --git a/Content.Client/Actions/UI/ActionMenuItem.cs b/Content.Client/Actions/UI/ActionMenuItem.cs index 137e9aa2e4..2e5e1311d4 100644 --- a/Content.Client/Actions/UI/ActionMenuItem.cs +++ b/Content.Client/Actions/UI/ActionMenuItem.cs @@ -129,7 +129,7 @@ namespace Content.Client.Actions.UI _smallActionIcon.Texture = null; _smallActionIcon.Visible = false; } - else if (Action.Provider != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem) + else if (Action.EntityIcon != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem) { _smallActionIcon.Texture = texture; _smallActionIcon.Modulate = Action.IconColor; @@ -149,7 +149,7 @@ namespace Content.Client.Actions.UI private void UpdateItemIcon() { - if (Action?.Provider == null || !IoCManager.Resolve().TryGetComponent(Action.Provider.Value, out SpriteComponent sprite)) + if (Action?.EntityIcon == null || !IoCManager.Resolve().TryGetComponent(Action.EntityIcon.Value, out SpriteComponent sprite)) { _bigItemSpriteView.Visible = false; _bigItemSpriteView.Sprite = null; diff --git a/Content.Client/Actions/UI/ActionSlot.cs b/Content.Client/Actions/UI/ActionSlot.cs index 52cb521291..73d96fe4ed 100644 --- a/Content.Client/Actions/UI/ActionSlot.cs +++ b/Content.Client/Actions/UI/ActionSlot.cs @@ -402,7 +402,7 @@ namespace Content.Client.Actions.UI _smallActionIcon.Texture = null; _smallActionIcon.Visible = false; } - else if (Action.Provider != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem) + else if (Action.EntityIcon != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem) { _smallActionIcon.Texture = texture; _smallActionIcon.Modulate = Action.IconColor; @@ -422,7 +422,7 @@ namespace Content.Client.Actions.UI private void UpdateItemIcon() { - if (Action?.Provider == null || !IoCManager.Resolve().TryGetComponent(Action.Provider.Value, out SpriteComponent sprite)) + if (Action?.EntityIcon == null || !IoCManager.Resolve().TryGetComponent(Action.EntityIcon.Value, out SpriteComponent sprite)) { _bigItemSpriteView.Visible = false; _bigItemSpriteView.Sprite = null; diff --git a/Content.Client/Clothing/ClothingComponent.cs b/Content.Client/Clothing/ClothingComponent.cs index 1ead482b3c..cb7edaac69 100644 --- a/Content.Client/Clothing/ClothingComponent.cs +++ b/Content.Client/Clothing/ClothingComponent.cs @@ -14,9 +14,6 @@ namespace Content.Client.Clothing [DataField("femaleMask")] public FemaleClothingMask FemaleMask { get; } = FemaleClothingMask.UniformFull; - [DataField("quickEquip")] - public bool QuickEquip = true; - public string? InSlot; } diff --git a/Content.Server/Administration/Commands/SetOutfitCommand.cs b/Content.Server/Administration/Commands/SetOutfitCommand.cs index ae5fe1471c..17138abb1d 100644 --- a/Content.Server/Administration/Commands/SetOutfitCommand.cs +++ b/Content.Server/Administration/Commands/SetOutfitCommand.cs @@ -92,7 +92,7 @@ namespace Content.Server.Administration.Commands { foreach (var slot in slotDefinitions) { - invSystem.TryUnequip(target, slot.Name, true, true, inventoryComponent); + invSystem.TryUnequip(target, slot.Name, true, true, false, inventoryComponent); var gearStr = startingGear.GetGear(slot.Name, profile); if (gearStr == string.Empty) { diff --git a/Content.Server/Clothing/Components/ClothingComponent.cs b/Content.Server/Clothing/Components/ClothingComponent.cs index 629e4597eb..93db5d6e58 100644 --- a/Content.Server/Clothing/Components/ClothingComponent.cs +++ b/Content.Server/Clothing/Components/ClothingComponent.cs @@ -16,9 +16,6 @@ namespace Content.Server.Clothing.Components [DataField("HeatResistance")] private int _heatResistance = 323; - [DataField("quickEquip")] - public bool QuickEquip = true; - [ViewVariables(VVAccess.ReadWrite)] public int HeatResistance => _heatResistance; } diff --git a/Content.Server/Disease/Zombie/DiseaseZombieSystem.cs b/Content.Server/Disease/Zombie/DiseaseZombieSystem.cs index fb9758c5e5..08e36317f3 100644 --- a/Content.Server/Disease/Zombie/DiseaseZombieSystem.cs +++ b/Content.Server/Disease/Zombie/DiseaseZombieSystem.cs @@ -98,7 +98,7 @@ namespace Content.Server.Disease.Zombie } if (TryComp(uid, out var servInvComp)) - _serverInventory.TryUnequip(uid, "gloves", true, true, servInvComp); + _serverInventory.TryUnequip(uid, "gloves", true, true, predicted: false, servInvComp); _popupSystem.PopupEntity(Loc.GetString("zombie-transform", ("target", uid)), uid, Filter.Pvs(uid)); diff --git a/Content.Shared/Actions/ActionTypes/ActionType.cs b/Content.Shared/Actions/ActionTypes/ActionType.cs index 5233810b7a..f7ac83a3bf 100644 --- a/Content.Shared/Actions/ActionTypes/ActionType.cs +++ b/Content.Shared/Actions/ActionTypes/ActionType.cs @@ -89,6 +89,17 @@ public abstract class ActionType : IEquatable, IComparable, ICloneab /// public EntityUid? Provider; + /// + /// Entity to use for the action icon. Defaults to using . + /// + public EntityUid? EntityIcon + { + get => _entityIcon ?? Provider; + set => _entityIcon = value; + } + + private EntityUid? _entityIcon; + /// /// Whether the action system should block this action if the user cannot currently interact. Some spells or /// abilities may want to disable this and implement their own checks. @@ -255,6 +266,7 @@ public abstract class ActionType : IEquatable, IComparable, ICloneab Popup = toClone.Popup; PopupToggleSuffix = toClone.PopupToggleSuffix; ItemIconStyle = toClone.ItemIconStyle; + _entityIcon = toClone._entityIcon; } public bool Equals(ActionType? other) diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 57a4a98150..b88eb23f43 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -43,7 +43,7 @@ public abstract class SharedActionsSystem : EntitySystem } #region ComponentStateManagement - protected virtual void Dirty(ActionType action) + public virtual void Dirty(ActionType action) { if (action.AttachedEntity == null) return; diff --git a/Content.Shared/Clothing/Components/AttachedClothingComponent.cs b/Content.Shared/Clothing/Components/AttachedClothingComponent.cs new file mode 100644 index 0000000000..5843c2a29e --- /dev/null +++ b/Content.Shared/Clothing/Components/AttachedClothingComponent.cs @@ -0,0 +1,20 @@ +using Content.Shared.Clothing.EntitySystems; + +namespace Content.Shared.Clothing.Components; + +/// +/// This component indicates that this clothing is attached to some other entity with a . When unequipped, this entity should be returned to the entity that it is +/// attached to, rather than being dumped on the floor or something like that. Intended for use with hardsuits and +/// hardsuit helmets. +/// +[Friend(typeof(ToggleableClothingSystem))] +[RegisterComponent] +public sealed class AttachedClothingComponent : Component +{ + /// + /// The Id of the piece of clothing that this entity belongs to. + /// + [DataField("AttachedUid")] + public EntityUid AttachedUid = default!; +} diff --git a/Content.Shared/Clothing/Components/ToggleableClothingComponent.cs b/Content.Shared/Clothing/Components/ToggleableClothingComponent.cs new file mode 100644 index 0000000000..0a8f0597ec --- /dev/null +++ b/Content.Shared/Clothing/Components/ToggleableClothingComponent.cs @@ -0,0 +1,59 @@ +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Clothing.EntitySystems; +using Content.Shared.Inventory; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Clothing.Components; + +/// +/// This component gives an item an action that will equip or un-equip some clothing. Intended for use with +/// hardsuits and hardsuit helmets. +/// +[Friend(typeof(ToggleableClothingSystem))] +[RegisterComponent] +public sealed class ToggleableClothingComponent : Component +{ + public const string DefaultClothingContainerId = "toggleable-clothing"; + + /// + /// Action used to toggle the clothing on or off. + /// + [DataField("actionId", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string ActionId = "ToggleSuitHelmet"; + public InstantAction? ToggleAction = null; + + /// + /// Default clothing entity prototype to spawn into the clothing container. + /// + [DataField("clothingPrototype", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] + public readonly string ClothingPrototype = default!; + + /// + /// The inventory slot that the clothing is equipped to. + /// + [DataField("slot")] + public string Slot = "head"; + + /// + /// The inventory slot flags required for this component to function. + /// + [DataField("requiredSlot")] + public SlotFlags RequiredFlags = SlotFlags.OUTERCLOTHING; + + /// + /// The container that the clothing is stored in when not equipped. + /// + [DataField("containerId")] + public string ContainerId = DefaultClothingContainerId; + + public ContainerSlot? Container; + + /// + /// 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. + /// + [DataField("clothingUid")] + public EntityUid? ClothingUid; +} diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs new file mode 100644 index 0000000000..0f1f6c9659 --- /dev/null +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -0,0 +1,208 @@ +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Clothing.Components; +using Content.Shared.Interaction; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Shared.Popups; +using Robust.Shared.Containers; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared.Clothing.EntitySystems; + +public sealed class ToggleableClothingSystem : EntitySystem +{ + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + + private Queue _toInsert = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAdd); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnToggleClothing); + SubscribeLocalEvent(OnGetActions); + SubscribeLocalEvent(OnRemoveToggleable); + SubscribeLocalEvent(OnToggleableUnequip); + + SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent(OnAttachedUnequip); + SubscribeLocalEvent(OnRemoveAttached); + } + + 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) + return; + + if (!TryComp(component.AttachedUid, out ToggleableClothingComponent? toggleCom) + || toggleCom.Container == null) + return; + + if (!_inventorySystem.TryUnequip(Transform(uid).ParentUid, toggleCom.Slot, force: true)) + return; + + toggleCom.Container.Insert(uid, EntityManager); + args.Handled = true; + } + + /// + /// Called when the suit is unequipped, to ensure that the helmet also gets unequipped. + /// + private void OnToggleableUnequip(EntityUid uid, ToggleableClothingComponent component, GotUnequippedEvent args) + { + if (component.Container != null && component.Container.ContainedEntity != null && component.ClothingUid != null) + _inventorySystem.TryUnequip(args.Equipee, component.Slot, force: true); + } + + private void OnRemoveToggleable(EntityUid uid, ToggleableClothingComponent component, ComponentRemove args) + { + // If the parent/owner component of the attached clothing is being removed (entity getting deleted?) we will + // delete the attached entity. We do this regardless of whether or not the attached entity is currently + // "outside" of the container or not. This means that if a hardsuit takes too much damage, the helmet will also + // automatically be deleted. + + // remove action. + if (component.ToggleAction?.AttachedEntity != null) + _actionsSystem.RemoveAction(component.ToggleAction.AttachedEntity.Value, component.ToggleAction); + + if (component.ClothingUid != null) + QueueDel(component.ClothingUid.Value); + } + + private void OnRemoveAttached(EntityUid uid, AttachedClothingComponent component, ComponentRemove args) + { + // if the attached component is being removed (maybe entity is being deleted?) we will just remove the + // toggleable clothing component. This means if you had a hard-suit helmet that took too much damage, you would + // still be left with a suit that was simply missing a helmet. There is currently no way to fix a partially + // broken suit like this. + + if (!TryComp(component.AttachedUid, out ToggleableClothingComponent? toggleComp)) + return; + + if (toggleComp.LifeStage > ComponentLifeStage.Running) + return; + + // remove action. + if (toggleComp.ToggleAction?.AttachedEntity != null) + _actionsSystem.RemoveAction(toggleComp.ToggleAction.AttachedEntity.Value, toggleComp.ToggleAction); + + RemComp(component.AttachedUid, toggleComp); + } + + /// + /// Called if the helmet was unequipped, to ensure that it gets moved into the suit's container. + /// + private void OnAttachedUnequip(EntityUid uid, AttachedClothingComponent component, GotUnequippedEvent args) + { + if (component.LifeStage > ComponentLifeStage.Running) + return; + + if (!TryComp(component.AttachedUid, out ToggleableClothingComponent? toggleComp)) + return; + + if (toggleComp.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); + } + + /// + /// Equip or unequip the toggleable clothing. + /// + private void OnToggleClothing(EntityUid uid, ToggleableClothingComponent component, ToggleClothingEvent args) + { + if (args.Handled || component.Container == null || component.ClothingUid == null) + return; + + var parent = Transform(uid).ParentUid; + if (component.Container.ContainedEntity == null) + _inventorySystem.TryUnequip(parent, component.Slot); + else if (_inventorySystem.TryGetSlotEntity(parent, component.Slot, out var existing)) + { + _popupSystem.PopupEntity(Loc.GetString("toggleable-clothing-remove-first", ("entity", existing)), + args.Performer, Filter.Entities(args.Performer)); + } + else + _inventorySystem.TryEquip(parent, component.ClothingUid.Value, component.Slot); + + args.Handled = true; + } + + private void OnGetActions(EntityUid uid, ToggleableClothingComponent component, GetItemActionsEvent args) + { + if (component.ClothingUid == null || (args.SlotFlags & component.RequiredFlags) != component.RequiredFlags) + return; + + if (component.ToggleAction != null) + args.Actions.Add(component.ToggleAction); + } + + private void OnAdd(EntityUid uid, ToggleableClothingComponent component, ComponentAdd args) + { + component.Container = _containerSystem.EnsureContainer(uid, component.ContainerId); + } + + /// + /// On map init, either spawn the appropriate entity into the suit slot, or if it already exists, perform some + /// sanity checks. Also updates the action icon to show the toggled-entity. + /// + private void OnMapInit(EntityUid uid, ToggleableClothingComponent component, MapInitEvent args) + { + if (component.Container!.ContainedEntity is EntityUid ent) + { + DebugTools.Assert(component.ClothingUid == ent, "Unexpected entity present inside of a toggleable clothing container."); + return; + } + + if (component.ToggleAction == null + && _proto.TryIndex(component.ActionId, out InstantActionPrototype? act)) + { + component.ToggleAction = new(act); + } + + if (component.ClothingUid != null && component.ToggleAction != null) + { + DebugTools.Assert(Exists(component.ClothingUid), "Toggleable clothing is missing expected entity."); + DebugTools.Assert(TryComp(component.ClothingUid, out AttachedClothingComponent? comp), "Toggleable clothing is missing an attached component"); + DebugTools.Assert(comp?.AttachedUid == uid, "Toggleable clothing uid mismatch"); + } + else + { + var xform = Transform(uid); + component.ClothingUid = Spawn(component.ClothingPrototype, xform.Coordinates); + EnsureComp(component.ClothingUid.Value).AttachedUid = uid; + component.Container.Insert(component.ClothingUid.Value, EntityManager, ownerTransform: xform); + } + + if (component.ToggleAction != null) + { + component.ToggleAction.EntityIcon = component.ClothingUid; + _actionsSystem.Dirty(component.ToggleAction); + } + } +} + +public sealed class ToggleClothingEvent : InstantActionEvent { } diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs index 6ed191320a..537a50585e 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs @@ -90,7 +90,7 @@ public abstract partial class SharedHandsSystem : EntitySystem if (handContainer == null || handContainer.ContainedEntity != null) return false; - if (!Resolve(entity, ref item, false)) + if (!Resolve(entity, ref item, false) || !item.CanPickup) return false; if (TryComp(entity, out PhysicsComponent? physics) && physics.BodyType == BodyType.Static) diff --git a/Content.Shared/Inventory/InventorySystem.Equip.cs b/Content.Shared/Inventory/InventorySystem.Equip.cs index 85216b2035..93d4661669 100644 --- a/Content.Shared/Inventory/InventorySystem.Equip.cs +++ b/Content.Shared/Inventory/InventorySystem.Equip.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Content.Shared.Clothing.Components; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; @@ -11,6 +12,7 @@ using Content.Shared.Strip.Components; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.Map; +using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Timing; @@ -24,6 +26,7 @@ public abstract partial class InventorySystem [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly INetManager _netMan = default!; private void InitializeEquip() { @@ -48,6 +51,10 @@ public abstract partial class InventorySystem if (TryGetSlotEntity(args.User, slotDef.Name, out var slotEntity, inv)) { + // Item in slot has to be quick equipable as well + if (TryComp(slotEntity, out SharedItemComponent? item) && !item.QuickEquip) + continue; + if (!TryUnequip(args.User, slotDef.Name, true, inventory: inv)) continue; @@ -175,11 +182,18 @@ public abstract partial class InventorySystem if(!silent && item.EquipSound != null && _gameTiming.IsFirstTimePredicted) { - var filter = Filter.Pvs(target); + Filter filter; - // don't play double audio for predicted interactions - if (predicted) - filter.RemoveWhereAttachedEntity(entity => entity == actor); + 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); + } SoundSystem.Play(filter, item.EquipSound.GetSound(), target, AudioParams.Default.WithVolume(-2f)); } @@ -193,6 +207,11 @@ public abstract partial class InventorySystem public bool CanAccess(EntityUid actor, EntityUid target, EntityUid itemUid) { + // if the item is something like a hardsuit helmet, it may be contained within the hardsuit. + // in that case, we check accesibility for the owner-entity instead. + if (TryComp(itemUid, out AttachedClothingComponent? attachedComp)) + itemUid = attachedComp.AttachedUid; + // Can the actor reach the target? if (actor != target && !(_interactionSystem.InRangeUnobstructed(actor, target) && _containerSystem.IsInSameOrParentContainer(actor, target))) return false; @@ -273,18 +292,18 @@ public abstract partial class InventorySystem return true; } - public bool TryUnequip(EntityUid uid, string slot, bool silent = false, bool force = false, - InventoryComponent? inventory = null) => TryUnequip(uid, uid, slot, silent, force, inventory); + public bool TryUnequip(EntityUid uid, string slot, bool silent = false, bool force = false, bool predicted = false, + InventoryComponent? inventory = null, SharedItemComponent? item = null) => TryUnequip(uid, uid, slot, silent, force, predicted, inventory, item); public bool TryUnequip(EntityUid actor, EntityUid target, string slot, bool silent = false, - bool force = false, InventoryComponent? inventory = null) => - TryUnequip(actor, target, slot, out _, silent, force, inventory); + bool force = false, bool predicted = false, InventoryComponent? inventory = null, SharedItemComponent? item = null) => + TryUnequip(actor, target, slot, out _, silent, force, predicted, inventory, item); - public bool TryUnequip(EntityUid uid, string slot, [NotNullWhen(true)] out EntityUid? removedItem, bool silent = false, bool force = false, - InventoryComponent? inventory = null) => TryUnequip(uid, uid, slot, out removedItem, silent, force, inventory); + public bool TryUnequip(EntityUid uid, string slot, [NotNullWhen(true)] out EntityUid? removedItem, bool silent = false, bool force = false, bool predicted = false, + InventoryComponent? inventory = null, SharedItemComponent? item = null) => TryUnequip(uid, uid, slot, out removedItem, silent, force, predicted, inventory, item); public bool TryUnequip(EntityUid actor, EntityUid target, string slot, [NotNullWhen(true)] out EntityUid? removedItem, bool silent = false, - bool force = false, InventoryComponent? inventory = null) + bool force = false, bool predicted = false, InventoryComponent? inventory = null, SharedItemComponent? item = null) { removedItem = null; if (!Resolve(target, ref inventory, false)) @@ -321,7 +340,7 @@ public abstract partial class InventorySystem if (slotDef != slotDefinition && slotDef.DependsOn == slotDefinition.Name) { //this recursive call might be risky - TryUnequip(actor, target, slotDef.Name, true, true, inventory); + TryUnequip(actor, target, slotDef.Name, true, true, predicted, inventory); } } @@ -340,6 +359,24 @@ public abstract partial class InventorySystem Transform(removedItem.Value).Coordinates = Transform(target).Coordinates; + if (!silent && Resolve(removedItem.Value, ref item) && item.UnequipSound != null && _gameTiming.IsFirstTimePredicted) + { + 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); + } + + SoundSystem.Play(filter, item.UnequipSound.GetSound(), target, AudioParams.Default.WithVolume(-2f)); + } + inventory.Dirty(); _movementSpeed.RefreshMovementSpeedModifiers(target); diff --git a/Content.Shared/Item/SharedItemComponent.cs b/Content.Shared/Item/SharedItemComponent.cs index 84a5832af3..0a4f4296d2 100644 --- a/Content.Shared/Item/SharedItemComponent.cs +++ b/Content.Shared/Item/SharedItemComponent.cs @@ -37,6 +37,20 @@ namespace Content.Shared.Item [DataField("clothingVisuals")] public Dictionary> ClothingVisuals = new(); + /// + /// Whether or not this item can be picked up. + /// + /// + /// This should almost always be true for items. But in some special cases, an item can be equipped but not + /// picked up. E.g., hardsuit helmets are attached to the suit, so we want to disable things like the pickup + /// verb. + /// + [DataField("canPickup")] + public bool CanPickup = true; + + [DataField("quickEquip")] + public bool QuickEquip = true; + /// /// Part of the state of the sprite shown on the player when this item is in their hands or inventory. /// @@ -61,9 +75,12 @@ namespace Content.Shared.Item [DataField("Slots")] public SlotFlags SlotFlags = SlotFlags.PREVENTEQUIP; //Different from None, NONE allows equips if no slot flags are required - [DataField("EquipSound")] + [DataField("equipSound")] public SoundSpecifier? EquipSound { get; set; } = default!; + [DataField("unequipSound")] + public SoundSpecifier? UnequipSound = default!; + /// /// Rsi of the sprite shown on the player when this item is in their hands. Used to generate a default entry for /// diff --git a/Content.Shared/Item/SharedItemSystem.cs b/Content.Shared/Item/SharedItemSystem.cs index 4e07a1b272..ff70b9fe76 100644 --- a/Content.Shared/Item/SharedItemSystem.cs +++ b/Content.Shared/Item/SharedItemSystem.cs @@ -1,4 +1,3 @@ -using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Inventory.Events; @@ -27,7 +26,7 @@ namespace Content.Shared.Item private void OnHandInteract(EntityUid uid, SharedItemComponent component, InteractHandEvent args) { - if (args.Handled) + if (args.Handled || !component.CanPickup) return; args.Handled = _handsSystem.TryPickup(args.User, uid, animateUser: false); @@ -68,6 +67,7 @@ namespace Content.Shared.Item args.Using != null || !args.CanAccess || !args.CanInteract || + !component.CanPickup || !_handsSystem.CanPickupAnyHand(args.User, args.Target, handsComp: args.Hands, item: component)) return; diff --git a/Resources/Audio/Mecha/license.txt b/Resources/Audio/Mecha/license.txt new file mode 100644 index 0000000000..85e55229b4 --- /dev/null +++ b/Resources/Audio/Mecha/license.txt @@ -0,0 +1 @@ +mechmove03.ogg taken from TG station at commit https://github.com/tgstation/tgstation/commit/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0 \ No newline at end of file diff --git a/Resources/Audio/Mecha/mechmove03.ogg b/Resources/Audio/Mecha/mechmove03.ogg new file mode 100644 index 0000000000..44ec14d961 Binary files /dev/null and b/Resources/Audio/Mecha/mechmove03.ogg differ diff --git a/Resources/Locale/en-US/actions/actions/hardsuit.ftl b/Resources/Locale/en-US/actions/actions/hardsuit.ftl new file mode 100644 index 0000000000..38acd9df3a --- /dev/null +++ b/Resources/Locale/en-US/actions/actions/hardsuit.ftl @@ -0,0 +1,2 @@ +action-name-hardsuit = Toggle Helmet +action-description-hardsuit = Remember to equip the helmet before flinging yourself into space. \ No newline at end of file diff --git a/Resources/Locale/en-US/clothing/components/toggleable-clothing-component.ftl b/Resources/Locale/en-US/clothing/components/toggleable-clothing-component.ftl new file mode 100644 index 0000000000..eb342d028d --- /dev/null +++ b/Resources/Locale/en-US/clothing/components/toggleable-clothing-component.ftl @@ -0,0 +1,2 @@ + +toggleable-clothing-remove-first = You have to unequip {$entity} first. \ No newline at end of file diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index cd019bdabe..0269fb0952 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -15,6 +15,14 @@ iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png event: !type:ToggleActionEvent +- type: instantAction + id: ToggleSuitHelmet + name: action-name-hardsuit + description: action-description-hardsuit + itemIconStyle: BigItem + useDelay: 1 # equip noise spam. + event: !type:ToggleClothingEvent + - type: entityTargetAction id: Disarm name: action-name-disarm diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml index 34020f9c29..c3a8a38a57 100644 --- a/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml +++ b/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml @@ -189,4 +189,3 @@ - type: StorageFill contents: - id: ClothingOuterHardsuitSyndie - - id: ClothingHeadHelmetHardsuitSyndie diff --git a/Resources/Prototypes/Catalog/Fills/Crates/salvage.yml b/Resources/Prototypes/Catalog/Fills/Crates/salvage.yml index 13134e625d..38cdb40519 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/salvage.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/salvage.yml @@ -7,7 +7,6 @@ components: - type: StorageFill contents: - - id: ClothingHeadHelmetHardsuitSalvage - id: ClothingOuterHardsuitSalvage - id: ClothingMaskBreath - id: OxygenTankFilled diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml b/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml index ac6fed8901..1bee62c68b 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml @@ -9,7 +9,6 @@ - id: ClothingOuterHardsuitSalvage - id: YellowOxygenTank - id: ClothingShoesBootsMag - - id: ClothingHeadHelmetHardsuitSalvage - id: ClothingMaskGasExplorer # Currently do not function as 'true' mesons, so they're useless for salvagers. # - id: ClothingEyesGlassesMeson @@ -17,6 +16,5 @@ - id: FlashlightSeclite - id: SurvivalKnife - id: ClothingOuterHardsuitSpatio - - id: ClothingHeadHelmetHardsuitSpatio diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml b/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml index 03d69815a7..8988e7758c 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml @@ -78,7 +78,6 @@ components: - type: StorageFill contents: - - id: ClothingHeadHelmetHardsuitAtmos - id: ClothingOuterHardsuitAtmos - id: ClothingMaskGasAtmos - id: OxygenTankFilled diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml index a84a25112d..335678ba80 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml @@ -40,7 +40,6 @@ - id: ClothingOuterHardsuitCap - id: ClothingMaskGasCaptain - id: TaserGun - - id: ClothingHeadHelmetHardsuitCap - id: CommsComputerCircuitboard - id: ClothingHeadsetAltCommand - id: SpaceCash1000 @@ -91,7 +90,6 @@ components: - type: StorageFill contents: - - id: ClothingHeadHelmetHardsuitEngineeringWhite - id: ClothingOuterHardsuitEngineeringWhite - id: ClothingMaskBreath - id: OxygenTankFilled @@ -120,7 +118,6 @@ - id: ClothingBackpackDuffelSurgeryFilled - id: ClothingOuterCoatLabCmo - id: ClothingMaskSterile - - id: ClothingHeadHelmetHardsuitMedical - id: ClothingOuterHardsuitMedical - id: DiagnoserMachineCircuitboard - id: VaccinatorMachineCircuitboard @@ -142,7 +139,6 @@ - id: CircuitImprinterMachineCircuitboard - id: ClothingNeckCloakRd - id: ClothingHeadsetMedicalScience - - id: ClothingHeadHelmetHardsuitRd - id: ClothingOuterHardsuitRd - id: PlushieSlime prob: 0.1 @@ -171,7 +167,6 @@ prob: 0.1 - id: ClothingUniformJumpskirtHoSParadeMale prob: 0.1 - - id: ClothingHeadHelmetHardsuitSecurityRed - id: ClothingOuterHardsuitSecurityRed - id: ClothingMaskGasSecurity - id: ClothingShoeSlippersDuck diff --git a/Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml b/Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml index 176371a367..46edd5f845 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml @@ -9,5 +9,5 @@ Slots: [belt] size: 50 quickEquip: false - EquipSound: + equipSound: path: /Audio/Items/belt_equip.ogg diff --git a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml index 3cc64733f7..1b3dc756ab 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml @@ -48,9 +48,15 @@ parent: ClothingHeadBase id: ClothingHeadHardsuitBase name: base hardsuit helmet + noSpawn: true components: - type: Clothing + #Apparently the hardsuit helmet equip sound is from a walking mech? + equipSound: /Audio/Mecha/mechmove03.ogg + unequipSound: /Audio/Mecha/mechmove03.ogg size: 15 + canPickup: false # attached to suit. + quickEquip: false - type: PressureProtection highPressureMultiplier: 0.3 lowPressureMultiplier: 1000 @@ -76,6 +82,7 @@ parent: ClothingHeadHardsuitBase id: ClothingHeadHardsuitWithLightBase name: base hardsuit helmet with light + noSpawn: true components: - type: Sprite netsync: false diff --git a/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml b/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml index dc20b0f707..ce7b54971e 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml @@ -1,6 +1,7 @@ - type: entity parent: ClothingHeadHardsuitWithLightBase id: ClothingHeadHelmetHardsuitAtmos + noSpawn: true name: atmos hardsuit helmet description: A special hardsuit helmet designed for working in low-pressure, high thermal environments. components: @@ -62,6 +63,7 @@ - type: entity parent: ClothingHeadHardsuitBase id: ClothingHeadHelmetHardsuitCap + noSpawn: true name: captain's hardsuit helmet description: Special hardsuit helmet, made for the captain of the station. components: @@ -76,6 +78,7 @@ - type: entity parent: HatBase id: ClothingHeadHelmetHardsuitDeathsquad + noSpawn: true name: deathsquad hardsuit helmet description: A robust helmet for special operations. components: @@ -90,6 +93,7 @@ - type: entity parent: ClothingHeadHardsuitWithLightBase id: ClothingHeadHelmetHardsuitEngineering + noSpawn: true name: engineering hardsuit helmet description: An engineering hardsuit helmet designed for working in low-pressure, high radioactive environments. components: @@ -104,6 +108,7 @@ - type: entity parent: ClothingHeadHardsuitWithLightBase id: ClothingHeadHelmetHardsuitEngineeringWhite + noSpawn: true name: chief engineer's hardsuit helmet description: Special hardsuit helmet, made for the chief engineer of the station. components: @@ -118,6 +123,7 @@ - type: entity parent: HatBase id: ClothingHeadHelmetIHSVoidHelm + noSpawn: true name: IHS voidhelm description: A robust, tactical IHS helmet. components: @@ -132,6 +138,7 @@ - type: entity parent: ClothingHeadHardsuitWithLightBase id: ClothingHeadHelmetHardsuitMedical + noSpawn: true name: chief medical officer's hardsuit helmet description: Lightweight medical hardsuit helmet that doesn't restrict your head movements. components: @@ -148,6 +155,7 @@ - type: entity parent: ClothingHeadHardsuitWithLightBase id: ClothingHeadHelmetHardsuitRd + noSpawn: true name: experimental research hardsuit helmet description: Lightweight hardsuit helmet that doesn't restrict your head movements. components: @@ -162,6 +170,7 @@ - type: entity parent: ClothingHeadHardsuitWithLightBase id: ClothingHeadHelmetHardsuitSalvage + noSpawn: true name: salvage hardsuit helmet description: A special helmet designed for work in a hazardous, low pressure environment. Has reinforced plating for wildlife encounters and dual floodlights. components: @@ -176,6 +185,7 @@ - type: entity parent: ClothingHeadHardsuitWithLightBase id: ClothingHeadHelmetHardsuitSecurity + noSpawn: true name: security hardsuit helmet description: Armored hardsuit helmet for security needs. components: @@ -190,6 +200,7 @@ - type: entity parent: ClothingHeadHardsuitWithLightBase id: ClothingHeadHelmetHardsuitSecurityRed + noSpawn: true name: head of security's hardsuit helmet description: Security hardsuit helmet with the latest top secret NT-HUD software. Belongs to the HoS. components: @@ -204,6 +215,7 @@ - type: entity parent: ClothingHeadHardsuitWithLightBase id: ClothingHeadHelmetHardsuitSyndie + noSpawn: true name: blood red hardsuit helmet description: An advanced red hardsuit helmet designed for work in special operations. components: @@ -220,6 +232,7 @@ - type: entity parent: ClothingHeadHardsuitWithLightBase id: ClothingHeadHelmetHardsuitWizard + noSpawn: true name: wizard hardsuit helmet description: A bizarre gem-encrusted helmet that radiates magical energies. components: @@ -234,6 +247,7 @@ - type: entity parent: ClothingHeadHardsuitBase id: ClothingHeadHelmetHardsuitLing + noSpawn: true name: organic space helmet description: A spaceworthy biomass of pressure and temperature resistant tissue. components: @@ -248,6 +262,7 @@ - type: entity parent: ClothingHeadHardsuitWithLightBase id: ClothingHeadHelmetHardsuitSpatio + noSpawn: true name: spationaut hardsuit helmet description: A sturdy helmet designed for complex industrial operations in space. components: diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml index c53775046a..877652d0c6 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml @@ -35,6 +35,7 @@ Radiation: 0.25 - type: DiseaseProtection protection: 0.05 + - type: ToggleableClothing - type: entity abstract: true diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml index e2d9aaaaf5..fbb0875c8a 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml @@ -26,7 +26,9 @@ coefficient: 0.01 - type: ExplosionResistance resistance: 0.8 - + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitAtmos + - type: entity parent: ClothingOuterHardsuitBase id: ClothingOuterHardsuitCap @@ -53,6 +55,8 @@ Radiation: 0.1 - type: ExplosionResistance resistance: 0.5 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitCap - type: entity parent: ClothingOuterHardsuitBase @@ -80,6 +84,8 @@ Radiation: 0.2 - type: ExplosionResistance resistance: 0.3 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitDeathsquad - type: entity parent: ClothingOuterHardsuitBase @@ -107,7 +113,9 @@ Radiation: 0.35 - type: ExplosionResistance resistance: 0.8 - + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitEngineering + - type: entity parent: ClothingOuterHardsuitBase id: ClothingOuterHardsuitEngineeringWhite @@ -134,6 +142,8 @@ Radiation: 0.25 - type: ExplosionResistance resistance: 0.8 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitEngineeringWhite #The voidsuit and eva suit are not techically 'hardsuits' but this seems to be the place for all EVA-capable outer clothing. This may be worth reevaluating when we have a lot more items. - type: entity @@ -152,6 +162,8 @@ - type: PressureProtection highPressureMultiplier: 0.3 lowPressureMultiplier: 10000 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetIHSVoidHelm - type: entity parent: ClothingOuterEVASuitBase @@ -210,7 +222,9 @@ modifiers: coefficients: Heat: 0.90 - Radiation: 0.25 + Radiation: 0.25 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitMedical - type: entity parent: ClothingOuterHardsuitBase @@ -238,6 +252,8 @@ sprintModifier: 0.75 - type: ExplosionResistance resistance: 0.7 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitRd - type: entity parent: ClothingOuterHardsuitBase @@ -265,6 +281,8 @@ Radiation: 0.5 - type: ExplosionResistance resistance: 0.8 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitSalvage - type: entity parent: ClothingOuterHardsuitBase @@ -289,6 +307,8 @@ Radiation: 0.25 - type: ExplosionResistance resistance: 0.6 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitSecurity - type: entity parent: ClothingOuterHardsuitSecurity @@ -316,6 +336,8 @@ Radiation: 0.25 - type: ExplosionResistance resistance: 0.6 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitSecurityRed - type: entity parent: ClothingOuterHardsuitBase @@ -343,6 +365,8 @@ Radiation: 0.20 - type: ExplosionResistance resistance: 0.5 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitSyndie - type: entity parent: ClothingOuterHardsuitBase @@ -370,6 +394,8 @@ Radiation: 0.20 - type: ExplosionResistance resistance: 0.5 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitWizard - type: entity parent: ClothingOuterHardsuitBase @@ -397,6 +423,8 @@ Radiation: 0.3 - type: ExplosionResistance resistance: 0.8 + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitLing - type: entity parent: ClothingOuterHardsuitBase @@ -422,6 +450,8 @@ Piercing: 0.95 Heat: 0.9 Radiation: 0.5 #low physical but gave some rad protection in case of rad artifacts/rad storms + - type: ToggleableClothing + clothingPrototype: ClothingHeadHelmetHardsuitSpatio - type: entity parent: ClothingOuterEVASuitBase @@ -449,4 +479,3 @@ Slash: 0.95 Heat: 0.90 Radiation: 0.75 - diff --git a/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml b/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml index 0bab697a9a..e7075936f8 100644 --- a/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml +++ b/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml @@ -19,7 +19,7 @@ state: icon - type: Clothing Slots: [innerclothing] - EquipSound: + equipSound: path: /Audio/Items/jumpsuit_equip.ogg - type: Butcherable butcheringType: Knife @@ -37,7 +37,7 @@ - type: Clothing Slots: [innerclothing] femaleMask: UniformTop - EquipSound: + equipSound: path: /Audio/Items/jumpsuit_equip.ogg - type: Butcherable butcheringType: Knife diff --git a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml index 039b981d0f..a55d3d772a 100644 --- a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml +++ b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml @@ -52,7 +52,6 @@ equipment: jumpsuit: ClothingUniformJumpsuitColorBlack back: ClothingBackpackDuffelSyndicateAmmo - head: ClothingHeadHelmetHardsuitDeathsquad mask: ClothingMaskBreath eyes: ClothingEyesGlassesSecurity ears: ClothingHeadsetCentCom diff --git a/Resources/Prototypes/Roles/Jobs/Fun/wizard_startinggear.yml b/Resources/Prototypes/Roles/Jobs/Fun/wizard_startinggear.yml index 92243668aa..c4e26651ba 100644 --- a/Resources/Prototypes/Roles/Jobs/Fun/wizard_startinggear.yml +++ b/Resources/Prototypes/Roles/Jobs/Fun/wizard_startinggear.yml @@ -46,7 +46,6 @@ equipment: jumpsuit: ClothingUniformJumpsuitColorPurple back: ClothingBackpackFilled - head: ClothingHeadHelmetHardsuitWizard outerClothing: ClothingOuterHardsuitWizard shoes: ClothingShoesWizard id: AssistantPDA