2023-12-22 09:13:45 -05:00
using System.Collections.Frozen ;
2025-07-11 10:18:15 -07:00
using Content.Server.Popups ;
2023-01-25 17:29:41 +01:00
using Content.Shared.Chat.Prototypes ;
2025-07-11 10:18:15 -07:00
using Content.Shared.Emoting ;
2024-04-29 07:38:23 +03:00
using Content.Shared.Speech ;
2025-06-05 19:45:55 -04:00
using Robust.Shared.Audio ;
2023-01-25 17:29:41 +01:00
using Robust.Shared.Prototypes ;
using Robust.Shared.Random ;
namespace Content.Server.Chat.Systems ;
// emotes using emote prototype
public partial class ChatSystem
{
2025-07-11 10:18:15 -07:00
[Dependency] private readonly PopupSystem _popupSystem = default ! ;
2023-12-22 09:13:45 -05:00
private FrozenDictionary < string , EmotePrototype > _wordEmoteDict = FrozenDictionary < string , EmotePrototype > . Empty ;
2023-01-25 17:29:41 +01:00
2023-12-22 09:13:45 -05:00
protected override void OnPrototypeReload ( PrototypesReloadedEventArgs obj )
2023-01-25 17:29:41 +01:00
{
2023-12-22 09:13:45 -05:00
base . OnPrototypeReload ( obj ) ;
if ( obj . WasModified < EmotePrototype > ( ) )
CacheEmotes ( ) ;
2023-01-25 17:29:41 +01:00
}
private void CacheEmotes ( )
{
2023-12-22 09:13:45 -05:00
var dict = new Dictionary < string , EmotePrototype > ( ) ;
2023-01-25 17:29:41 +01:00
var emotes = _prototypeManager . EnumeratePrototypes < EmotePrototype > ( ) ;
foreach ( var emote in emotes )
{
foreach ( var word in emote . ChatTriggers )
{
var lowerWord = word . ToLower ( ) ;
2023-12-22 09:13:45 -05:00
if ( dict . TryGetValue ( lowerWord , out var value ) )
2023-01-25 17:29:41 +01:00
{
2023-12-22 09:13:45 -05:00
var errMsg = $"Duplicate of emote word {lowerWord} in emotes {emote.ID} and {value.ID}" ;
Log . Error ( errMsg ) ;
2023-01-25 17:29:41 +01:00
continue ;
}
2023-12-22 09:13:45 -05:00
dict . Add ( lowerWord , emote ) ;
2023-01-25 17:29:41 +01:00
}
}
2023-12-22 09:13:45 -05:00
_wordEmoteDict = dict . ToFrozenDictionary ( ) ;
2023-01-25 17:29:41 +01:00
}
/// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> and sends message to chat.
/// </summary>
/// <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>
Northstar Gloves (#16021)
* Added Gloves of North Star, no sprite or talking yet...
* Added sprites for the gloves of the north star...
* Replaced more placeholder sprites for northstar gloves...
* Added gloves of the north star to uplink...
* Added speech on hit, not yet configureable
* Not functional yet, but a step in the right direction I hope...
* IT WORKS!!
* Licensing and cleanup
* Reduced attack speed, changed from chat to popup, added some admin logging. It was causing too much adminlog spam otherwise
* Reorganized some files, final build??
* Changed the adminlog type from Verb to new type ItemConfigure
* More cleanup, fix sprite reference maybe
* Keronshb's suggestions, fixed some stuff, made hit sound use the meaty punch sfx
* Adds support for hiding speak/whisper/emote from adminlogs, makes northstar speak again!
* Some file shuffling, some of Keronshb's requests. Might appear a bit funky in github because vscode kept duplicating files for some reason and I had to delete them
* Made it work with the latest changes on Master
* Final? cleanup, upped dmg to 8, made ui not activate on activateinhand, instead you need to right click
* Set value to 0 credits, that's all
* Well that was much easier than I made it out to be. Now you can only activate the gloves with right click, no more mispredicts.
* Update MeleeWeaponSystem.cs
Iunno why this got changed in the first place, but I'm changin it back
* emptycommit
* emptycommit
* The tiny fixening
2023-05-23 11:12:30 -07:00
/// <param name="hideLog">Whether or not this message should appear in the adminlog window</param>
2023-05-04 20:08:08 +01:00
/// <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>
2023-01-25 17:29:41 +01:00
/// <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>
2024-06-03 18:45:00 -05:00
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
2025-07-11 10:18:15 -07:00
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
public bool TryEmoteWithChat (
2023-08-11 22:56:34 -07:00
EntityUid source ,
string emoteId ,
ChatTransmitRange range = ChatTransmitRange . Normal ,
bool hideLog = false ,
string? nameOverride = null ,
2024-06-03 18:45:00 -05:00
bool ignoreActionBlocker = false ,
bool forceEmote = false
2023-08-11 22:56:34 -07:00
)
2023-01-25 17:29:41 +01:00
{
if ( ! _prototypeManager . TryIndex < EmotePrototype > ( emoteId , out var proto ) )
2025-07-11 10:18:15 -07:00
return false ;
return TryEmoteWithChat ( source , proto , range , hideLog : hideLog , nameOverride , ignoreActionBlocker : ignoreActionBlocker , forceEmote : forceEmote ) ;
2023-01-25 17:29:41 +01:00
}
/// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> and sends message to chat.
/// </summary>
/// <param name="source">The entity that is speaking</param>
/// <param name="emote">The emote prototype. Should has valid <see cref="EmotePrototype.ChatMessages"/></param>
Northstar Gloves (#16021)
* Added Gloves of North Star, no sprite or talking yet...
* Added sprites for the gloves of the north star...
* Replaced more placeholder sprites for northstar gloves...
* Added gloves of the north star to uplink...
* Added speech on hit, not yet configureable
* Not functional yet, but a step in the right direction I hope...
* IT WORKS!!
* Licensing and cleanup
* Reduced attack speed, changed from chat to popup, added some admin logging. It was causing too much adminlog spam otherwise
* Reorganized some files, final build??
* Changed the adminlog type from Verb to new type ItemConfigure
* More cleanup, fix sprite reference maybe
* Keronshb's suggestions, fixed some stuff, made hit sound use the meaty punch sfx
* Adds support for hiding speak/whisper/emote from adminlogs, makes northstar speak again!
* Some file shuffling, some of Keronshb's requests. Might appear a bit funky in github because vscode kept duplicating files for some reason and I had to delete them
* Made it work with the latest changes on Master
* Final? cleanup, upped dmg to 8, made ui not activate on activateinhand, instead you need to right click
* Set value to 0 credits, that's all
* Well that was much easier than I made it out to be. Now you can only activate the gloves with right click, no more mispredicts.
* Update MeleeWeaponSystem.cs
Iunno why this got changed in the first place, but I'm changin it back
* emptycommit
* emptycommit
* The tiny fixening
2023-05-23 11:12:30 -07:00
/// <param name="hideLog">Whether or not this message should appear in the adminlog window</param>
/// <param name="hideChat">Whether or not this message should appear in the chat window</param>
2023-05-04 20:08:08 +01:00
/// <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>
2023-01-25 17:29:41 +01:00
/// <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>
2024-06-03 18:45:00 -05:00
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
2025-07-11 10:18:15 -07:00
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
public bool TryEmoteWithChat (
2023-08-11 22:56:34 -07:00
EntityUid source ,
EmotePrototype emote ,
ChatTransmitRange range = ChatTransmitRange . Normal ,
bool hideLog = false ,
string? nameOverride = null ,
2024-06-03 18:45:00 -05:00
bool ignoreActionBlocker = false ,
bool forceEmote = false
2025-07-11 10:18:15 -07:00
)
2023-01-25 17:29:41 +01:00
{
2024-06-03 18:45:00 -05:00
if ( ! forceEmote & & ! AllowedToUseEmote ( source , emote ) )
2025-07-11 10:18:15 -07:00
return false ;
var didEmote = TryEmoteWithoutChat ( source , emote , ignoreActionBlocker ) ;
2024-04-29 07:38:23 +03:00
2023-01-25 17:29:41 +01:00
// check if proto has valid message for chat
2025-07-11 10:18:15 -07:00
if ( didEmote & & emote . ChatMessages . Count ! = 0 )
2023-01-25 17:29:41 +01:00
{
2023-08-11 22:56:34 -07:00
// 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 ) ) ;
2023-08-18 23:59:23 +10:00
SendEntityEmote ( source , action , range , nameOverride , hideLog : hideLog , checkEmote : false , ignoreActionBlocker : ignoreActionBlocker ) ;
2023-01-25 17:29:41 +01:00
}
2025-07-11 10:18:15 -07:00
return didEmote ;
2023-01-25 17:29:41 +01:00
}
/// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
/// </summary>
2025-07-11 10:18:15 -07:00
/// <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 )
2023-01-25 17:29:41 +01:00
{
if ( ! _prototypeManager . TryIndex < EmotePrototype > ( emoteId , out var proto ) )
2025-07-11 10:18:15 -07:00
return false ;
2023-08-11 22:56:34 -07:00
2025-07-11 10:18:15 -07:00
return TryEmoteWithoutChat ( uid , proto , ignoreActionBlocker ) ;
2023-01-25 17:29:41 +01:00
}
/// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
/// </summary>
2025-07-11 10:18:15 -07:00
/// <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 )
2023-01-25 17:29:41 +01:00
{
2023-08-11 22:56:34 -07:00
if ( ! _actionBlocker . CanEmote ( uid ) & & ! ignoreActionBlocker )
2025-07-11 10:18:15 -07:00
return false ;
2023-01-25 17:29:41 +01:00
2025-07-11 10:18:15 -07:00
return TryInvokeEmoteEvent ( uid , proto ) ;
2023-01-25 17:29:41 +01:00
}
/// <summary>
/// Tries to find and play relevant emote sound in emote sounds collection.
/// </summary>
/// <returns>True if emote sound was played.</returns>
2025-06-05 19:45:55 -04:00
public bool TryPlayEmoteSound ( EntityUid uid , EmoteSoundsPrototype ? proto , EmotePrototype emote , AudioParams ? audioParams = null )
2023-01-25 17:29:41 +01:00
{
2025-06-05 19:45:55 -04:00
return TryPlayEmoteSound ( uid , proto , emote . ID , audioParams ) ;
2023-01-25 17:29:41 +01:00
}
/// <summary>
/// Tries to find and play relevant emote sound in emote sounds collection.
/// </summary>
/// <returns>True if emote sound was played.</returns>
2025-06-05 19:45:55 -04:00
public bool TryPlayEmoteSound ( EntityUid uid , EmoteSoundsPrototype ? proto , string emoteId , AudioParams ? audioParams = null )
2023-01-25 17:29:41 +01:00
{
if ( proto = = null )
return false ;
// try to get specific sound for this emote
if ( ! proto . Sounds . TryGetValue ( emoteId , out var sound ) )
{
// no specific sound - check fallback
sound = proto . FallbackSound ;
if ( sound = = null )
return false ;
}
2025-06-05 19:45:55 -04:00
// optional override params > general params for all sounds in set > individual sound params
var param = audioParams ? ? proto . GeneralParams ? ? sound . Params ;
2023-01-25 17:29:41 +01:00
_audio . PlayPvs ( sound , uid , param ) ;
return true ;
}
2024-06-03 18:45:00 -05:00
/// <summary>
/// Checks if a valid emote was typed, to play sounds and etc and invokes an event.
/// </summary>
/// <param name="uid"></param>
/// <param name="textInput"></param>
2025-07-11 10:18:15 -07:00
/// <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 )
2023-01-25 17:29:41 +01:00
{
2024-08-19 00:49:07 +02:00
var actionTrimmedLower = TrimPunctuation ( textInput . ToLower ( ) ) ;
if ( ! _wordEmoteDict . TryGetValue ( actionTrimmedLower , out var emote ) )
2025-07-11 10:18:15 -07:00
return true ;
2023-01-25 17:29:41 +01:00
2024-06-03 18:45:00 -05:00
if ( ! AllowedToUseEmote ( uid , emote ) )
2025-07-11 10:18:15 -07:00
return true ;
2024-06-03 18:45:00 -05:00
2025-07-11 10:18:15 -07:00
return TryInvokeEmoteEvent ( uid , emote ) ;
2024-08-19 00:49:07 +02:00
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 ] ;
}
2023-01-25 17:29:41 +01:00
}
2024-06-03 18:45:00 -05:00
/// <summary>
/// Checks if we can use this emote based on the emotes whitelist, blacklist, and availibility to the entity.
/// </summary>
/// <param name="source">The entity that is speaking</param>
/// <param name="emote">The emote being used</param>
/// <returns></returns>
private bool AllowedToUseEmote ( EntityUid source , EmotePrototype emote )
{
2025-02-17 22:59:33 +01:00
// If emote is in AllowedEmotes, it will bypass whitelist and blacklist
if ( TryComp < SpeechComponent > ( source , out var speech ) & &
speech . AllowedEmotes . Contains ( emote . ID ) )
{
return true ;
}
// Check the whitelist and blacklist
if ( _whitelistSystem . IsWhitelistFail ( emote . Whitelist , source ) | |
_whitelistSystem . IsBlacklistPass ( emote . Blacklist , source ) )
{
2024-06-03 18:45:00 -05:00
return false ;
2025-02-17 22:59:33 +01:00
}
2024-06-03 18:45:00 -05:00
2025-02-17 22:59:33 +01:00
// Check if the emote is available for all
if ( ! emote . Available )
{
2024-06-03 18:45:00 -05:00
return false ;
2025-02-17 22:59:33 +01:00
}
2024-06-03 18:45:00 -05:00
return true ;
}
2023-01-25 17:29:41 +01:00
2025-07-11 10:18:15 -07:00
/// <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 )
2023-01-25 17:29:41 +01:00
{
2025-07-11 10:18:15 -07:00
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 ;
}
2023-01-25 17:29:41 +01:00
var ev = new EmoteEvent ( proto ) ;
RaiseLocalEvent ( uid , ref ev ) ;
2025-07-11 10:18:15 -07:00
return true ;
2023-01-25 17:29:41 +01:00
}
}
/// <summary>
/// Raised by chat system when entity made some emote.
/// Use it to play sound, change sprite or something else.
/// </summary>
[ByRefEvent]
2025-07-11 10:18:15 -07:00
public sealed class EmoteEvent : HandledEntityEventArgs
2023-01-25 17:29:41 +01:00
{
public readonly EmotePrototype Emote ;
public EmoteEvent ( EmotePrototype emote )
{
Emote = emote ;
Handled = false ;
}
}