]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Component for clothes to suppress emotes and scream action in general, and the muzzle...
authorCentronias <me@centronias.com>
Fri, 11 Jul 2025 17:18:15 +0000 (10:18 -0700)
committerGitHub <noreply@github.com>
Fri, 11 Jul 2025 17:18:15 +0000 (13:18 -0400)
* Component for clothes to suppress scream noise

GaggedComponent + AddGaggedClothingComponent and relevant systems to make them work.

Currently only stifles the scream _action_, not all emotes

because if a mime can silently emote, so can gagged you!

* fix comments

* swap to inventory relay

and make it more general such that specific emotes or emotes of a given category can be blocked

* power gloves shouldn't block snapping

* easy fixes

* blockable emote event

* pr comments, switch to using emote event mostly

* pr comments

add beforeEmoteEvent

add emote blocker name to popup

maybe some other stuff, I forget

* get rid of emoteevent's source because I don't need it anymore

* smol clean

* formatting, style, and one minor thing where having a muzzle in your pocket would gag you

Content.Server/Chat/Systems/ChatSystem.Emote.cs
Content.Server/Chat/Systems/ChatSystem.cs
Content.Server/Speech/Components/EmoteBlockerComponent.cs [new file with mode: 0644]
Content.Server/Speech/EntitySystems/EmoteBlockerSystem.cs [new file with mode: 0644]
Content.Shared/Emoting/EmoteAttemptEvent.cs [deleted file]
Content.Shared/Emoting/EmoteEvents.cs [new file with mode: 0644]
Content.Shared/Inventory/InventorySystem.Relay.cs
Resources/Locale/en-US/chat/emotes.ftl
Resources/Locale/en-US/emote/emote.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Clothing/Masks/masks.yml

index 75acd2493bbc606a06c5853f323abfac9df870aa..ee891e087064b4afc945a8a26834b457b1a32746 100644 (file)
@@ -1,5 +1,7 @@
 using System.Collections.Frozen;
+using Content.Server.Popups;
 using Content.Shared.Chat.Prototypes;
+using Content.Shared.Emoting;
 using Content.Shared.Speech;
 using Robust.Shared.Audio;
 using Robust.Shared.Prototypes;
@@ -10,6 +12,8 @@ namespace Content.Server.Chat.Systems;
 // emotes using emote prototype
 public partial class ChatSystem
 {
+    [Dependency] private readonly PopupSystem _popupSystem = default!;
+
     private FrozenDictionary<string, EmotePrototype> _wordEmoteDict = FrozenDictionary<string, EmotePrototype>.Empty;
 
     protected override void OnPrototypeReload(PrototypesReloadedEventArgs obj)
@@ -51,7 +55,8 @@ public partial class ChatSystem
     /// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
     /// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
     /// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
-    public void TryEmoteWithChat(
+    /// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
+    public bool TryEmoteWithChat(
         EntityUid source,
         string emoteId,
         ChatTransmitRange range = ChatTransmitRange.Normal,
@@ -62,8 +67,8 @@ public partial class ChatSystem
         )
     {
         if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
-            return;
-        TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
+            return false;
+        return TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
     }
 
     /// <summary>
@@ -76,7 +81,8 @@ public partial class ChatSystem
     /// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
     /// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
     /// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
-    public void TryEmoteWithChat(
+    /// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
+    public bool TryEmoteWithChat(
         EntityUid source,
         EmotePrototype emote,
         ChatTransmitRange range = ChatTransmitRange.Normal,
@@ -84,43 +90,46 @@ public partial class ChatSystem
         string? nameOverride = null,
         bool ignoreActionBlocker = false,
         bool forceEmote = false
-        )
+    )
     {
         if (!forceEmote && !AllowedToUseEmote(source, emote))
-            return;
+            return false;
+
+        var didEmote = TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
 
         // check if proto has valid message for chat
-        if (emote.ChatMessages.Count != 0)
+        if (didEmote && emote.ChatMessages.Count != 0)
         {
             // not all emotes are loc'd, but for the ones that are we pass in entity
             var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source));
             SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker);
         }
 
-        // do the rest of emote event logic here
-        TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
+        return didEmote;
     }
 
     /// <summary>
     ///     Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
     /// </summary>
-    public void TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
+    /// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
+    public bool TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
     {
         if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
-            return;
+            return false;
 
-        TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
+        return TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
     }
 
     /// <summary>
     ///     Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
     /// </summary>
-    public void TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
+    /// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
+    public bool TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
     {
         if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker)
-            return;
+            return false;
 
-        InvokeEmoteEvent(uid, proto);
+        return TryInvokeEmoteEvent(uid, proto);
     }
 
     /// <summary>
@@ -160,17 +169,17 @@ public partial class ChatSystem
     /// </summary>
     /// <param name="uid"></param>
     /// <param name="textInput"></param>
-    private void TryEmoteChatInput(EntityUid uid, string textInput)
+    /// <returns>True if the chat message should be displayed (because the emote was explicitly cancelled), false if it should not be.</returns>
+    private bool TryEmoteChatInput(EntityUid uid, string textInput)
     {
         var actionTrimmedLower = TrimPunctuation(textInput.ToLower());
         if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote))
-            return;
+            return true;
 
         if (!AllowedToUseEmote(uid, emote))
-            return;
+            return true;
 
-        InvokeEmoteEvent(uid, emote);
-        return;
+        return TryInvokeEmoteEvent(uid, emote);
 
         static string TrimPunctuation(string textInput)
         {
@@ -220,11 +229,50 @@ public partial class ChatSystem
         return true;
     }
 
-
-    private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
+    /// <summary>
+    /// Creates and raises <see cref="BeforeEmoteEvent"/> and then <see cref="EmoteEvent"/> to let other systems do things like play audio.
+    /// In the case that the Before event is cancelled, EmoteEvent will NOT be raised, and will optionally show a message to the player
+    /// explaining why the emote didn't happen.
+    /// </summary>
+    /// <param name="uid">The entity which is emoting</param>
+    /// <param name="proto">The emote which is being performed</param>
+    /// <returns>True if the emote was performed, false otherwise.</returns>
+    private bool TryInvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
     {
+        var beforeEv = new BeforeEmoteEvent(uid, proto);
+        RaiseLocalEvent(uid, ref beforeEv);
+
+        if (beforeEv.Cancelled)
+        {
+            if (beforeEv.Blocker != null)
+            {
+                _popupSystem.PopupEntity(
+                    Loc.GetString(
+                        "chat-system-emote-cancelled-blocked",
+                        ("emote", Loc.GetString(proto.Name).ToLower()),
+                        ("blocker", beforeEv.Blocker.Value)
+                    ),
+                    uid,
+                    uid
+                );
+            }
+            else
+            {
+                _popupSystem.PopupEntity(
+                    Loc.GetString("chat-system-emote-cancelled-generic",
+                        ("emote", Loc.GetString(proto.Name).ToLower())),
+                    uid,
+                    uid
+                );
+            }
+
+            return false;
+        }
+
         var ev = new EmoteEvent(proto);
         RaiseLocalEvent(uid, ref ev);
+
+        return true;
     }
 }
 
@@ -233,9 +281,8 @@ public partial class ChatSystem
 ///     Use it to play sound, change sprite or something else.
 /// </summary>
 [ByRefEvent]
-public struct EmoteEvent
+public sealed class EmoteEvent : HandledEntityEventArgs
 {
-    public bool Handled;
     public readonly EmotePrototype Emote;
 
     public EmoteEvent(EmotePrototype emote)
index f21c3f78aa2638376681e30d5bcc80023dc4e57d..e0e6273f77e1c409d1b33d17fcf3addeea8401d7 100644 (file)
@@ -5,9 +5,8 @@ using Content.Server.Administration.Logs;
 using Content.Server.Administration.Managers;
 using Content.Server.Chat.Managers;
 using Content.Server.GameTicking;
-using Content.Server.Players.RateLimiting;
-using Content.Server.Speech.Prototypes;
 using Content.Server.Speech.EntitySystems;
+using Content.Server.Speech.Prototypes;
 using Content.Server.Station.Components;
 using Content.Server.Station.Systems;
 using Content.Shared.ActionBlocker;
@@ -593,8 +592,10 @@ public sealed partial class ChatSystem : SharedChatSystem
             ("entity", ent),
             ("message", FormattedMessage.RemoveMarkupOrThrow(action)));
 
-        if (checkEmote)
-            TryEmoteChatInput(source, action);
+        if (checkEmote &&
+            !TryEmoteChatInput(source, action))
+            return;
+
         SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
         if (!hideLog)
             if (name != Name(source))
diff --git a/Content.Server/Speech/Components/EmoteBlockerComponent.cs b/Content.Server/Speech/Components/EmoteBlockerComponent.cs
new file mode 100644 (file)
index 0000000..603fd1b
--- /dev/null
@@ -0,0 +1,24 @@
+using Content.Shared.Chat.Prototypes;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Speech.Components;
+
+/// <summary>
+/// Suppresses emotes with the given categories or ID.
+/// Additionally, if the Scream Emote would be blocked, also blocks the Scream Action.
+/// </summary>
+[RegisterComponent]
+public sealed partial class EmoteBlockerComponent : Component
+{
+    /// <summary>
+    /// Which categories of emotes are blocked by this component.
+    /// </summary>
+    [DataField]
+    public HashSet<EmoteCategory> BlocksCategories = [];
+
+    /// <summary>
+    /// IDs of which specific emotes are blocked by this component.
+    /// </summary>
+    [DataField]
+    public HashSet<ProtoId<EmotePrototype>> BlocksEmotes = [];
+}
diff --git a/Content.Server/Speech/EntitySystems/EmoteBlockerSystem.cs b/Content.Server/Speech/EntitySystems/EmoteBlockerSystem.cs
new file mode 100644 (file)
index 0000000..3599268
--- /dev/null
@@ -0,0 +1,41 @@
+using Content.Server.Speech.Components;
+using Content.Shared.Emoting;
+using Content.Shared.Inventory;
+
+namespace Content.Server.Speech.EntitySystems;
+
+public sealed class EmoteBlockerSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<EmoteBlockerComponent, BeforeEmoteEvent>(OnEmoteEvent);
+        SubscribeLocalEvent<EmoteBlockerComponent, InventoryRelayedEvent<BeforeEmoteEvent>>(OnRelayedEmoteEvent);
+    }
+
+    private static void OnRelayedEmoteEvent(Entity<EmoteBlockerComponent> entity, ref InventoryRelayedEvent<BeforeEmoteEvent> args)
+    {
+        OnEmoteEvent(entity, ref args.Args);
+    }
+
+    private static void OnEmoteEvent(Entity<EmoteBlockerComponent> entity, ref BeforeEmoteEvent args)
+    {
+        if (entity.Comp.BlocksEmotes.Contains(args.Emote))
+        {
+            args.Cancel();
+            args.Blocker = entity;
+            return;
+        }
+
+        foreach (var blockedCat in entity.Comp.BlocksCategories)
+        {
+            if (blockedCat == args.Emote.Category)
+            {
+                args.Cancel();
+                args.Blocker = entity;
+                return;
+            }
+        }
+    }
+}
diff --git a/Content.Shared/Emoting/EmoteAttemptEvent.cs b/Content.Shared/Emoting/EmoteAttemptEvent.cs
deleted file mode 100644 (file)
index 3592646..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Content.Shared.Emoting
-{
-    public sealed class EmoteAttemptEvent : CancellableEntityEventArgs
-    {
-        public EmoteAttemptEvent(EntityUid uid)
-        {
-            Uid = uid;
-        }
-
-        public EntityUid Uid { get; }
-    }
-}
diff --git a/Content.Shared/Emoting/EmoteEvents.cs b/Content.Shared/Emoting/EmoteEvents.cs
new file mode 100644 (file)
index 0000000..ea3073f
--- /dev/null
@@ -0,0 +1,27 @@
+using Content.Shared.Chat.Prototypes;
+using Content.Shared.Inventory;
+
+namespace Content.Shared.Emoting;
+
+public sealed class EmoteAttemptEvent(EntityUid uid) : CancellableEntityEventArgs
+{
+    public EntityUid Uid { get; } = uid;
+}
+
+/// <summary>
+/// An event raised just before an emote is performed, providing systems with an opportunity to cancel the emote's performance.
+/// </summary>
+[ByRefEvent]
+public sealed class BeforeEmoteEvent(EntityUid source, EmotePrototype emote)
+    : CancellableEntityEventArgs, IInventoryRelayEvent
+{
+    public readonly EntityUid Source = source;
+    public readonly EmotePrototype Emote = emote;
+
+    /// <summary>
+    ///     The equipment that is blocking emoting. Should only be non-null if the event was canceled.
+    /// </summary>
+    public EntityUid? Blocker = null;
+
+    public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET;
+}
index f6a719a59beae5f6c57c79153b59a650595e8ba9..fe3267a92f060b2016fcbe0ce47fedc093c6f3b5 100644 (file)
@@ -8,6 +8,7 @@ using Content.Shared.Contraband;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Events;
 using Content.Shared.Electrocution;
+using Content.Shared.Emoting;
 using Content.Shared.Explosion;
 using Content.Shared.Eye.Blinding.Systems;
 using Content.Shared.Flash;
@@ -54,6 +55,7 @@ public partial class InventorySystem
         SubscribeLocalEvent<InventoryComponent, IsEquippingTargetAttemptEvent>(RelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, IsUnequippingTargetAttemptEvent>(RelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, ChameleonControllerOutfitSelectedEvent>(RelayInventoryEvent);
+        SubscribeLocalEvent<InventoryComponent, BeforeEmoteEvent>(RelayInventoryEvent);
 
         // by-ref events
         SubscribeLocalEvent<InventoryComponent, RefreshFrictionModifiersEvent>(RefRelayInventoryEvent);
index 0944aa48dd8667fc78ab8686b888b1a59dbc6a3c..cf6d91b511ea4c49ec5d5a73b1b9ed59ebfce00a 100644 (file)
@@ -4,7 +4,7 @@ chat-emote-name-laugh = Laugh
 chat-emote-name-honk = Honk
 chat-emote-name-sigh = Sigh
 chat-emote-name-whistle = Whistle
-chat-emote-name-crying = Crying
+chat-emote-name-crying = Cry
 chat-emote-name-squish = Squish
 chat-emote-name-chitter = Chitter
 chat-emote-name-squeak = Squeak
@@ -27,8 +27,8 @@ chat-emote-name-ping = Ping
 chat-emote-name-sneeze = Sneeze
 chat-emote-name-cough = Cough
 chat-emote-name-catmeow = Cat Meow
-chat-emote-name-cathisses = Cat Hisses
-chat-emote-name-monkeyscreeches = Monkey Screeches
+chat-emote-name-cathisses = Cat Hiss
+chat-emote-name-monkeyscreeches = Monkey Screech
 chat-emote-name-robotbeep = Robot
 chat-emote-name-yawn = Yawn
 chat-emote-name-snore = Snore
diff --git a/Resources/Locale/en-US/emote/emote.ftl b/Resources/Locale/en-US/emote/emote.ftl
new file mode 100644 (file)
index 0000000..11ccd71
--- /dev/null
@@ -0,0 +1,2 @@
+chat-system-emote-cancelled-generic = You can't {$emote} right now!
+chat-system-emote-cancelled-blocked = You can't {$emote} because of {THE($blocker)}!
index e45618b5131afb651bc30f958eb1ef7fa155237d..e1db710fec4180c10135780fa4875046381ef766 100644 (file)
   - type: IngestionBlocker
   - type: AddAccentClothing
     accent: MumbleAccent
+  - type: EmoteBlocker
+    blocksCategories:
+    - Vocal
   - type: Construction
     graph: Muzzle
     node: muzzle