ReplacementAccentSystem.cs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. using System.Linq;
  2. using System.Text.RegularExpressions;
  3. using Content.Server.Speech.Components;
  4. using Content.Server.Speech.Prototypes;
  5. using JetBrains.Annotations;
  6. using Robust.Shared.Prototypes;
  7. using Robust.Shared.Random;
  8. namespace Content.Server.Speech.EntitySystems
  9. {
  10. // TODO: Code in-game languages and make this a language
  11. /// <summary>
  12. /// Replaces text in messages, either with full replacements or word replacements.
  13. /// </summary>
  14. public sealed class ReplacementAccentSystem : EntitySystem
  15. {
  16. [Dependency] private readonly IPrototypeManager _proto = default!;
  17. [Dependency] private readonly IRobustRandom _random = default!;
  18. [Dependency] private readonly ILocalizationManager _loc = default!;
  19. public override void Initialize()
  20. {
  21. SubscribeLocalEvent<ReplacementAccentComponent, AccentGetEvent>(OnAccent);
  22. }
  23. private void OnAccent(EntityUid uid, ReplacementAccentComponent component, AccentGetEvent args)
  24. {
  25. args.Message = ApplyReplacements(args.Message, component.Accent);
  26. }
  27. /// <summary>
  28. /// Attempts to apply a given replacement accent prototype to a message.
  29. /// </summary>
  30. [PublicAPI]
  31. public string ApplyReplacements(string message, string accent)
  32. {
  33. if (!_proto.TryIndex<ReplacementAccentPrototype>(accent, out var prototype))
  34. return message;
  35. if (!_random.Prob(prototype.ReplacementChance))
  36. return message;
  37. // Prioritize fully replacing if that exists--
  38. // ideally both aren't used at the same time (but we don't have a way to enforce that in serialization yet)
  39. if (prototype.FullReplacements != null)
  40. {
  41. return prototype.FullReplacements.Length != 0 ? Loc.GetString(_random.Pick(prototype.FullReplacements)) : "";
  42. }
  43. if (prototype.WordReplacements == null)
  44. return message;
  45. // Prohibition of repeated word replacements.
  46. // All replaced words placed in the final message are placed here as dashes (___) with the same length.
  47. // The regex search goes through this buffer message, from which the already replaced words are crossed out,
  48. // ensuring that the replaced words cannot be replaced again.
  49. var maskMessage = message;
  50. foreach (var (first, replace) in prototype.WordReplacements)
  51. {
  52. var f = _loc.GetString(first);
  53. var r = _loc.GetString(replace);
  54. // this is kind of slow but its not that bad
  55. // essentially: go over all matches, try to match capitalization where possible, then replace
  56. // rather than using regex.replace
  57. for (int i = Regex.Count(maskMessage, $@"(?<!\w){f}(?!\w)", RegexOptions.IgnoreCase); i > 0; i--)
  58. {
  59. // fetch the match again as the character indices may have changed
  60. Match match = Regex.Match(maskMessage, $@"(?<!\w){f}(?!\w)", RegexOptions.IgnoreCase);
  61. var replacement = r;
  62. // Intelligently replace capitalization
  63. // two cases where we will do so:
  64. // - the string is all upper case (just uppercase the replacement too)
  65. // - the first letter of the word is capitalized (common, just uppercase the first letter too)
  66. // any other cases are not really useful or not viable, since the match & replacement can be different
  67. // lengths
  68. // second expression here is weird--its specifically for single-word capitalization for I or A
  69. // dwarf expands I -> Ah, without that it would transform I -> AH
  70. // so that second case will only fully-uppercase if the replacement length is also 1
  71. if (!match.Value.Any(char.IsLower) && (match.Length > 1 || replacement.Length == 1))
  72. {
  73. replacement = replacement.ToUpperInvariant();
  74. }
  75. else if (match.Length >= 1 && replacement.Length >= 1 && char.IsUpper(match.Value[0]))
  76. {
  77. replacement = replacement[0].ToString().ToUpper() + replacement[1..];
  78. }
  79. // In-place replace the match with the transformed capitalization replacement
  80. message = message.Remove(match.Index, match.Length).Insert(match.Index, replacement);
  81. var mask = new string('_', replacement.Length);
  82. maskMessage = maskMessage.Remove(match.Index, match.Length).Insert(match.Index, mask);
  83. }
  84. }
  85. return message;
  86. }
  87. }
  88. }