Move ChatSystem.Emotes to shared (#40866)

* move to shared

* entity effect to shared

* refactor: whitespaces+xml-doc typo fixups

* refactor: a little bit more of xml-doc typos fixups

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
This commit is contained in:
slarticodefast
2025-10-13 20:06:01 +02:00
committed by GitHub
parent 774468ad71
commit ee9d1032bb
28 changed files with 209 additions and 171 deletions

View File

@@ -6,6 +6,7 @@ using Content.Server.Popups;
using Content.Shared.Access; using Content.Shared.Access;
using Content.Shared.Access.Components; using Content.Shared.Access.Components;
using Content.Shared.Access.Systems; using Content.Shared.Access.Systems;
using Content.Shared.Chat;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Popups; using Content.Shared.Popups;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;

View File

@@ -8,6 +8,7 @@ using Content.Shared.Atmos;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Body.Events; using Content.Shared.Body.Events;
using Content.Shared.Body.Prototypes; using Content.Shared.Body.Prototypes;
using Content.Shared.Chat;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;

View File

@@ -1,5 +1,6 @@
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Chat;
using Robust.Shared.Console; using Robust.Shared.Console;
using Robust.Shared.Enums; using Robust.Shared.Enums;

View File

@@ -1,5 +1,6 @@
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Chat;
using Robust.Shared.Console; using Robust.Shared.Console;
using Robust.Shared.Enums; using Robust.Shared.Enums;

View File

@@ -1,5 +1,6 @@
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Chat;
using Robust.Shared.Console; using Robust.Shared.Console;
using Robust.Shared.Enums; using Robust.Shared.Enums;

View File

@@ -1,4 +1,5 @@
using System.Linq; using System.Linq;
using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;

View File

@@ -68,7 +68,7 @@ public sealed partial class ChatSystem : SharedChatSystem
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
CacheEmotes();
Subs.CVar(_configurationManager, CCVars.LoocEnabled, OnLoocEnabledChanged, true); Subs.CVar(_configurationManager, CCVars.LoocEnabled, OnLoocEnabledChanged, true);
Subs.CVar(_configurationManager, CCVars.DeadLoocEnabled, OnDeadLoocEnabledChanged, true); Subs.CVar(_configurationManager, CCVars.DeadLoocEnabled, OnDeadLoocEnabledChanged, true);
Subs.CVar(_configurationManager, CCVars.CritLoocEnabled, OnCritLoocEnabledChanged, true); Subs.CVar(_configurationManager, CCVars.CritLoocEnabled, OnCritLoocEnabledChanged, true);
@@ -563,7 +563,7 @@ public sealed partial class ChatSystem : SharedChatSystem
} }
} }
private void SendEntityEmote( protected override void SendEntityEmote(
EntityUid source, EntityUid source,
string action, string action,
ChatTransmitRange range, ChatTransmitRange range,
@@ -975,18 +975,3 @@ public enum InGameOOCChatType : byte
Looc, Looc,
Dead Dead
} }
/// <summary>
/// Controls transmission of chat.
/// </summary>
public enum ChatTransmitRange : byte
{
/// Acts normal, ghosts can hear across the map, etc.
Normal,
/// Normal but ghosts are still range-limited.
GhostRangeLimit,
/// Hidden from the chat window.
HideChat,
/// Ghosts can't hear or see it at all. Regular players can if in-range.
NoGhosts
}

View File

@@ -1,5 +1,6 @@
namespace Content.Server.Chat.Systems; namespace Content.Server.Chat.Systems;
using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Content.Shared.Damage; using Content.Shared.Damage;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;

View File

@@ -1,20 +1,21 @@
using Content.Server.Popups;
using Content.Shared.Popups;
using Content.Shared.Mobs;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.Clothing.Systems;
using Content.Shared.Chat.Prototypes;
using Robust.Shared.Random;
using Content.Shared.Stunnable;
using Content.Shared.Damage;
using Robust.Shared.Prototypes;
using Content.Server.Emoting.Systems; using Content.Server.Emoting.Systems;
using Content.Server.Clothing.Systems;
using Content.Server.Popups;
using Content.Server.Speech.EntitySystems; using Content.Server.Speech.EntitySystems;
using Content.Shared.Cluwne; using Content.Shared.Chat;
using Robust.Shared.Audio.Systems; using Content.Shared.Chat.Prototypes;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Clumsy; using Content.Shared.Clumsy;
using Content.Shared.Cluwne;
using Content.Shared.Damage;
using Content.Shared.Mobs;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Stunnable;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
namespace Content.Server.Cluwne; namespace Content.Server.Cluwne;
@@ -75,7 +76,7 @@ public sealed class CluwneSystem : EntitySystem
EnsureComp<AutoEmoteComponent>(ent.Owner); EnsureComp<AutoEmoteComponent>(ent.Owner);
_autoEmote.AddEmote(ent.Owner, ent.Comp.AutoEmoteId); _autoEmote.AddEmote(ent.Owner, ent.Comp.AutoEmoteId);
} }
EnsureComp<ClumsyComponent>(ent.Owner); EnsureComp<ClumsyComponent>(ent.Owner);
var transformMessage = Loc.GetString(ent.Comp.TransformMessage, ("target", ent.Owner)); var transformMessage = Loc.GetString(ent.Comp.TransformMessage, ("target", ent.Owner));

View File

@@ -1,5 +1,6 @@
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.Emoting.Components; using Content.Server.Emoting.Components;
using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;

View File

@@ -1,22 +0,0 @@
using Content.Server.Chat.Systems;
using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.Effects;
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// Makes this entity emote.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class EmoteEntityEffectSystem : EntityEffectSystem<MetaDataComponent, Emote>
{
[Dependency] private readonly ChatSystem _chat = default!;
protected override void Effect(Entity<MetaDataComponent> entity, ref EntityEffectEvent<Emote> args)
{
if (args.Effect.ShowInChat)
_chat.TryEmoteWithChat(entity, args.Effect.EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: args.Effect.Force);
else
_chat.TryEmoteWithoutChat(entity, args.Effect.EmoteId);
}
}

View File

@@ -4,6 +4,7 @@ using Content.Server.Power.EntitySystems;
using Content.Server.Telephone; using Content.Server.Telephone;
using Content.Shared.Access.Systems; using Content.Shared.Access.Systems;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Chat;
using Content.Shared.Chat.TypingIndicator; using Content.Shared.Chat.TypingIndicator;
using Content.Shared.Holopad; using Content.Shared.Holopad;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;

View File

@@ -1,7 +1,7 @@
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Speech.Muting; using Content.Shared.Chat;
using Content.Shared.Mobs; using Content.Shared.Mobs;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;

View File

@@ -1,5 +1,5 @@
using Content.Server.Chat.Systems; using Content.Shared.Chat;
using Content.Shared.Chat; using Content.Server.Chat.Systems;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.Speech; namespace Content.Server.Speech;

View File

@@ -1,5 +1,6 @@
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.Speech.Components; using Content.Server.Speech.Components;
using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Content.Shared.Speech; using Content.Shared.Speech;
using Content.Shared.Speech.Components; using Content.Shared.Speech.Components;

View File

@@ -1,12 +1,10 @@
using Content.Server.Actions; using Content.Server.Actions;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.Speech.Components; using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Content.Shared.Cloning.Events;
using Content.Shared.Humanoid; using Content.Shared.Humanoid;
using Content.Shared.Speech; using Content.Shared.Speech;
using Content.Shared.Speech.Components; using Content.Shared.Speech.Components;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;

View File

@@ -1,8 +1,7 @@
using Content.Shared.Abilities.Mime;
using Content.Server.Chat.Systems;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Speech.Components;
using Content.Server.Speech.EntitySystems; using Content.Server.Speech.EntitySystems;
using Content.Shared.Abilities.Mime;
using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Content.Shared.Puppet; using Content.Shared.Puppet;
using Content.Shared.Speech; using Content.Shared.Speech;

View File

@@ -1,4 +1,5 @@
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Shared.Chat;
using Content.Shared.Teleportation; using Content.Shared.Teleportation;
using Content.Shared.Teleportation.Components; using Content.Shared.Teleportation.Components;
using Content.Shared.Teleportation.Systems; using Content.Shared.Teleportation.Systems;

View File

@@ -2,6 +2,7 @@ using Content.Server.Chat.Systems;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.Vocalization.Components; using Content.Server.Vocalization.Components;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Chat;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;

View File

@@ -10,6 +10,7 @@ using Content.Shared.Anomaly.Components;
using Content.Shared.Armor; using Content.Shared.Armor;
using Content.Shared.Bed.Sleep; using Content.Shared.Bed.Sleep;
using Content.Shared.Cloning.Events; using Content.Shared.Cloning.Events;
using Content.Shared.Chat;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Humanoid; using Content.Shared.Humanoid;
using Content.Shared.Inventory; using Content.Shared.Inventory;

View File

@@ -1,9 +1,49 @@
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Content.Shared.Inventory;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.Chat; namespace Content.Shared.Chat;
/// <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;
}
/// <summary>
/// Raised by the chat system when an entity made some emote.
/// Use it to play sound, change sprite or something else.
/// </summary>
[ByRefEvent]
public record struct EmoteEvent(EmotePrototype Emote)
{
/// <summary>
/// The used emote.
/// </summary>
public EmotePrototype Emote = Emote;
/// <summary>
/// If this message has already been "handled" by a previous system.
/// </summary>
public bool Handled;
}
/// <summary>
/// Sent by the client when requesting the server to play a specific emote selected from the emote radial menu.
/// </summary>
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class PlayEmoteMessage(ProtoId<EmotePrototype> protoId) : EntityEventArgs public sealed class PlayEmoteMessage(ProtoId<EmotePrototype> protoId) : EntityEventArgs
{ {

View File

@@ -1,28 +1,15 @@
using System.Collections.Frozen; using System.Collections.Frozen;
using Content.Server.Popups;
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Content.Shared.Emoting;
using Content.Shared.Speech; using Content.Shared.Speech;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
namespace Content.Server.Chat.Systems; namespace Content.Shared.Chat;
// emotes using emote prototype public abstract partial class SharedChatSystem
public partial class ChatSystem
{ {
[Dependency] private readonly PopupSystem _popupSystem = default!;
private FrozenDictionary<string, EmotePrototype> _wordEmoteDict = FrozenDictionary<string, EmotePrototype>.Empty; private FrozenDictionary<string, EmotePrototype> _wordEmoteDict = FrozenDictionary<string, EmotePrototype>.Empty;
protected override void OnPrototypeReload(PrototypesReloadedEventArgs obj)
{
base.OnPrototypeReload(obj);
if (obj.WasModified<EmotePrototype>())
CacheEmotes();
}
private void CacheEmotes() private void CacheEmotes()
{ {
var dict = new Dictionary<string, EmotePrototype>(); var dict = new Dictionary<string, EmotePrototype>();
@@ -47,15 +34,19 @@ public partial class ChatSystem
} }
/// <summary> /// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> and sends message to chat. /// Makes the selected entity emote using the given <see cref="EmotePrototype"/> and sends a message to chat.
/// </summary> /// </summary>
/// <param name="source">The entity that is speaking</param> /// <param name="source">The entity that is speaking</param>
/// <param name="emoteId">The id of emote prototype. Should has valid <see cref="EmotePrototype.ChatMessages"/></param> /// <param name="emoteId">The id of emote prototype. Should have valid <see cref="EmotePrototype.ChatMessages"/></param>
/// <param name="hideLog">Whether or not this message should appear in the adminlog window</param> /// <param name="hideLog">Whether this message should appear in the adminlog window, or not.</param>
/// <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="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="ignoreActionBlocker">Whether emote action blocking should be ignored or not.</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> /// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns> /// <returns>True if an emote was performed. False if the emote is unavailable, cancelled, etc.</returns>
public bool TryEmoteWithChat( public bool TryEmoteWithChat(
EntityUid source, EntityUid source,
string emoteId, string emoteId,
@@ -64,24 +55,28 @@ public partial class ChatSystem
string? nameOverride = null, string? nameOverride = null,
bool ignoreActionBlocker = false, bool ignoreActionBlocker = false,
bool forceEmote = false bool forceEmote = false
) )
{ {
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto)) if (!_prototypeManager.Resolve<EmotePrototype>(emoteId, out var proto))
return false; return false;
return TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote); return TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
} }
/// <summary> /// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> and sends message to chat. /// Makes the selected entity emote using the given <see cref="EmotePrototype"/> and sends a message to chat.
/// </summary> /// </summary>
/// <param name="source">The entity that is speaking</param> /// <param name="source">The entity that is speaking.</param>
/// <param name="emote">The emote prototype. Should has valid <see cref="EmotePrototype.ChatMessages"/></param> /// <param name="emote">The emote prototype. Should have valid <see cref="EmotePrototype.ChatMessages"/>.</param>
/// <param name="hideLog">Whether or not this message should appear in the adminlog window</param> /// <param name="hideLog">Whether this message should appear in the adminlog window or not.</param>
/// <param name="hideChat">Whether or not this message should appear in the chat window</param> /// <param name="ignoreActionBlocker">Whether emote action blocking should be ignored or not.</param>
/// <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="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="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> /// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns> /// <returns>True if an emote was performed. False if the emote is unavailable, cancelled, etc.</returns>
public bool TryEmoteWithChat( public bool TryEmoteWithChat(
EntityUid source, EntityUid source,
EmotePrototype emote, EmotePrototype emote,
@@ -109,21 +104,21 @@ public partial class ChatSystem
} }
/// <summary> /// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat. /// Makes the selected entity emote using the given <see cref="EmotePrototype"/> without sending any messages to chat.
/// </summary> /// </summary>
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns> /// <returns>True if an emote was performed. False if the emote is unavailable, cancelled, etc.</returns>
public bool TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false) public bool TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
{ {
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto)) if (!_prototypeManager.Resolve<EmotePrototype>(emoteId, out var proto))
return false; return false;
return TryEmoteWithoutChat(uid, proto, ignoreActionBlocker); return TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
} }
/// <summary> /// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat. /// Makes the selected entity emote using the given <see cref="EmotePrototype"/> without sending any messages to chat.
/// </summary> /// </summary>
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns> /// <returns>True if an emote was performed. False if the emote is unavailable, cancelled, etc.</returns>
public bool TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false) public bool TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
{ {
if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker) if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker)
@@ -133,7 +128,7 @@ public partial class ChatSystem
} }
/// <summary> /// <summary>
/// Tries to find and play relevant emote sound in emote sounds collection. /// Tries to find and play the relevant emote sound in an emote sounds collection.
/// </summary> /// </summary>
/// <returns>True if emote sound was played.</returns> /// <returns>True if emote sound was played.</returns>
public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, EmotePrototype emote, AudioParams? audioParams = null) public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, EmotePrototype emote, AudioParams? audioParams = null)
@@ -142,7 +137,7 @@ public partial class ChatSystem
} }
/// <summary> /// <summary>
/// Tries to find and play relevant emote sound in emote sounds collection. /// Tries to find and play the relevant emote sound in an emote sounds collection.
/// </summary> /// </summary>
/// <returns>True if emote sound was played.</returns> /// <returns>True if emote sound was played.</returns>
public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, string emoteId, AudioParams? audioParams = null) public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, string emoteId, AudioParams? audioParams = null)
@@ -167,44 +162,27 @@ public partial class ChatSystem
/// <summary> /// <summary>
/// Checks if a valid emote was typed, to play sounds and etc and invokes an event. /// Checks if a valid emote was typed, to play sounds and etc and invokes an event.
/// </summary> /// </summary>
/// <param name="uid"></param> /// <param name="source">The entity that is speaking</param>
/// <param name="textInput"></param> /// <param name="textInput">Formatted emote message.</param>
/// <returns>True if the chat message should be displayed (because the emote was explicitly cancelled), false if it should not be.</returns> /// <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) protected bool TryEmoteChatInput(EntityUid source, string textInput)
{ {
var actionTrimmedLower = TrimPunctuation(textInput.ToLower()); var actionTrimmedLower = TrimPunctuation(textInput.ToLower());
if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote)) if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote))
return true; return true;
if (!AllowedToUseEmote(uid, emote)) if (!AllowedToUseEmote(source, emote))
return true; return true;
return TryInvokeEmoteEvent(uid, emote); return TryInvokeEmoteEvent(source, emote);
static string TrimPunctuation(string textInput)
{
var trimEnd = textInput.Length;
while (trimEnd > 0 && char.IsPunctuation(textInput[trimEnd - 1]))
{
trimEnd--;
}
var trimStart = 0;
while (trimStart < trimEnd && char.IsPunctuation(textInput[trimStart]))
{
trimStart++;
}
return textInput[trimStart..trimEnd];
}
} }
/// <summary> /// <summary>
/// Checks if we can use this emote based on the emotes whitelist, blacklist, and availibility to the entity. /// Checks if we can use this emote based on the emotes whitelist, blacklist, and availability to the entity.
/// </summary> /// </summary>
/// <param name="source">The entity that is speaking</param> /// <param name="source">The entity that is speaking</param>
/// <param name="emote">The emote being used</param> /// <param name="emote">The emote being used</param>
/// <returns></returns> public bool AllowedToUseEmote(EntityUid source, EmotePrototype emote)
private bool AllowedToUseEmote(EntityUid source, EmotePrototype emote)
{ {
// If emote is in AllowedEmotes, it will bypass whitelist and blacklist // If emote is in AllowedEmotes, it will bypass whitelist and blacklist
if (TryComp<SpeechComponent>(source, out var speech) && if (TryComp<SpeechComponent>(source, out var speech) &&
@@ -214,8 +192,8 @@ public partial class ChatSystem
} }
// Check the whitelist and blacklist // Check the whitelist and blacklist
if (_whitelistSystem.IsWhitelistFail(emote.Whitelist, source) || if (_whitelist.IsWhitelistFail(emote.Whitelist, source) ||
_whitelistSystem.IsBlacklistPass(emote.Blacklist, source)) _whitelist.IsBlacklistPass(emote.Blacklist, source))
{ {
return false; return false;
} }
@@ -244,9 +222,13 @@ public partial class ChatSystem
if (beforeEv.Cancelled) if (beforeEv.Cancelled)
{ {
// Chat is not predicted anyways, so no need to predict this popup either.
if (_net.IsClient)
return false;
if (beforeEv.Blocker != null) if (beforeEv.Blocker != null)
{ {
_popupSystem.PopupEntity( _popup.PopupEntity(
Loc.GetString( Loc.GetString(
"chat-system-emote-cancelled-blocked", "chat-system-emote-cancelled-blocked",
("emote", Loc.GetString(proto.Name).ToLower()), ("emote", Loc.GetString(proto.Name).ToLower()),
@@ -258,7 +240,7 @@ public partial class ChatSystem
} }
else else
{ {
_popupSystem.PopupEntity( _popup.PopupEntity(
Loc.GetString("chat-system-emote-cancelled-generic", Loc.GetString("chat-system-emote-cancelled-generic",
("emote", Loc.GetString(proto.Name).ToLower())), ("emote", Loc.GetString(proto.Name).ToLower())),
uid, uid,
@@ -274,20 +256,21 @@ public partial class ChatSystem
return true; return true;
} }
}
/// <summary> private string TrimPunctuation(string textInput)
/// Raised by chat system when entity made some emote.
/// Use it to play sound, change sprite or something else.
/// </summary>
[ByRefEvent]
public sealed class EmoteEvent : HandledEntityEventArgs
{
public readonly EmotePrototype Emote;
public EmoteEvent(EmotePrototype emote)
{ {
Emote = emote; var trimEnd = textInput.Length;
Handled = false; while (trimEnd > 0 && char.IsPunctuation(textInput[trimEnd - 1]))
{
trimEnd--;
}
var trimStart = 0;
while (trimStart < trimEnd && char.IsPunctuation(textInput[trimStart]))
{
trimStart++;
}
return textInput[trimStart..trimEnd];
} }
} }

View File

@@ -1,15 +1,21 @@
using System.Collections.Frozen; using System.Collections.Frozen;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Content.Shared.ActionBlocker;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Radio; using Content.Shared.Radio;
using Content.Shared.Speech; using Content.Shared.Speech;
using Content.Shared.Whitelist;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Shared.Chat; namespace Content.Shared.Chat;
public abstract class SharedChatSystem : EntitySystem public abstract partial class SharedChatSystem : EntitySystem
{ {
public const char RadioCommonPrefix = ';'; public const char RadioCommonPrefix = ';';
public const char RadioChannelPrefix = ':'; public const char RadioChannelPrefix = ':';
@@ -38,6 +44,11 @@ public abstract class SharedChatSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly INetManager _net = default!;
/// <summary> /// <summary>
/// Cache of the keycodes for faster lookup. /// Cache of the keycodes for faster lookup.
@@ -47,15 +58,21 @@ public abstract class SharedChatSystem : EntitySystem
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
DebugTools.Assert(_prototypeManager.HasIndex(CommonChannel)); DebugTools.Assert(_prototypeManager.HasIndex(CommonChannel));
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload); SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload);
CacheRadios(); CacheRadios();
CacheEmotes();
} }
protected virtual void OnPrototypeReload(PrototypesReloadedEventArgs obj) protected virtual void OnPrototypeReload(PrototypesReloadedEventArgs obj)
{ {
if (obj.WasModified<RadioChannelPrototype>()) if (obj.WasModified<RadioChannelPrototype>())
CacheRadios(); CacheRadios();
if (obj.WasModified<EmotePrototype>())
CacheEmotes();
} }
private void CacheRadios() private void CacheRadios()
@@ -293,4 +310,31 @@ public abstract class SharedChatSystem : EntitySystem
tagStart += tag.Length + 2; tagStart += tag.Length + 2;
return rawmsg.Substring(tagStart, tagEnd - tagStart); return rawmsg.Substring(tagStart, tagEnd - tagStart);
} }
protected virtual void SendEntityEmote(
EntityUid source,
string action,
ChatTransmitRange range,
string? nameOverride,
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,
NetUserId? author = null
)
{ }
}
/// <summary>
/// Controls transmission of chat.
/// </summary>
public enum ChatTransmitRange : byte
{
/// Acts normal, ghosts can hear across the map, etc.
Normal,
/// Normal but ghosts are still range-limited.
GhostRangeLimit,
/// Hidden from the chat window.
HideChat,
/// Ghosts can't hear or see it at all. Regular players can if in-range.
NoGhosts
} }

View File

@@ -1,27 +1,6 @@
using Content.Shared.Chat.Prototypes; namespace Content.Shared.Emoting;
using Content.Shared.Inventory;
namespace Content.Shared.Emoting;
public sealed class EmoteAttemptEvent(EntityUid uid) : CancellableEntityEventArgs public sealed class EmoteAttemptEvent(EntityUid uid) : CancellableEntityEventArgs
{ {
public EntityUid Uid { get; } = uid; 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;
}

View File

@@ -1,8 +1,26 @@
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.Effects; namespace Content.Shared.EntityEffects.Effects;
/// <summary>
/// Makes this entity emote.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class EmoteEntityEffectSystem : EntityEffectSystem<MetaDataComponent, Emote>
{
[Dependency] private readonly SharedChatSystem _chat = default!;
protected override void Effect(Entity<MetaDataComponent> entity, ref EntityEffectEvent<Emote> args)
{
if (args.Effect.ShowInChat)
_chat.TryEmoteWithChat(entity, args.Effect.EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: args.Effect.Force);
else
_chat.TryEmoteWithoutChat(entity, args.Effect.EmoteId);
}
}
/// <inheritdoc cref="EntityEffect"/> /// <inheritdoc cref="EntityEffect"/>
public sealed partial class Emote : EntityEffectBase<Emote> public sealed partial class Emote : EntityEffectBase<Emote>
{ {

View File

@@ -8,7 +8,6 @@ using Content.Shared.Contraband;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Events; using Content.Shared.Damage.Events;
using Content.Shared.Electrocution; using Content.Shared.Electrocution;
using Content.Shared.Emoting;
using Content.Shared.Explosion; using Content.Shared.Explosion;
using Content.Shared.Eye.Blinding.Systems; using Content.Shared.Eye.Blinding.Systems;
using Content.Shared.Flash; using Content.Shared.Flash;

View File

@@ -1,24 +1,25 @@
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.Speech.Components; namespace Content.Shared.Speech.Components;
/// <summary> /// <summary>
/// Suppresses emotes with the given categories or ID. /// Suppresses emotes with the given categories or ID.
/// Additionally, if the Scream Emote would be blocked, also blocks the Scream Action. /// Additionally, if the Scream Emote would be blocked, also blocks the Scream Action.
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class EmoteBlockerComponent : Component public sealed partial class EmoteBlockerComponent : Component
{ {
/// <summary> /// <summary>
/// Which categories of emotes are blocked by this component. /// Which categories of emotes are blocked by this component.
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public HashSet<EmoteCategory> BlocksCategories = []; public HashSet<EmoteCategory> BlocksCategories = [];
/// <summary> /// <summary>
/// IDs of which specific emotes are blocked by this component. /// IDs of which specific emotes are blocked by this component.
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public HashSet<ProtoId<EmotePrototype>> BlocksEmotes = []; public HashSet<ProtoId<EmotePrototype>> BlocksEmotes = [];
} }

View File

@@ -1,8 +1,8 @@
using Content.Server.Speech.Components; using Content.Shared.Chat;
using Content.Shared.Emoting;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.EntitySystems; namespace Content.Shared.Speech.EntitySystems;
public sealed class EmoteBlockerSystem : EntitySystem public sealed class EmoteBlockerSystem : EntitySystem
{ {