ChatSystem.Emote.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. using System.Collections.Frozen;
  2. using Content.Shared.Chat.Prototypes;
  3. using Content.Shared.Speech;
  4. using Robust.Shared.Prototypes;
  5. using Robust.Shared.Random;
  6. namespace Content.Server.Chat.Systems;
  7. // emotes using emote prototype
  8. public partial class ChatSystem
  9. {
  10. private FrozenDictionary<string, EmotePrototype> _wordEmoteDict = FrozenDictionary<string, EmotePrototype>.Empty;
  11. protected override void OnPrototypeReload(PrototypesReloadedEventArgs obj)
  12. {
  13. base.OnPrototypeReload(obj);
  14. if (obj.WasModified<EmotePrototype>())
  15. CacheEmotes();
  16. }
  17. private void CacheEmotes()
  18. {
  19. var dict = new Dictionary<string, EmotePrototype>();
  20. var emotes = _prototypeManager.EnumeratePrototypes<EmotePrototype>();
  21. foreach (var emote in emotes)
  22. {
  23. foreach (var word in emote.ChatTriggers)
  24. {
  25. var lowerWord = word.ToLower();
  26. if (dict.TryGetValue(lowerWord, out var value))
  27. {
  28. var errMsg = $"Duplicate of emote word {lowerWord} in emotes {emote.ID} and {value.ID}";
  29. Log.Error(errMsg);
  30. continue;
  31. }
  32. dict.Add(lowerWord, emote);
  33. }
  34. }
  35. _wordEmoteDict = dict.ToFrozenDictionary();
  36. }
  37. /// <summary>
  38. /// Makes selected entity to emote using <see cref="EmotePrototype"/> and sends message to chat.
  39. /// </summary>
  40. /// <param name="source">The entity that is speaking</param>
  41. /// <param name="emoteId">The id of emote prototype. Should has valid <see cref="EmotePrototype.ChatMessages"/></param>
  42. /// <param name="hideLog">Whether or not this message should appear in the adminlog window</param>
  43. /// <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>
  44. /// <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>
  45. /// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
  46. public void TryEmoteWithChat(
  47. EntityUid source,
  48. string emoteId,
  49. ChatTransmitRange range = ChatTransmitRange.Normal,
  50. bool hideLog = false,
  51. string? nameOverride = null,
  52. bool ignoreActionBlocker = false,
  53. bool forceEmote = false
  54. )
  55. {
  56. if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
  57. return;
  58. TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
  59. }
  60. /// <summary>
  61. /// Makes selected entity to emote using <see cref="EmotePrototype"/> and sends message to chat.
  62. /// </summary>
  63. /// <param name="source">The entity that is speaking</param>
  64. /// <param name="emote">The emote prototype. Should has valid <see cref="EmotePrototype.ChatMessages"/></param>
  65. /// <param name="hideLog">Whether or not this message should appear in the adminlog window</param>
  66. /// <param name="hideChat">Whether or not this message should appear in the chat window</param>
  67. /// <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>
  68. /// <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>
  69. /// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
  70. public void TryEmoteWithChat(
  71. EntityUid source,
  72. EmotePrototype emote,
  73. ChatTransmitRange range = ChatTransmitRange.Normal,
  74. bool hideLog = false,
  75. string? nameOverride = null,
  76. bool ignoreActionBlocker = false,
  77. bool forceEmote = false
  78. )
  79. {
  80. if (!forceEmote && !AllowedToUseEmote(source, emote))
  81. return;
  82. // check if proto has valid message for chat
  83. if (emote.ChatMessages.Count != 0)
  84. {
  85. // not all emotes are loc'd, but for the ones that are we pass in entity
  86. var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source));
  87. SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker);
  88. }
  89. // do the rest of emote event logic here
  90. TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
  91. }
  92. /// <summary>
  93. /// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
  94. /// </summary>
  95. public void TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
  96. {
  97. if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
  98. return;
  99. TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
  100. }
  101. /// <summary>
  102. /// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
  103. /// </summary>
  104. public void TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
  105. {
  106. if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker)
  107. return;
  108. InvokeEmoteEvent(uid, proto);
  109. }
  110. /// <summary>
  111. /// Tries to find and play relevant emote sound in emote sounds collection.
  112. /// </summary>
  113. /// <returns>True if emote sound was played.</returns>
  114. public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, EmotePrototype emote)
  115. {
  116. return TryPlayEmoteSound(uid, proto, emote.ID);
  117. }
  118. /// <summary>
  119. /// Tries to find and play relevant emote sound in emote sounds collection.
  120. /// </summary>
  121. /// <returns>True if emote sound was played.</returns>
  122. public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, string emoteId)
  123. {
  124. if (proto == null)
  125. return false;
  126. // try to get specific sound for this emote
  127. if (!proto.Sounds.TryGetValue(emoteId, out var sound))
  128. {
  129. // no specific sound - check fallback
  130. sound = proto.FallbackSound;
  131. if (sound == null)
  132. return false;
  133. }
  134. // if general params for all sounds set - use them
  135. var param = proto.GeneralParams ?? sound.Params;
  136. _audio.PlayPvs(sound, uid, param);
  137. return true;
  138. }
  139. /// <summary>
  140. /// Checks if a valid emote was typed, to play sounds and etc and invokes an event.
  141. /// </summary>
  142. /// <param name="uid"></param>
  143. /// <param name="textInput"></param>
  144. private void TryEmoteChatInput(EntityUid uid, string textInput)
  145. {
  146. var actionTrimmedLower = TrimPunctuation(textInput.ToLower());
  147. if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote))
  148. return;
  149. if (!AllowedToUseEmote(uid, emote))
  150. return;
  151. InvokeEmoteEvent(uid, emote);
  152. return;
  153. static string TrimPunctuation(string textInput)
  154. {
  155. var trimEnd = textInput.Length;
  156. while (trimEnd > 0 && char.IsPunctuation(textInput[trimEnd - 1]))
  157. {
  158. trimEnd--;
  159. }
  160. var trimStart = 0;
  161. while (trimStart < trimEnd && char.IsPunctuation(textInput[trimStart]))
  162. {
  163. trimStart++;
  164. }
  165. return textInput[trimStart..trimEnd];
  166. }
  167. }
  168. /// <summary>
  169. /// Checks if we can use this emote based on the emotes whitelist, blacklist, and availibility to the entity.
  170. /// </summary>
  171. /// <param name="source">The entity that is speaking</param>
  172. /// <param name="emote">The emote being used</param>
  173. /// <returns></returns>
  174. private bool AllowedToUseEmote(EntityUid source, EmotePrototype emote)
  175. {
  176. // If emote is in AllowedEmotes, it will bypass whitelist and blacklist
  177. if (TryComp<SpeechComponent>(source, out var speech) &&
  178. speech.AllowedEmotes.Contains(emote.ID))
  179. {
  180. return true;
  181. }
  182. // Check the whitelist and blacklist
  183. if (_whitelistSystem.IsWhitelistFail(emote.Whitelist, source) ||
  184. _whitelistSystem.IsBlacklistPass(emote.Blacklist, source))
  185. {
  186. return false;
  187. }
  188. // Check if the emote is available for all
  189. if (!emote.Available)
  190. {
  191. return false;
  192. }
  193. return true;
  194. }
  195. private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
  196. {
  197. var ev = new EmoteEvent(proto);
  198. RaiseLocalEvent(uid, ref ev);
  199. }
  200. }
  201. /// <summary>
  202. /// Raised by chat system when entity made some emote.
  203. /// Use it to play sound, change sprite or something else.
  204. /// </summary>
  205. [ByRefEvent]
  206. public struct EmoteEvent
  207. {
  208. public bool Handled;
  209. public readonly EmotePrototype Emote;
  210. public EmoteEvent(EmotePrototype emote)
  211. {
  212. Emote = emote;
  213. Handled = false;
  214. }
  215. }