diff --git a/Content.Server/Clothing/Components/MaskComponent.cs b/Content.Server/Clothing/Components/MaskComponent.cs
new file mode 100644
index 0000000000..f2ea000d0e
--- /dev/null
+++ b/Content.Server/Clothing/Components/MaskComponent.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Actions;
+using Content.Shared.Actions.ActionTypes;
+
+namespace Content.Server.Clothing.Components
+{
+ [Access(typeof(MaskSystem))]
+ [RegisterComponent]
+ public sealed class MaskComponent : Component
+ {
+ ///
+ /// This mask can be toggled (pulled up/down)
+ ///
+ [DataField("toggleAction")]
+ public InstantAction? ToggleAction = null;
+
+ public bool IsToggled = false;
+ }
+
+ public sealed class ToggleMaskEvent : InstantActionEvent { }
+}
diff --git a/Content.Server/Clothing/MaskSystem.cs b/Content.Server/Clothing/MaskSystem.cs
new file mode 100644
index 0000000000..9b3d3a876b
--- /dev/null
+++ b/Content.Server/Clothing/MaskSystem.cs
@@ -0,0 +1,106 @@
+using Content.Shared.Actions;
+using Content.Shared.Toggleable;
+using Content.Shared.Inventory;
+using Content.Shared.Inventory.Events;
+using Content.Shared.Item;
+using Content.Server.Actions;
+using Content.Server.Atmos.Components;
+using Content.Server.Body.Components;
+using Content.Server.Clothing.Components;
+using Content.Server.Disease.Components;
+using Content.Server.Nutrition.EntitySystems;
+using Content.Server.Popups;
+using Robust.Shared.Player;
+
+namespace Content.Server.Clothing
+{
+ public sealed class MaskSystem : EntitySystem
+ {
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly InventorySystem _inventorySystem = default!;
+ [Dependency] private readonly ActionsSystem _actionSystem = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnToggleMask);
+ SubscribeLocalEvent(OnGetActions);
+ SubscribeLocalEvent(OnGotUnequipped);
+ }
+
+ private void OnGetActions(EntityUid uid, MaskComponent component, GetItemActionsEvent args)
+ {
+ if (component.ToggleAction != null && !args.InHands)
+ args.Actions.Add(component.ToggleAction);
+ }
+
+ private void OnToggleMask(EntityUid uid, MaskComponent mask, ToggleMaskEvent args)
+ {
+ if (mask.ToggleAction == null)
+ return;
+
+ if (!_inventorySystem.TryGetSlotEntity(args.Performer, "mask", out var existing) || !mask.Owner.Equals(existing))
+ return;
+
+ mask.IsToggled ^= true;
+ _actionSystem.SetToggled(mask.ToggleAction, mask.IsToggled);
+
+ if (mask.IsToggled)
+ _popupSystem.PopupEntity(Loc.GetString("action-mask-pull-down-popup-message", ("mask", mask.Owner)), args.Performer, Filter.Entities(args.Performer));
+ else
+ _popupSystem.PopupEntity(Loc.GetString("action-mask-pull-up-popup-message", ("mask", mask.Owner)), args.Performer, Filter.Entities(args.Performer));
+
+ ToggleMaskComponents(uid, mask, args.Performer);
+ }
+
+ // set to untoggled when unequipped, so it isn't left in a 'pulled down' state
+ private void OnGotUnequipped(EntityUid uid, MaskComponent mask, GotUnequippedEvent args)
+ {
+ if (mask.ToggleAction == null)
+ return;
+
+ mask.IsToggled = false;
+ _actionSystem.SetToggled(mask.ToggleAction, mask.IsToggled);
+
+ ToggleMaskComponents(uid, mask, args.Equipee, true);
+ }
+
+ private void ToggleMaskComponents(EntityUid uid, MaskComponent mask, EntityUid wearer, bool isEquip = false)
+ {
+ //toggle visuals
+ if (TryComp(mask.Owner, out var item))
+ {
+ //TODO: sprites for 'pulled down' state. defaults to invisible due to no sprite with this prefix
+ item.EquippedPrefix = mask.IsToggled ? "toggled" : null;
+ Dirty(item);
+ }
+
+ //toggle ingestion blocking
+ if (TryComp(uid, out var blocker))
+ blocker.Enabled = !mask.IsToggled;
+
+ //toggle disease protection
+ if (TryComp(uid, out var diseaseProtection))
+ diseaseProtection.IsActive = !mask.IsToggled;
+
+ //toggle breath tool connection (skip during equip since that is handled in LungSystem)
+ if (isEquip || !TryComp(uid, out var breathTool))
+ return;
+
+ if (mask.IsToggled)
+ {
+ breathTool.DisconnectInternals();
+ }
+ else
+ {
+ breathTool.IsFunctional = true;
+
+ if (TryComp(wearer, out InternalsComponent? internals))
+ {
+ breathTool.ConnectedInternalsEntity = wearer;
+ internals.ConnectBreathTool(uid);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/Nutrition/Components/IngestionBlockerComponent.cs b/Content.Server/Nutrition/Components/IngestionBlockerComponent.cs
index 7b1d3c64a4..c1764a7e28 100644
--- a/Content.Server/Nutrition/Components/IngestionBlockerComponent.cs
+++ b/Content.Server/Nutrition/Components/IngestionBlockerComponent.cs
@@ -1,3 +1,5 @@
+using Content.Server.Clothing;
+
namespace Content.Server.Nutrition.EntitySystems;
///
@@ -7,7 +9,7 @@ namespace Content.Server.Nutrition.EntitySystems;
/// In the event that more head-wear & mask functionality is added (like identity systems, or raising/lowering of
/// masks), then this component might become redundant.
///
-[RegisterComponent, Access(typeof(FoodSystem), typeof(DrinkSystem))]
+[RegisterComponent, Access(typeof(FoodSystem), typeof(DrinkSystem), typeof(MaskSystem))]
public sealed class IngestionBlockerComponent : Component
{
///
diff --git a/Resources/Locale/en-US/actions/actions/mask.ftl b/Resources/Locale/en-US/actions/actions/mask.ftl
new file mode 100644
index 0000000000..ec1113e4e6
--- /dev/null
+++ b/Resources/Locale/en-US/actions/actions/mask.ftl
@@ -0,0 +1,4 @@
+action-name-mask = Toggle Mask
+action-description-mask-toggle = Handy, but prevents insertion of pie into your pie hole.
+action-mask-pull-up-popup-message = You pull up your {$mask}.
+action-mask-pull-down-popup-message = You pull down your {$mask}.
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml b/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml
index a27d5b63db..d194d0fe18 100644
--- a/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml
+++ b/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml
@@ -7,3 +7,16 @@
state: icon
- type: Clothing
Slots: [mask]
+
+- type: entity
+ abstract: true
+ parent: ClothingMaskBase
+ id: ClothingMaskPullableBase
+ components:
+ - type: Mask
+ toggleAction:
+ name: action-name-mask
+ description: action-description-mask-toggle
+ icon: Clothing/Mask/gas.rsi/icon.png
+ iconOn: Interface/Inventory/blocked.png
+ event: !type:ToggleMaskEvent
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml
index 505ab33f56..d260371acd 100644
--- a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml
+++ b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml
@@ -1,5 +1,5 @@
- type: entity
- parent: ClothingMaskBase
+ parent: ClothingMaskPullableBase
id: ClothingMaskGas
name: gas mask
description: A face-covering mask that can be connected to an air supply.
@@ -14,7 +14,7 @@
protection: 0.05
- type: entity
- parent: ClothingMaskBase
+ parent: ClothingMaskPullableBase
id: ClothingMaskGasSecurity
name: security gas mask
description: A standard issue Security gas mask.
@@ -29,7 +29,7 @@
protection: 0.05
- type: entity
- parent: ClothingMaskBase
+ parent: ClothingMaskPullableBase
id: ClothingMaskGasSyndicate
name: syndicate gas mask
description: A close-fitting tactical mask that can be connected to an air supply.
@@ -45,7 +45,7 @@
- type: FlashImmunity
- type: entity
- parent: ClothingMaskBase
+ parent: ClothingMaskPullableBase
id: ClothingMaskGasAtmos
name: atmospheric gas mask
description: Improved gas mask utilized by atmospheric technicians. It's flameproof!
@@ -94,7 +94,7 @@
protection: 0.05
- type: entity
- parent: ClothingMaskBase
+ parent: ClothingMaskPullableBase
id: ClothingMaskGasExplorer
name: explorer gas mask
description: A military-grade gas mask that can be connected to an air supply.
@@ -116,7 +116,7 @@
Heat: 0.95
- type: entity
- parent: ClothingMaskBase
+ parent: ClothingMaskPullableBase
id: ClothingMaskBreathMedical
name: medical mask
description: A close-fitting sterile mask that can be connected to an air supply.
@@ -131,7 +131,7 @@
protection: 0.10
- type: entity
- parent: ClothingMaskBase
+ parent: ClothingMaskPullableBase
id: ClothingMaskBreath
name: breath mask
description: Might as well keep this on 24/7.
@@ -182,7 +182,7 @@
- type: BreathMask
- type: entity
- parent: ClothingMaskBase
+ parent: ClothingMaskPullableBase
id: ClothingMaskSterile
name: sterile mask
description: A sterile mask designed to help prevent the spread of diseases.
@@ -211,7 +211,7 @@
replacement: mumble
- type: entity
- parent: ClothingMaskBase
+ parent: ClothingMaskPullableBase
id: ClothingMaskPlague
name: plague doctor mask
description: A bad omen.