using System.Text; using Content.Shared.Language.Systems; namespace Content.Shared.Language; [ImplicitDataDefinitionForInheritors] public abstract partial class ObfuscationMethod { /// /// The fallback obfuscation method, replaces the message with the string "<?>". /// public static readonly ObfuscationMethod Default = new ReplacementObfuscation { Replacement = new List { "" } }; /// /// Obfuscates the provided message and writes the result into the provided StringBuilder. /// Implementations should use the context's pseudo-random number generator and provide stable obfuscations. /// internal abstract void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context); /// /// Obfuscates the provided message. This method should only be used for debugging purposes. /// For all other purposes, use instead. /// public string Obfuscate(string message) { var builder = new StringBuilder(); Obfuscate(builder, message, IoCManager.Resolve().GetEntitySystem()); return builder.ToString(); } } /// /// The most primitive method of obfuscation - replaces the entire message with one random replacement phrase. /// Similar to ReplacementAccent. Base for all replacement-based obfuscation methods. /// public partial class ReplacementObfuscation : ObfuscationMethod { /// /// A list of replacement phrases used in the obfuscation process. /// [DataField(required: true)] public List Replacement = []; internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) { var idx = context.PseudoRandomNumber(message.GetHashCode(), 0, Replacement.Count - 1); builder.Append(Replacement[idx]); } } /// /// Obfuscates the provided message by replacing each word with a random number of syllables in the range (min, max), /// preserving the original punctuation to a resonable extent. /// /// /// The words are obfuscated in a stable manner, such that every particular word will be obfuscated the same way throughout one round. /// This means that particular words can be memorized within a round, but not across rounds. /// public sealed partial class SyllableObfuscation : ReplacementObfuscation { [DataField] public int MinSyllables = 1; [DataField] public int MaxSyllables = 4; internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) { const char eof = (char) 0; // Special character to mark the end of the message in the code below var wordBeginIndex = 0; var hashCode = 0; for (var i = 0; i <= message.Length; i++) { var ch = i < message.Length ? char.ToLower(message[i]) : eof; var isWordEnd = char.IsWhiteSpace(ch) || IsPunctuation(ch) || ch == eof; // If this is a normal char, add it to the hash sum if (!isWordEnd) hashCode = hashCode * 31 + ch; // If a word ends before this character, construct a new word and append it to the new message. if (isWordEnd) { var wordLength = i - wordBeginIndex; if (wordLength > 0) { var newWordLength = context.PseudoRandomNumber(hashCode, MinSyllables, MaxSyllables); for (var j = 0; j < newWordLength; j++) { var index = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); builder.Append(Replacement[index]); } } hashCode = 0; wordBeginIndex = i + 1; } // If this message concludes a word (i.e. is a whitespace or a punctuation mark), append it to the message if (isWordEnd && ch != eof) builder.Append(ch); } } private static bool IsPunctuation(char ch) { return ch is '.' or '!' or '?' or ',' or ':'; } } /// /// Obfuscates each sentence in the message by concatenating a number of obfuscation phrases. /// The number of phrases in the obfuscated message is proportional to the length of the original message. /// public sealed partial class PhraseObfuscation : ReplacementObfuscation { [DataField] public int MinPhrases = 1; [DataField] public int MaxPhrases = 4; /// /// A string used to separate individual phrases within one sentence. Default is a space. /// [DataField] public string Separator = " "; /// /// A power to which the number of characters in the original message is raised to determine the number of phrases in the result. /// Default is 1/3, i.e. the cubic root of the original number. /// /// /// Using the default proportion, you will need at least 27 characters for 2 phrases, at least 64 for 3, at least 125 for 4, etc. /// Increasing the proportion to 1/4 will result in the numbers changing to 81, 256, 625, etc. /// [DataField] public float Proportion = 1f / 3; internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) { var sentenceBeginIndex = 0; var hashCode = 0; for (var i = 0; i < message.Length; i++) { var ch = char.ToLower(message[i]); if (!IsPunctuation(ch) && i != message.Length - 1) { hashCode = hashCode * 31 + ch; continue; } var length = i - sentenceBeginIndex; if (length > 0) { var newLength = (int) Math.Clamp(Math.Pow(length, Proportion) - 1, MinPhrases, MaxPhrases); for (var j = 0; j < newLength; j++) { var phraseIdx = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); var phrase = Replacement[phraseIdx]; builder.Append(phrase); builder.Append(Separator); } } sentenceBeginIndex = i + 1; if (IsPunctuation(ch)) builder.Append(ch).Append(' '); // TODO: this will turn '...' into '. . . ' } } private static bool IsPunctuation(char ch) { return ch is '.' or '!' or '?'; // Doesn't include mid-sentence punctuation like the comma } }