Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component for clothes to suppress emotes and scream action in general, and the muzzle to suppress vocal emotes in particular #32588

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
85 changes: 62 additions & 23 deletions Content.Server/Chat/Systems/ChatSystem.Emote.cs
Original file line number Diff line number Diff line change
@@ -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.Prototypes;
using Robust.Shared.Random;
Expand All @@ -9,6 +11,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)
Expand Down Expand Up @@ -50,7 +54,8 @@ private void CacheEmotes()
/// <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,
Expand All @@ -61,8 +66,8 @@ public void TryEmoteWithChat(
)
{
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>
Expand All @@ -75,51 +80,55 @@ public void TryEmoteWithChat(
/// <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,
bool hideLog = false,
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>
Expand Down Expand Up @@ -159,17 +168,17 @@ public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, string
/// </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)
{
Expand Down Expand Up @@ -207,10 +216,41 @@ private bool AllowedToUseEmote(EntityUid source, EmotePrototype emote)
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;
}
}

Expand All @@ -219,9 +259,8 @@ private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
/// 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)
Expand Down
8 changes: 6 additions & 2 deletions Content.Server/Chat/Systems/ChatSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Players.RateLimiting;
using Content.Server.Speech.Components;
using Content.Server.Speech.EntitySystems;
using Content.Server.Station.Components;
Expand Down Expand Up @@ -594,7 +593,12 @@ private void SendEntityEmote(
("message", FormattedMessage.RemoveMarkupOrThrow(action)));

if (checkEmote)
TryEmoteChatInput(source, action);
{
var emoteSucceeded = TryEmoteChatInput(source, action);
if (!emoteSucceeded)
return;
}

SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
if (!hideLog)
if (name != Name(source))
Expand Down
26 changes: 26 additions & 0 deletions Content.Server/Speech/Components/EmoteBlockerComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public HashSet<EmoteCategory> BlocksCategories = [];

/// <summary>
/// IDs of which specific emotes are blocked by this component.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public HashSet<ProtoId<EmotePrototype>> BlocksEmotes = [];
}
41 changes: 41 additions & 0 deletions Content.Server/Speech/EntitySystems/EmoteBlockerSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Content.Server.Speech.Components;
using Content.Shared.Emoting;
using Content.Shared.Inventory;

namespace Content.Server.Speech;

public sealed class EmoteBlockerSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<EmoteBlockerComponent, BeforeEmoteEvent>(OnEmoteEvent);
SubscribeLocalEvent<EmoteBlockerComponent, InventoryRelayedEvent<BeforeEmoteEvent>>(OnRelayedEmoteEvent);
}

private void OnRelayedEmoteEvent(EntityUid uid, EmoteBlockerComponent component, InventoryRelayedEvent<BeforeEmoteEvent> args)
{
OnEmoteEvent(uid, component, ref args.Args);
}

private void OnEmoteEvent(EntityUid uid, EmoteBlockerComponent component, ref BeforeEmoteEvent args)
{
if (component.BlocksEmotes.Contains(args.Emote))
{
args.Cancel();
args.Blocker = uid;
return;
}

foreach (var blockedCat in component.BlocksCategories)
{
if (blockedCat == args.Emote.Category)
{
args.Cancel();
args.Blocker = uid;
return;
}
}
}
}
12 changes: 0 additions & 12 deletions Content.Shared/Emoting/EmoteAttemptEvent.cs
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to EmoteEvents.cs

This file was deleted.

37 changes: 37 additions & 0 deletions Content.Shared/Emoting/EmoteEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Content.Shared.Chat.Prototypes;
using Content.Shared.Inventory;

namespace Content.Shared.Emoting;

public sealed class EmoteAttemptEvent : CancellableEntityEventArgs
{
public EmoteAttemptEvent(EntityUid uid)
{
Uid = uid;
}

public EntityUid Uid { get; }
}

/// <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 : CancellableEntityEventArgs, IInventoryRelayEvent
{
public readonly EntityUid Source;
public readonly EmotePrototype Emote;

/// <summary>
/// The equipment that is blocking emoting. Should only be non-null if the event was canceled.
/// </summary>
public EntityUid? Blocker = null;

public BeforeEmoteEvent(EntityUid source, EmotePrototype emote)
{
Source = source;
Emote = emote;
}

public SlotFlags TargetSlots => SlotFlags.All;
}
2 changes: 2 additions & 0 deletions Content.Shared/Inventory/InventorySystem.Relay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Content.Shared.Temperature;
using Content.Shared.Verbs;
using Content.Shared.Chat;
using Content.Shared.Emoting;
Centronias marked this conversation as resolved.
Show resolved Hide resolved

namespace Content.Shared.Inventory;

Expand All @@ -33,6 +34,7 @@ public void InitializeRelay()
SubscribeLocalEvent<InventoryComponent, GetDefaultRadioChannelEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshNameModifiersEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, TransformSpeakerNameEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, BeforeEmoteEvent>(RelayInventoryEvent);

// by-ref events
SubscribeLocalEvent<InventoryComponent, GetExplosionResistanceEvent>(RefRelayInventoryEvent);
Expand Down
6 changes: 3 additions & 3 deletions Resources/Locale/en-US/chat/emotes.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ chat-emote-name-laugh = Laugh
chat-emote-name-honk = Honk
chat-emote-name-sigh = Sigh
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fully agree but I think we are supposed to do cleanup like this in a separate PR (Unless its necessary for your PR to work)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with making another PR, but in this case, it's kiiiiinda "for it to work" in that it makes the grammar correct in this popup:
https://github.com/space-wizards/space-station-14/pull/32588/files#diff-9800b8c546f5be15ba58e55af1406f1d04f5f40de312c99a1210739ad79ac229R101

Without it, the localized string You can't {$emote} right now! would end up as like You can't crying right now!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah then it makes sense for it to be here I think 👍

chat-emote-name-whistle = Whistle
chat-emote-name-crying = Crying
chat-emote-name-crying = Cry
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this while testing -- nearly all of the other emotes are in the infinitive tense rather than the gerund like Crying here.
(okay if you wanna fight me, it's probably more accurately described as the imperative mood without a person-conjugation, but the point remains that it was wrong)

chat-emote-name-squish = Squish
chat-emote-name-chitter = Chitter
chat-emote-name-squeak = Squeak
Expand All @@ -25,8 +25,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
Comment on lines +28 to +29
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as Crying above -- these seemed to have been in the third person declarative.

chat-emote-name-robotbeep = Robot
chat-emote-name-yawn = Yawn
chat-emote-name-snore = Snore
Expand Down
2 changes: 2 additions & 0 deletions Resources/Locale/en-US/emote/emote.ftl
Original file line number Diff line number Diff line change
@@ -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)}!
3 changes: 3 additions & 0 deletions Resources/Prototypes/Entities/Clothing/Masks/masks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@
- type: AddAccentClothing
accent: ReplacementAccent
replacement: mumble
- type: EmoteBlocker
blocksCategories:
- Vocal
Comment on lines +324 to +326
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Muzzles block all vocal emotes.

- type: Construction
graph: Muzzle
node: muzzle
Expand Down
Loading