From 1141c19d76e7eb2a0fafa4184fda29c5102386a3 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sat, 23 Apr 2022 15:31:45 +1200 Subject: [PATCH] Toggleable Hardsuit Helmets (#7559) --- Content.Client/Actions/ActionsSystem.cs | 2 +- Content.Client/Actions/UI/ActionMenuItem.cs | 4 +- Content.Client/Actions/UI/ActionSlot.cs | 4 +- Content.Client/Clothing/ClothingComponent.cs | 3 - .../Commands/SetOutfitCommand.cs | 2 +- .../Clothing/Components/ClothingComponent.cs | 3 - .../Disease/Zombie/DiseaseZombieSystem.cs | 2 +- .../Actions/ActionTypes/ActionType.cs | 12 + Content.Shared/Actions/SharedActionsSystem.cs | 2 +- .../Components/AttachedClothingComponent.cs | 20 ++ .../Components/ToggleableClothingComponent.cs | 59 +++++ .../EntitySystems/ToggleableClothingSystem.cs | 208 ++++++++++++++++++ .../EntitySystems/SharedHandsSystem.Pickup.cs | 2 +- .../Inventory/InventorySystem.Equip.cs | 61 ++++- Content.Shared/Item/SharedItemComponent.cs | 19 +- Content.Shared/Item/SharedItemSystem.cs | 4 +- Resources/Audio/Mecha/license.txt | 1 + Resources/Audio/Mecha/mechmove03.ogg | Bin 0 -> 13301 bytes .../Locale/en-US/actions/actions/hardsuit.ftl | 2 + .../toggleable-clothing-component.ftl | 2 + Resources/Prototypes/Actions/types.yml | 8 + .../Catalog/Fills/Backpacks/duffelbag.yml | 1 - .../Catalog/Fills/Crates/salvage.yml | 1 - .../Catalog/Fills/Lockers/cargo.yml | 2 - .../Catalog/Fills/Lockers/engineer.yml | 1 - .../Catalog/Fills/Lockers/heads.yml | 5 - .../Clothing/Belt/base_clothingbelt.yml | 2 +- .../Clothing/Head/base_clothinghead.yml | 7 + .../Clothing/Head/hardsuit-helmets.yml | 15 ++ .../OuterClothing/base_clothingouter.yml | 1 + .../Clothing/OuterClothing/hardsuits.yml | 37 +++- .../Uniforms/base_clothinguniforms.yml | 4 +- .../Roles/Jobs/Fun/misc_startinggear.yml | 1 - .../Roles/Jobs/Fun/wizard_startinggear.yml | 1 - 34 files changed, 449 insertions(+), 49 deletions(-) create mode 100644 Content.Shared/Clothing/Components/AttachedClothingComponent.cs create mode 100644 Content.Shared/Clothing/Components/ToggleableClothingComponent.cs create mode 100644 Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs create mode 100644 Resources/Audio/Mecha/license.txt create mode 100644 Resources/Audio/Mecha/mechmove03.ogg create mode 100644 Resources/Locale/en-US/actions/actions/hardsuit.ftl create mode 100644 Resources/Locale/en-US/clothing/components/toggleable-clothing-component.ftl 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 0000000000000000000000000000000000000000..44ec14d961946d9429c74b054dde3ce1e4206e4f GIT binary patch literal 13301 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