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;
// 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)
/// <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,
)
{
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>
/// <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,
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>
/// </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)
{
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;
}
}
/// 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)