From 45c1bf0efd8e418b5d467fb3409b98edf4f27bdd Mon Sep 17 00:00:00 2001 From: fox Date: Fri, 14 Jun 2024 02:59:25 +0300 Subject: [PATCH 01/17] Discover polymorphism --- Content.Server/Language/LanguageSystem.cs | 104 +---------- Content.Shared/Language/LanguagePrototype.cs | 10 +- Content.Shared/Language/ObfuscationMethods.cs | 168 ++++++++++++++++++ .../Language/Systems/SharedLanguageSystem.cs | 36 +++- 4 files changed, 208 insertions(+), 110 deletions(-) create mode 100644 Content.Shared/Language/ObfuscationMethods.cs diff --git a/Content.Server/Language/LanguageSystem.cs b/Content.Server/Language/LanguageSystem.cs index f1bf44c1f4f..065d696aa26 100644 --- a/Content.Server/Language/LanguageSystem.cs +++ b/Content.Server/Language/LanguageSystem.cs @@ -17,10 +17,6 @@ private readonly DetermineEntityLanguagesEvent _determineLanguagesEvent = new(string.Empty, new(), new()), _universalLanguagesEvent = new(UniversalPrototype, [UniversalPrototype], [UniversalPrototype]); // Returned for universal speakers only - /// - /// A random number added to each pseudo-random number's seed. Changes every round. - /// - public int RandomRoundSeed { get; private set; } public override void Initialize() @@ -29,7 +25,6 @@ public override void Initialize() SubscribeNetworkEvent(OnClientSetLanguage); SubscribeLocalEvent(OnInitLanguageSpeaker); - SubscribeLocalEvent(_ => RandomRoundSeed = _random.Next()); InitializeNet(); } @@ -41,24 +36,10 @@ public override void Initialize() /// public string ObfuscateSpeech(EntityUid source, string message) { - var language = GetLanguage(source) ?? Universal; + var language = GetLanguage(source); return ObfuscateSpeech(message, language); } - /// - /// Obfuscate a message using the given language. - /// - public string ObfuscateSpeech(string message, LanguagePrototype language) - { - var builder = new StringBuilder(); - if (language.ObfuscateSyllables) - ObfuscateSyllables(builder, message, language); - else - ObfuscatePhrases(builder, message, language); - - return builder.ToString(); - } - public bool CanUnderstand(EntityUid listener, LanguagePrototype language, LanguageSpeakerComponent? listenerLanguageComp = null) { if (language.ID == UniversalPrototype || HasComp(listener)) @@ -156,76 +137,6 @@ private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent compo } #endregion - #region internal api - obfuscation - private void ObfuscateSyllables(StringBuilder builder, string message, LanguagePrototype language) - { - // Go through each word. Calculate its hash sum and count the number of letters. - // Replicate it with pseudo-random syllables of pseudo-random (but similar) length. Use the hash code as the seed. - // This means that identical words will be obfuscated identically. Simple words like "hello" or "yes" in different langs can be memorized. - var wordBeginIndex = 0; - var hashCode = 0; - for (var i = 0; i < message.Length; i++) - { - var ch = char.ToLower(message[i]); - // A word ends when one of the following is found: a space, a sentence end, or EOM - if (char.IsWhiteSpace(ch) || IsSentenceEnd(ch) || i == message.Length - 1) - { - var wordLength = i - wordBeginIndex; - if (wordLength > 0) - { - var newWordLength = PseudoRandomNumber(hashCode, 1, 4); - - for (var j = 0; j < newWordLength; j++) - { - var index = PseudoRandomNumber(hashCode + j, 0, language.Replacement.Count); - builder.Append(language.Replacement[index]); - } - } - - builder.Append(ch); - hashCode = 0; - wordBeginIndex = i + 1; - } - else - hashCode = hashCode * 31 + ch; - } - } - - private void ObfuscatePhrases(StringBuilder builder, string message, LanguagePrototype language) - { - // In a similar manner, each phrase is obfuscated with a random number of conjoined obfuscation phrases. - // However, the number of phrases depends on the number of characters in the original phrase. - var sentenceBeginIndex = 0; - for (var i = 0; i < message.Length; i++) - { - var ch = char.ToLower(message[i]); - if (IsSentenceEnd(ch) || i == message.Length - 1) - { - var length = i - sentenceBeginIndex; - if (length > 0) - { - var newLength = (int) Math.Clamp(Math.Cbrt(length) - 1, 1, 4); // 27+ chars for 2 phrases, 64+ for 3, 125+ for 4. - - for (var j = 0; j < newLength; j++) - { - var phrase = _random.Pick(language.Replacement); - builder.Append(phrase); - } - } - sentenceBeginIndex = i + 1; - - if (IsSentenceEnd(ch)) - builder.Append(ch).Append(" "); - } - } - } - - private static bool IsSentenceEnd(char ch) - { - return ch is '.' or '!' or '?'; - } - #endregion - #region internal api - misc /// /// Dynamically resolves the current language of the entity and the list of all languages it speaks. @@ -257,19 +168,6 @@ private DetermineEntityLanguagesEvent GetLanguages(EntityUid speaker, LanguageSp return ev; } - /// - /// Generates a stable pseudo-random number in the range (min, max) for the given seed. - /// Each input seed corresponds to exactly one random number. - /// - private int PseudoRandomNumber(int seed, int min, int max) - { - // This is not a uniform distribution, but it shouldn't matter given there's 2^31 possible random numbers, - // the bias of this function should be so tiny it will never be noticed. - seed += RandomRoundSeed; - var random = ((seed * 1103515245) + 12345) & 0x7fffffff; // Source: http://cs.uccs.edu/~cs591/bufferOverflow/glibc-2.2.4/stdlib/random_r.c - return random % (max - min) + min; - } - /// /// Set CurrentLanguage of the client, the client must be able to Understand the language requested. /// diff --git a/Content.Shared/Language/LanguagePrototype.cs b/Content.Shared/Language/LanguagePrototype.cs index 801ab8a393b..d5a013161d0 100644 --- a/Content.Shared/Language/LanguagePrototype.cs +++ b/Content.Shared/Language/LanguagePrototype.cs @@ -10,15 +10,13 @@ public sealed class LanguagePrototype : IPrototype public string ID { get; private set; } = default!; /// - /// If true, obfuscated phrases of creatures speaking this language will have their syllables replaced with "replacement" syllables. - /// Otherwise entire sentences will be replaced. + /// Obfuscation method used by this language. By default, uses /// - [DataField(required: true)] - public bool ObfuscateSyllables; + [DataField("obfuscation")] + public ObfuscationMethod Obfuscation = ObfuscationMethod.Default; /// - /// Lists all syllables that are used to obfuscate a message a listener cannot understand if obfuscateSyllables is true. - /// Otherwise uses all possible phrases the creature can make when trying to say anything. + /// A list of replacement phrases used in /// [DataField(required: true)] public List Replacement = []; diff --git a/Content.Shared/Language/ObfuscationMethods.cs b/Content.Shared/Language/ObfuscationMethods.cs new file mode 100644 index 00000000000..5925279bd78 --- /dev/null +++ b/Content.Shared/Language/ObfuscationMethods.cs @@ -0,0 +1,168 @@ +using System.Text; +using Content.Shared.Language.Systems; + +namespace Content.Shared.Language; + +[ImplicitDataDefinitionForInheritors] +public abstract partial class ObfuscationMethod +{ + public static readonly ObfuscationMethod Default = new ReplacementObfuscation(); + + internal abstract void Obfuscate(StringBuilder builder, string message, LanguagePrototype language, 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, LanguagePrototype language) + { + var builder = new StringBuilder(); + Obfuscate(builder, message, language, IoCManager.Resolve().GetEntitySystem()); + return builder.ToString(); + } +} + +/// +/// The most primitive method of obfuscation - replaces the entire message with one random replacement phrase. +/// Similar to ReplacementAccent. +/// +public sealed partial class ReplacementObfuscation : ObfuscationMethod +{ + internal override void Obfuscate(StringBuilder builder, string message, LanguagePrototype language, SharedLanguageSystem context) + { + var idx = context.PseudoRandomNumber(0, 0, language.Replacement.Count - 1); + builder.Append(language.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 : ObfuscationMethod +{ + [DataField] + public int MinSyllables = 1; + + [DataField] + public int MaxSyllables = 4; + + internal override void Obfuscate(StringBuilder builder, string message, LanguagePrototype language, 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, language.Replacement.Count - 1); + builder.Append(language.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 : ObfuscationMethod +{ + [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, LanguagePrototype language, 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, language.Replacement.Count - 1); + var phrase = language.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 + } +} diff --git a/Content.Shared/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/Language/Systems/SharedLanguageSystem.cs index e2eeb8bb493..e030b7eec6f 100644 --- a/Content.Shared/Language/Systems/SharedLanguageSystem.cs +++ b/Content.Shared/Language/Systems/SharedLanguageSystem.cs @@ -1,4 +1,6 @@ +using System.Text; using Content.Shared.Actions; +using Content.Shared.GameTicking; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -24,7 +26,7 @@ public abstract class SharedLanguageSystem : EntitySystem public static LanguagePrototype Universal { get; private set; } = default!; [Dependency] protected readonly IPrototypeManager _prototype = default!; - [Dependency] protected readonly IRobustRandom _random = default!; + [Dependency] protected readonly SharedGameTicker _ticker = default!; public override void Initialize() { @@ -36,4 +38,36 @@ public override void Initialize() _prototype.TryIndex(id, out var proto); return proto; } + + /// + /// Obfuscate a message using the given language. + /// + public string ObfuscateSpeech(string message, LanguagePrototype language) + { + var builder = new StringBuilder(); + var method = language.Obfuscation; + method.Obfuscate(builder, message, language, this); + + { + Log.Info($"RECEIVED {message} -> {builder.ToString()}"); + } + + return builder.ToString(); + } + + /// + /// Generates a stable pseudo-random number in the range (min, max) (inclusively) for the given seed. + /// One seed always corresponds to one number, however the resulting number also depends on the current round number. + /// This method is meant to be used in to provide stable obfuscation. + /// + internal int PseudoRandomNumber(int seed, int min, int max) + { + // Using RobustRandom or System.Random here is a bad idea because this method can get called hundreds of times per message. + // Each call would require us to allocate a new instance of random, which would lead to lots of unnecessary calculations. + // Instead, we use a simple but effective algorithm derived from the C language. + // It does not produce a truly random number, but for the purpose of obfuscating messages in an RP-based game it's more than alright. + seed = seed ^ (_ticker.RoundId * 127); + var random = seed * 1103515245 + 12345; + return min + Math.Abs(random) % (max - min + 1); + } } From 3df0c63f671bd26d11a2375e952d482cd852a221 Mon Sep 17 00:00:00 2001 From: fox Date: Fri, 14 Jun 2024 02:59:45 +0300 Subject: [PATCH 02/17] Apply polymorphism + fix some obfuscation phrases --- Resources/Prototypes/Language/languages.yml | 231 +++++++++++++------- 1 file changed, 148 insertions(+), 83 deletions(-) diff --git a/Resources/Prototypes/Language/languages.yml b/Resources/Prototypes/Language/languages.yml index 90bce1baed2..7b81f9ad050 100644 --- a/Resources/Prototypes/Language/languages.yml +++ b/Resources/Prototypes/Language/languages.yml @@ -2,18 +2,22 @@ # Do not use otherwise. Try to use the respective component instead of this language. - type: language id: Universal - obfuscateSyllables: false + obfuscation: + !type:ReplacementObfuscation # Should never be used anyway replacement: - "*incomprehensible*" # The common galactic tongue. - type: language id: GalacticCommon - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 replacement: - - Blah - - Blah - - Blah + - blah + - blah + - blah - dingle-doingle - dingle - dangle @@ -34,7 +38,10 @@ # Spoken by slimes. - type: language id: Bubblish - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 replacement: - blob - plop @@ -45,7 +52,10 @@ # Spoken by moths. - type: language id: Moffic - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 # Replacements are really short + maxSyllables: 4 replacement: - år - i @@ -105,10 +115,13 @@ - we - hön - # Spoken by dionas. +# Spoken by dionas. - type: language id: RootSpeak - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 5 replacement: - hs - zt @@ -119,7 +132,10 @@ # A mess of broken Japanese, spoken by Felinds and Oni - type: language id: Nekomimetic - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 # May be too long even, we'll see. replacement: - neko - nyan @@ -173,7 +189,10 @@ # Spoken by the Lizard race. - type: language id: Draconic - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 replacement: - za - az @@ -263,7 +282,10 @@ # Spoken by the Vulpkanin race. - type: language id: Canilunzt - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 replacement: - rur - ya @@ -292,7 +314,7 @@ - vor - nic - gro - - lll +# - lll - enem - zandt - tzch @@ -327,7 +349,10 @@ # The common language of the Sol system. - type: language id: SolCommon - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 replacement: - tao - shi @@ -349,23 +374,21 @@ - type: language id: RobotTalk - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 10 # Crazy replacement: - 0 - 1 - - 01 - - 10 - - 001 - - 100 - - 011 - - 110 - - 101 - - 010 # Languages spoken by various critters. - type: language id: Cat - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 replacement: - murr - meow @@ -374,7 +397,10 @@ - type: language id: Dog - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 replacement: - woof - bark @@ -385,109 +411,148 @@ - type: language id: Fox - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 replacement: - - bark - - gecker - ruff - raff - garr + - yip + - yap + - myah - type: language id: Xeno - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # I was crazy once replacement: - - sss - - sSs - - SSS + - s + - S - type: language id: Monkey - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # They locked me in a room... replacement: - - ok - - ook - - oook - - ooook - - oooook + - o + - k - type: language id: Mouse - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 3 replacement: - - Squeak - - Piep - - Chuu - - Eeee - - Pip - - Fwiep - - Heep + - squ + - eak + - pi + - ep + - chuu + - ee + - fwi + - he - type: language id: Chicken - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 replacement: - - Coo - - Coot - - Cooot + - co + - coo + - ot - type: language id: Duck - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 replacement: - - Quack - - Quack quack + - qu + - ack + - quack - type: language id: Cow - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 replacement: - - Moo - - Mooo + - moo + - mooo - type: language id: Sheep - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 replacement: - - Ba - - Baa - - Baaa + - ba + - baa + - aa - type: language id: Kangaroo - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 replacement: - - Shreak - - Chuu + - shre + - ack + - chuu + - choo - type: language id: Pig - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 replacement: - - Oink - - Oink oink + - oink # Please someone come up with something better - type: language id: Crab - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 replacement: - - Click - - Click-clack - - Clack - - Tipi-tap - - Clik-tap - - Cliliick + - click + - clack + - ti + - pi + - tap + - cli + - ick - type: language id: Kobold - obfuscateSyllables: true + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 replacement: - - Yip - - Grrar. - - Yap - - Bip - - Screet - - Gronk - - Hiss - - Eeee - - Yip + - yip + - yap + - gar + - grr + - ar + - scre + - et + - gronk + - hiss + - ss + - ee From f0af64c25041bf3b51a67a0ebfa780de3a74b14c Mon Sep 17 00:00:00 2001 From: fox Date: Fri, 14 Jun 2024 03:06:55 +0300 Subject: [PATCH 03/17] Oops --- Content.Shared/Language/Systems/SharedLanguageSystem.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Content.Shared/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/Language/Systems/SharedLanguageSystem.cs index e030b7eec6f..2ec4dde1ce8 100644 --- a/Content.Shared/Language/Systems/SharedLanguageSystem.cs +++ b/Content.Shared/Language/Systems/SharedLanguageSystem.cs @@ -48,10 +48,6 @@ public string ObfuscateSpeech(string message, LanguagePrototype language) var method = language.Obfuscation; method.Obfuscate(builder, message, language, this); - { - Log.Info($"RECEIVED {message} -> {builder.ToString()}"); - } - return builder.ToString(); } From 86eaa73768fbc45bdc6bf319de010410f232a6ec Mon Sep 17 00:00:00 2001 From: fox Date: Fri, 14 Jun 2024 03:59:46 +0300 Subject: [PATCH 04/17] Cleaned up imports --- Content.Client/Language/LanguageMenuWindow.xaml.cs | 12 ++++-------- Content.Client/Language/Systems/LanguageSystem.cs | 1 - Content.Server/Language/LanguageSystem.cs | 3 --- Content.Shared/Language/LanguagePrototype.cs | 1 - .../Language/Systems/SharedLanguageSystem.cs | 2 -- 5 files changed, 4 insertions(+), 15 deletions(-) diff --git a/Content.Client/Language/LanguageMenuWindow.xaml.cs b/Content.Client/Language/LanguageMenuWindow.xaml.cs index 312814aca35..0f8bb0976fd 100644 --- a/Content.Client/Language/LanguageMenuWindow.xaml.cs +++ b/Content.Client/Language/LanguageMenuWindow.xaml.cs @@ -1,14 +1,8 @@ using Content.Client.Language.Systems; -using Content.Shared.Language; -using Content.Shared.Language.Systems; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; -using Robust.Shared.Console; -using Robust.Shared.Utility; -using Serilog; -using static Content.Shared.Language.Systems.SharedLanguageSystem; namespace Content.Client.Language; @@ -121,8 +115,10 @@ private void AddLanguageEntry(string language) private void OnLanguageChosen(string id) { var proto = _clientLanguageSystem.GetLanguagePrototype(id); - if (proto != null) - _clientLanguageSystem.RequestSetLanguage(proto); + if (proto == null) + return; + + _clientLanguageSystem.RequestSetLanguage(proto); } diff --git a/Content.Client/Language/Systems/LanguageSystem.cs b/Content.Client/Language/Systems/LanguageSystem.cs index 9714078b2c5..5dc2fc1f4e7 100644 --- a/Content.Client/Language/Systems/LanguageSystem.cs +++ b/Content.Client/Language/Systems/LanguageSystem.cs @@ -2,7 +2,6 @@ using Content.Shared.Language.Events; using Content.Shared.Language.Systems; using Robust.Client; -using Robust.Shared.Console; namespace Content.Client.Language.Systems; diff --git a/Content.Server/Language/LanguageSystem.cs b/Content.Server/Language/LanguageSystem.cs index 065d696aa26..2b5bf454ce0 100644 --- a/Content.Server/Language/LanguageSystem.cs +++ b/Content.Server/Language/LanguageSystem.cs @@ -1,10 +1,7 @@ using System.Linq; -using System.Text; -using Content.Server.GameTicking.Events; using Content.Shared.Language; using Content.Shared.Language.Events; using Content.Shared.Language.Systems; -using Robust.Shared.Random; using UniversalLanguageSpeakerComponent = Content.Shared.Language.Components.UniversalLanguageSpeakerComponent; namespace Content.Server.Language; diff --git a/Content.Shared/Language/LanguagePrototype.cs b/Content.Shared/Language/LanguagePrototype.cs index d5a013161d0..15557739f7d 100644 --- a/Content.Shared/Language/LanguagePrototype.cs +++ b/Content.Shared/Language/LanguagePrototype.cs @@ -1,4 +1,3 @@ -using System.Runtime.CompilerServices; using Robust.Shared.Prototypes; namespace Content.Shared.Language; diff --git a/Content.Shared/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/Language/Systems/SharedLanguageSystem.cs index 2ec4dde1ce8..b9ea6869ee0 100644 --- a/Content.Shared/Language/Systems/SharedLanguageSystem.cs +++ b/Content.Shared/Language/Systems/SharedLanguageSystem.cs @@ -1,8 +1,6 @@ using System.Text; -using Content.Shared.Actions; using Content.Shared.GameTicking; using Robust.Shared.Prototypes; -using Robust.Shared.Random; namespace Content.Shared.Language.Systems; From 6770f30cf2764938071cd607ce5a1b624884066c Mon Sep 17 00:00:00 2001 From: fox Date: Fri, 14 Jun 2024 03:59:59 +0300 Subject: [PATCH 05/17] Refactored and localized commands; made them more user-friendly. --- .../Language/Commands/ListLanguagesCommand.cs | 24 +++++++-- .../Language/Commands/SayLanguageCommand.cs | 6 +-- .../Commands/SelectLanguageCommand.cs | 52 ++++++++++++++++--- Resources/Locale/en-US/language/commands.ftl | 16 ++++-- 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/Content.Server/Language/Commands/ListLanguagesCommand.cs b/Content.Server/Language/Commands/ListLanguagesCommand.cs index 6698e1b6453..7cf96d74e61 100644 --- a/Content.Server/Language/Commands/ListLanguagesCommand.cs +++ b/Content.Server/Language/Commands/ListLanguagesCommand.cs @@ -1,4 +1,3 @@ -using System.Linq; using Content.Shared.Administration; using Robust.Shared.Console; using Robust.Shared.Enums; @@ -32,8 +31,27 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) var languages = IoCManager.Resolve().GetEntitySystem(); var (spokenLangs, knownLangs) = languages.GetAllLanguages(playerEntity); + var currentLang = languages.GetLanguage(playerEntity).ID; - shell.WriteLine("Spoken:\n" + string.Join("\n", spokenLangs)); - shell.WriteLine("Understood:\n" + string.Join("\n", knownLangs)); + shell.WriteLine(Loc.GetString("command-language-spoken")); + for (int i = 0; i < spokenLangs.Count; i++) + { + var lang = spokenLangs[i]; + shell.WriteLine(lang == currentLang + ? Loc.GetString("command-language-current-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang))) + : Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang)))); + } + + shell.WriteLine(Loc.GetString("command-language-understood")); + for (int i = 0; i < knownLangs.Count; i++) + { + var lang = knownLangs[i]; + shell.WriteLine(Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang)))); + } + } + + private string LanguageName(string id) + { + return Loc.GetString($"language-{id}-name"); } } diff --git a/Content.Server/Language/Commands/SayLanguageCommand.cs b/Content.Server/Language/Commands/SayLanguageCommand.cs index 2e4a27b1dcc..2304781fa04 100644 --- a/Content.Server/Language/Commands/SayLanguageCommand.cs +++ b/Content.Server/Language/Commands/SayLanguageCommand.cs @@ -32,7 +32,6 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) if (args.Length < 2) return; - var languageId = args[0]; var message = string.Join(" ", args, startIndex: 1, count: args.Length - 1).Trim(); if (string.IsNullOrEmpty(message)) @@ -41,10 +40,9 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) var languages = IoCManager.Resolve().GetEntitySystem(); var chats = IoCManager.Resolve().GetEntitySystem(); - var language = languages.GetLanguagePrototype(languageId); - if (language == null || !languages.CanSpeak(playerEntity, language.ID)) + if (!SelectLanguageCommand.TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var language)) { - shell.WriteError($"Language {languageId} is invalid or you cannot speak it!"); + shell.WriteError(failReason); return; } diff --git a/Content.Server/Language/Commands/SelectLanguageCommand.cs b/Content.Server/Language/Commands/SelectLanguageCommand.cs index e3363846539..8a6ca3174a9 100644 --- a/Content.Server/Language/Commands/SelectLanguageCommand.cs +++ b/Content.Server/Language/Commands/SelectLanguageCommand.cs @@ -1,5 +1,7 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Administration; +using Content.Shared.Language; using Robust.Shared.Console; using Robust.Shared.Enums; @@ -32,17 +34,55 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) if (args.Length < 1) return; - var languageId = args[0]; - var languages = IoCManager.Resolve().GetEntitySystem(); - var language = languages.GetLanguagePrototype(languageId); - if (language == null || !languages.CanSpeak(playerEntity, language.ID)) + if (!TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var languageId)) { - shell.WriteError($"Language {languageId} is invalid or you cannot speak it!"); + shell.WriteError(failReason); return; } - languages.SetLanguage(playerEntity, language.ID); + languages.SetLanguage(playerEntity, languageId.ID); + } + + // TODO: find a better place for this method + /// + /// Tries to parse the input argument as either a language ID or the position of the language in the list of languages + /// the entity can speak. Returns true if sucessful. + /// + public static bool TryParseLanguageArgument( + LanguageSystem languageSystem, + EntityUid speaker, + string input, + [NotNullWhen(false)] out string? failureReason, + [NotNullWhen(true)] out LanguagePrototype? language) + { + failureReason = null; + language = null; + + if (int.TryParse(input, out var num)) + { + // The argument is a number + var (spoken, _) = languageSystem.GetAllLanguages(speaker); + if (num > 0 && num - 1 < spoken.Count) + language = languageSystem.GetLanguagePrototype(spoken[num - 1]); + + if (language != null) // the ability to speak it is implied + return true; + + failureReason = Loc.GetString("command-language-invalid-number", ("total", spoken.Count)); + return false; + } + else + { + // The argument is a language ID + language = languageSystem.GetLanguagePrototype(input); + + if (language != null && languageSystem.CanSpeak(speaker, language.ID)) + return true; + + failureReason = Loc.GetString("command-language-invalid-language", ("id", input)); + return false; + } } } diff --git a/Resources/Locale/en-US/language/commands.ftl b/Resources/Locale/en-US/language/commands.ftl index 32fa5415b8c..ba2b3160094 100644 --- a/Resources/Locale/en-US/language/commands.ftl +++ b/Resources/Locale/en-US/language/commands.ftl @@ -1,8 +1,16 @@ command-list-langs-desc = List languages your current entity can speak at the current moment. command-list-langs-help = Usage: {$command} -command-saylang-desc = Send a message in a specific language. -command-saylang-help = Usage: {$command} . Example: {$command} GalacticCommon "Hello World!" +command-saylang-desc = Send a message in a specific language. To choose a language, you can use either the name of the language, or its position in the list of languages. +command-saylang-help = Usage: {$command} . Example: {$command} GalacticCommon "Hello World!". Example: {$command} 1 "Hello World!" -command-language-select-desc = Select the currently spoken language of your entity. -command-language-select-help = Usage: {$command} . Example: {$command} GalacticCommon +command-language-select-desc = Select the currently spoken language of your entity. You can use either the name of the language, or its position in the list of languages. +command-language-select-help = Usage: {$command} . Example: {$command} 1. Example: {$command} GalacticCommon + +command-language-spoken = Spoken: +command-language-understood = Understood: +command-language-current-entry = {$id}. {$language} - {$name} (current) +command-language-entry = {$id}. {$language} - {$name} + +command-language-invalid-number = The language number must be between 0 and {$total}. Alternatively, use the language name. +command-language-invalid-language = The language {$id} does not exist or you cannot speak it. From e4f5917d5a373b616db0f37c0e3b356bf25149f7 Mon Sep 17 00:00:00 2001 From: fox Date: Sat, 15 Jun 2024 00:45:03 +0300 Subject: [PATCH 06/17] Added HandheldTranslatorComponent.SetLanguageOnInteract for convenience --- Content.Server/Language/TranslatorSystem.cs | 52 +++++++++++-------- .../HandheldTranslatorComponent.cs | 15 ++++-- .../Entities/Objects/Devices/translators.yml | 1 + 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index 3b7704b9a71..d1e0853d219 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Language.Systems; using Content.Shared.PowerCell; using Content.Shared.Language.Components.Translators; +using Robust.Shared.Utility; namespace Content.Server.Language; @@ -23,7 +24,6 @@ public override void Initialize() { base.Initialize(); - // I wanna die. But my death won't help us discover polymorphism. SubscribeLocalEvent(OnDetermineLanguages); SubscribeLocalEvent(OnDetermineLanguages); SubscribeLocalEvent(OnDetermineLanguages); @@ -31,7 +31,6 @@ public override void Initialize() SubscribeLocalEvent(OnTranslatorToggle); SubscribeLocalEvent(OnPowerCellSlotEmpty); - // TODO: why does this use InteractHandEvent?? SubscribeLocalEvent(OnTranslatorInteract); SubscribeLocalEvent(OnTranslatorDropped); } @@ -120,54 +119,65 @@ private void OnTranslatorDropped(EntityUid translator, HandheldTranslatorCompone RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); } - private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponent component, ActivateInWorldEvent args) + private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponent translatorComp, ActivateInWorldEvent args) { - if (!component.ToggleOnInteract) + if (!translatorComp.ToggleOnInteract) return; + // This will show a popup if false var hasPower = _powerCell.HasDrawCharge(translator); if (Transform(args.Target).ParentUid is { Valid: true } holder - && EntityManager.HasComponent(holder)) + && TryComp(holder, out var languageComp)) { // This translator is held by a language speaker and thus has an intrinsic counterpart bound to it. // Make sure it's up-to-date. var intrinsic = EnsureComp(holder); - var isEnabled = !component.Enabled; - if (intrinsic.Issuer != component) + var isEnabled = !translatorComp.Enabled; + if (intrinsic.Issuer != translatorComp) { - // The intrinsic comp wasn't owned by this handheld component, so this comp wasn't the active translator. - // Thus it needs to be turned on regardless of its previous state. - intrinsic.Issuer = component; + // The intrinsic comp wasn't owned by this handheld translator, so this wasn't the active translator. + // Thus, the intrinsic comp needs to be turned on regardless of its previous state. + intrinsic.Issuer = translatorComp; isEnabled = true; } - isEnabled &= hasPower; - UpdateBoundIntrinsicComp(component, intrinsic, isEnabled); - component.Enabled = isEnabled; + + UpdateBoundIntrinsicComp(translatorComp, intrinsic, isEnabled); + translatorComp.Enabled = isEnabled; _powerCell.SetPowerCellDrawEnabled(translator, isEnabled); - _language.EnsureValidLanguage(holder); + // Update the current language of the entity if necessary + if (isEnabled && translatorComp.SetLanguageOnInteract) + { + var firstNew = translatorComp.SpokenLanguages.FirstOrDefault(it => !languageComp.SpokenLanguages.Contains(it)); + if (firstNew != null) + _language.SetLanguage(holder, firstNew, languageComp); + } + + if (!isEnabled) + _language.EnsureValidLanguage(holder, languageComp); + + // TODO this raises at least 2 disctinct events of this type RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); } else { // This is a standalone translator (e.g. lying on the ground), toggle its state. - component.Enabled = !component.Enabled && hasPower; - _powerCell.SetPowerCellDrawEnabled(translator, !component.Enabled && hasPower); + translatorComp.Enabled = !translatorComp.Enabled && hasPower; + _powerCell.SetPowerCellDrawEnabled(translator, !translatorComp.Enabled && hasPower); } - OnAppearanceChange(translator, component); + OnAppearanceChange(translator, translatorComp); - // HasPower shows a popup when there's no power, so we do not proceed in that case if (hasPower) { var message = Loc.GetString( - component.Enabled + translatorComp.Enabled ? "translator-component-turnon" : "translator-component-shutoff", - ("translator", component.Owner)); - _popup.PopupEntity(message, component.Owner, args.User); + ("translator", translatorComp.Owner)); + _popup.PopupEntity(message, translatorComp.Owner, args.User); } } diff --git a/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs b/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs index f900603f01d..7e3de0eca61 100644 --- a/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs +++ b/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs @@ -7,9 +7,18 @@ namespace Content.Shared.Language.Components.Translators; public sealed partial class HandheldTranslatorComponent : Translators.BaseTranslatorComponent { /// - /// Whether or not interacting with this translator - /// toggles it on or off. + /// Whether interacting with this translator toggles it on and off. /// - [DataField("toggleOnInteract")] + [DataField] public bool ToggleOnInteract = true; + + /// + /// If true, when this translator is turned on, the entities' current spoken language will be set + /// to the first new language added by this translator. + /// + /// + /// This should generally be used for translators that translate speech between two languages. + /// + [DataField] + public bool SetLanguageOnInteract = true; } diff --git a/Resources/Prototypes/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/Entities/Objects/Devices/translators.yml index e5ad824c5d9..18f2dead687 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translators.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translators.yml @@ -203,3 +203,4 @@ - Kobold requires: - GalacticCommon + setLanguageOnInteract: false From ad56d5368e6d1df4d160250a02e92c8aab751227 Mon Sep 17 00:00:00 2001 From: fox Date: Sat, 15 Jun 2024 07:22:29 +0300 Subject: [PATCH 07/17] Refactor everything and pray it works: - Made it so that LanguageSpeakerComponent stores the actual data about entities' languages, while LanguageKnowledgeComponent stores intrinsic knowledge - Rewrote a lot of shitcode --- Content.Server/Chat/Systems/ChatSystem.cs | 4 +- .../Chemistry/ReagentEffects/MakeSentient.cs | 23 +- .../Language/Commands/ListLanguagesCommand.cs | 13 +- .../Commands/SelectLanguageCommand.cs | 6 +- .../Language/DetermineEntityLanguagesEvent.cs | 32 ++- .../Language/LanguageSystem.Networking.cs | 33 ++- Content.Server/Language/LanguageSystem.cs | 226 ++++++++++-------- .../Language}/LanguagesUpdateEvent.cs | 2 +- .../Language/TranslatorImplanterSystem.cs | 5 +- Content.Server/Language/TranslatorSystem.cs | 60 ++--- .../Mind/Commands/MakeSentientCommand.cs | 7 +- .../Radio/EntitySystems/HeadsetSystem.cs | 2 +- .../Radio/EntitySystems/RadioSystem.cs | 2 +- .../Components/LanguageKnowledgeComponent.cs | 22 ++ .../Components/LanguageSpeakerComponent.cs | 33 +-- 15 files changed, 263 insertions(+), 207 deletions(-) rename {Content.Shared/Language/Events => Content.Server/Language}/LanguagesUpdateEvent.cs (78%) create mode 100644 Content.Shared/Language/Components/LanguageKnowledgeComponent.cs diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 7eecaa32b43..a6c2223bc6d 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -502,7 +502,7 @@ private void SendEntityWhisper( if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full) continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them. - var canUnderstandLanguage = _language.CanUnderstand(listener, language); + var canUnderstandLanguage = _language.CanUnderstand(listener, language.ID); // How the entity perceives the message depends on whether it can understand its language var perceivedMessage = canUnderstandLanguage ? message : languageObfuscatedMessage; @@ -717,7 +717,7 @@ private void SendInVoiceRange(ChatChannel channel, string name, string message, // If the channel does not support languages, or the entity can understand the message, send the original message, otherwise send the obfuscated version - if (channel == ChatChannel.LOOC || channel == ChatChannel.Emotes || _language.CanUnderstand(listener, language)) + if (channel == ChatChannel.LOOC || channel == ChatChannel.Emotes || _language.CanUnderstand(listener, language.ID)) { _chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author); } diff --git a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs index da16529d515..8d5a583f6d8 100644 --- a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs +++ b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs @@ -1,13 +1,17 @@ using System.Linq; using Content.Server.Ghost.Roles.Components; +using Content.Server.Language; +using Content.Server.Language.Events; using Content.Server.Speech.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Language; using Content.Shared.Language.Systems; using Content.Shared.Mind.Components; using Robust.Shared.Prototypes; -using Content.Server.Psionics; //Nyano - Summary: pulls in the ability for the sentient creature to become psionic. -using Content.Shared.Humanoid; //Delta-V - Banning humanoids from becoming ghost roles. +using Content.Server.Psionics; +using Content.Shared.Body.Part; //Nyano - Summary: pulls in the ability for the sentient creature to become psionic. +using Content.Shared.Humanoid; +using Content.Shared.Language.Components; //Delta-V - Banning humanoids from becoming ghost roles. using Content.Shared.Language.Events; namespace Content.Server.Chemistry.ReagentEffects; @@ -28,19 +32,18 @@ public override void Effect(ReagentEffectArgs args) entityManager.RemoveComponent(uid); entityManager.RemoveComponent(uid); + // Make sure the entity knows at least fallback (Galactic Common) var speaker = entityManager.EnsureComponent(uid); + var knowledge = entityManager.EnsureComponent(uid); var fallback = SharedLanguageSystem.FallbackLanguagePrototype; - if (!speaker.UnderstoodLanguages.Contains(fallback)) - speaker.UnderstoodLanguages.Add(fallback); + if (!knowledge.UnderstoodLanguages.Contains(fallback)) + knowledge.UnderstoodLanguages.Add(fallback); - if (!speaker.SpokenLanguages.Contains(fallback)) - { - speaker.CurrentLanguage = fallback; - speaker.SpokenLanguages.Add(fallback); - } + if (!knowledge.SpokenLanguages.Contains(fallback)) + knowledge.SpokenLanguages.Add(fallback); - args.EntityManager.EventBus.RaiseLocalEvent(uid, new LanguagesUpdateEvent(), true); + IoCManager.Resolve().GetEntitySystem().UpdateEntityLanguages(uid, speaker); // Stops from adding a ghost role to things like people who already have a mind if (entityManager.TryGetComponent(uid, out var mindContainer) && mindContainer.HasMind) diff --git a/Content.Server/Language/Commands/ListLanguagesCommand.cs b/Content.Server/Language/Commands/ListLanguagesCommand.cs index 7cf96d74e61..e5787cba48c 100644 --- a/Content.Server/Language/Commands/ListLanguagesCommand.cs +++ b/Content.Server/Language/Commands/ListLanguagesCommand.cs @@ -1,4 +1,5 @@ using Content.Shared.Administration; +using Content.Shared.Language; using Robust.Shared.Console; using Robust.Shared.Enums; @@ -29,23 +30,23 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) } var languages = IoCManager.Resolve().GetEntitySystem(); - - var (spokenLangs, knownLangs) = languages.GetAllLanguages(playerEntity); var currentLang = languages.GetLanguage(playerEntity).ID; shell.WriteLine(Loc.GetString("command-language-spoken")); - for (int i = 0; i < spokenLangs.Count; i++) + var spoken = languages.GetSpokenLanguages(playerEntity); + for (int i = 0; i < spoken.Count; i++) { - var lang = spokenLangs[i]; + var lang = spoken[i]; shell.WriteLine(lang == currentLang ? Loc.GetString("command-language-current-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang))) : Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang)))); } shell.WriteLine(Loc.GetString("command-language-understood")); - for (int i = 0; i < knownLangs.Count; i++) + var understood = languages.GetUnderstoodLanguages(playerEntity); + for (int i = 0; i < understood.Count; i++) { - var lang = knownLangs[i]; + var lang = understood[i]; shell.WriteLine(Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang)))); } } diff --git a/Content.Server/Language/Commands/SelectLanguageCommand.cs b/Content.Server/Language/Commands/SelectLanguageCommand.cs index 8a6ca3174a9..d340135925d 100644 --- a/Content.Server/Language/Commands/SelectLanguageCommand.cs +++ b/Content.Server/Language/Commands/SelectLanguageCommand.cs @@ -36,13 +36,13 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) var languages = IoCManager.Resolve().GetEntitySystem(); - if (!TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var languageId)) + if (!TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var language)) { shell.WriteError(failReason); return; } - languages.SetLanguage(playerEntity, languageId.ID); + languages.SetLanguage(playerEntity, language.ID); } // TODO: find a better place for this method @@ -63,7 +63,7 @@ public static bool TryParseLanguageArgument( if (int.TryParse(input, out var num)) { // The argument is a number - var (spoken, _) = languageSystem.GetAllLanguages(speaker); + var spoken = languageSystem.GetSpokenLanguages(speaker); if (num > 0 && num - 1 < spoken.Count) language = languageSystem.GetLanguagePrototype(spoken[num - 1]); diff --git a/Content.Server/Language/DetermineEntityLanguagesEvent.cs b/Content.Server/Language/DetermineEntityLanguagesEvent.cs index 13ab2cac279..8d6b868d070 100644 --- a/Content.Server/Language/DetermineEntityLanguagesEvent.cs +++ b/Content.Server/Language/DetermineEntityLanguagesEvent.cs @@ -1,29 +1,25 @@ +using Content.Shared.Language; + namespace Content.Server.Language; /// -/// Raised in order to determine the language an entity speaks at the current moment, -/// as well as the list of all languages the entity may speak and understand. +/// Raised in order to determine the list of languages the entity can speak and understand at the given moment. +/// Typically raised on an entity after a language agent (e.g. a translator) has been added to or removed from them. /// -public sealed class DetermineEntityLanguagesEvent : EntityEventArgs +[ByRefEvent] +public record struct DetermineEntityLanguagesEvent { /// - /// The default language of this entity. If empty, remain unchanged. - /// This field has no effect if the entity decides to speak in a concrete language. - /// - public string CurrentLanguage; - /// - /// The list of all languages the entity may speak. Must NOT be held as a reference! + /// The list of all languages the entity may speak. + /// By default, contains the languages this entity speaks intrinsically. /// - public List SpokenLanguages; + public HashSet SpokenLanguages = new(); + /// - /// The list of all languages the entity may understand. Must NOT be held as a reference! + /// The list of all languages the entity may understand. + /// By default, contains the languages this entity understands intrinsically. /// - public List UnderstoodLanguages; + public HashSet UnderstoodLanguages = new(); - public DetermineEntityLanguagesEvent(string currentLanguage, List spokenLanguages, List understoodLanguages) - { - CurrentLanguage = currentLanguage; - SpokenLanguages = spokenLanguages; - UnderstoodLanguages = understoodLanguages; - } + public DetermineEntityLanguagesEvent() {} } diff --git a/Content.Server/Language/LanguageSystem.Networking.cs b/Content.Server/Language/LanguageSystem.Networking.cs index 7517b4185e3..038db9d8fcb 100644 --- a/Content.Server/Language/LanguageSystem.Networking.cs +++ b/Content.Server/Language/LanguageSystem.Networking.cs @@ -1,3 +1,4 @@ +using Content.Server.Language.Events; using Content.Server.Mind; using Content.Shared.Language; using Content.Shared.Language.Events; @@ -7,11 +8,6 @@ namespace Content.Server.Language; -/// -/// LanguageSystem Networking -/// This is used to update client state when mind change entity. -/// - public sealed partial class LanguageSystem { [Dependency] private readonly MindSystem _mind = default!; @@ -19,6 +15,11 @@ public sealed partial class LanguageSystem public void InitializeNet() { + SubscribeNetworkEvent(OnClientSetLanguage); + SubscribeNetworkEvent((_, session) => SendLanguageStateToClient(session.SenderSession)); + + SubscribeLocalEvent((uid, comp, _) => SendLanguageStateToClient(uid, comp)); + // Refresh the client's state when its mind hops to a different entity SubscribeLocalEvent((uid, _, _) => SendLanguageStateToClient(uid)); SubscribeLocalEvent((_, _, args) => @@ -26,12 +27,21 @@ public void InitializeNet() if (args.Mind.Comp.Session != null) SendLanguageStateToClient(args.Mind.Comp.Session); }); - - SubscribeLocalEvent((uid, comp, _) => SendLanguageStateToClient(uid, comp)); - SubscribeNetworkEvent((_, session) => SendLanguageStateToClient(session.SenderSession)); } + private void OnClientSetLanguage(LanguagesSetMessage message, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity is not { Valid: true } uid) + return; + + var language = GetLanguagePrototype(message.CurrentLanguage); + if (language == null || !CanSpeak(uid, language.ID)) + return; + + SetLanguage(uid, language.ID); + } + private void SendLanguageStateToClient(EntityUid uid, LanguageSpeakerComponent? comp = null) { // Try to find a mind inside the entity and notify its session @@ -52,8 +62,11 @@ private void SendLanguageStateToClient(ICommonSession session, LanguageSpeakerCo private void SendLanguageStateToClient(EntityUid uid, ICommonSession session, LanguageSpeakerComponent? component = null) { - var langs = GetLanguages(uid, component); - var message = new LanguagesUpdatedMessage(langs.CurrentLanguage, langs.SpokenLanguages, langs.UnderstoodLanguages); + if (!Resolve(uid, ref component)) + return; + + // TODO this is really stupid and can be avoided if we just make everything shared... + var message = new LanguagesUpdatedMessage(component.CurrentLanguage, component.SpokenLanguages, component.UnderstoodLanguages); RaiseNetworkEvent(message, session); } } diff --git a/Content.Server/Language/LanguageSystem.cs b/Content.Server/Language/LanguageSystem.cs index 2b5bf454ce0..2774f28374e 100644 --- a/Content.Server/Language/LanguageSystem.cs +++ b/Content.Server/Language/LanguageSystem.cs @@ -1,5 +1,7 @@ using System.Linq; +using Content.Server.Language.Events; using Content.Shared.Language; +using Content.Shared.Language.Components; using Content.Shared.Language.Events; using Content.Shared.Language.Systems; using UniversalLanguageSpeakerComponent = Content.Shared.Language.Components.UniversalLanguageSpeakerComponent; @@ -8,177 +10,203 @@ namespace Content.Server.Language; public sealed partial class LanguageSystem : SharedLanguageSystem { - // Static and re-used event instances used to minimize memory allocations during language processing, which can happen many times per tick. - // These are used in the method GetLanguages and returned from it. They should never be mutated outside of that method or returned outside this system. - private readonly DetermineEntityLanguagesEvent - _determineLanguagesEvent = new(string.Empty, new(), new()), - _universalLanguagesEvent = new(UniversalPrototype, [UniversalPrototype], [UniversalPrototype]); // Returned for universal speakers only - - - public override void Initialize() { base.Initialize(); + InitializeNet(); - SubscribeNetworkEvent(OnClientSetLanguage); SubscribeLocalEvent(OnInitLanguageSpeaker); - - InitializeNet(); } #region public api - /// - /// Obfuscate a message using an entity's default language. - /// - public string ObfuscateSpeech(EntityUid source, string message) - { - var language = GetLanguage(source); - return ObfuscateSpeech(message, language); - } - public bool CanUnderstand(EntityUid listener, LanguagePrototype language, LanguageSpeakerComponent? listenerLanguageComp = null) + public bool CanUnderstand(EntityUid listener, string language, LanguageSpeakerComponent? component = null) { - if (language.ID == UniversalPrototype || HasComp(listener)) + if (language == UniversalPrototype || HasComp(listener)) return true; - var listenerLanguages = GetLanguages(listener, listenerLanguageComp)?.UnderstoodLanguages; + if (!Resolve(listener, ref component)) + return false; - return listenerLanguages?.Contains(language.ID, StringComparer.Ordinal) ?? false; + return component.UnderstoodLanguages.Contains(language); } - public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? speakerComp = null) + public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? component = null) { if (HasComp(speaker)) return true; - var langs = GetLanguages(speaker, speakerComp)?.UnderstoodLanguages; - return langs?.Contains(language, StringComparer.Ordinal) ?? false; + if (!Resolve(speaker, ref component)) + return false; + + return component.SpokenLanguages.Contains(language); } /// - /// Returns the current language of the given entity. - /// Assumes Universal if not specified. + /// Returns the current language of the given entity, assumes Universal if it's not a language speaker. /// - public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? languageComp = null) + public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? component = null) { - var id = GetLanguages(speaker, languageComp)?.CurrentLanguage; - if (id == null) - return Universal; // Fallback + if (HasComp(speaker) || !Resolve(speaker, ref component)) + return Universal; // Serves both as a fallback and uhhh something (TODO: fix this comment) - _prototype.TryIndex(id, out LanguagePrototype? proto); + if (string.IsNullOrEmpty(component.CurrentLanguage) || !_prototype.TryIndex(component.CurrentLanguage, out var proto)) + return Universal; - return proto ?? Universal; + return proto; } - public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? languageComp = null) + /// + /// Returns the list of languages this entity can speak. + /// + /// Typically, checking is sufficient. + public List GetSpokenLanguages(EntityUid uid) { - if (!CanSpeak(speaker, language) || HasComp(speaker)) - return; + if (HasComp(uid)) + return [UniversalPrototype]; - if (languageComp == null && !TryComp(speaker, out languageComp)) - return; + if (TryComp(uid, out var component)) + return component.SpokenLanguages; - if (languageComp.CurrentLanguage == language) + return []; + } + + /// + /// Returns the list of languages this entity can understand. + /// + /// Typically, checking is sufficient. + public List GetUnderstoodLanguages(EntityUid uid) + { + if (HasComp(uid)) + return [UniversalPrototype]; // This one is tricky because... well, they understand all of them, not just one. + + if (TryComp(uid, out var component)) + return component.UnderstoodLanguages; + + return []; + } + + public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? component = null) + { + if (!CanSpeak(speaker, language) || (HasComp(speaker) && language != UniversalPrototype)) return; - languageComp.CurrentLanguage = language; + if (!Resolve(speaker, ref component) || component.CurrentLanguage == language) + return; + component.CurrentLanguage = language; RaiseLocalEvent(speaker, new LanguagesUpdateEvent(), true); } /// - /// Adds a new language to the lists of understood and/or spoken languages of the given component. + /// Adds a new language to the respective lists of intrinsically known languages of the given entity. /// - public void AddLanguage(LanguageSpeakerComponent comp, string language, bool addSpoken = true, bool addUnderstood = true) + public void AddLanguage( + EntityUid uid, + string language, + bool addSpoken = true, + bool addUnderstood = true, + LanguageKnowledgeComponent? knowledge = null, + LanguageSpeakerComponent? speaker = null) { - if (addSpoken && !comp.SpokenLanguages.Contains(language)) - comp.SpokenLanguages.Add(language); + if (knowledge == null) + knowledge = EnsureComp(uid); + + if (addSpoken && !knowledge.SpokenLanguages.Contains(language)) + knowledge.SpokenLanguages.Add(language); - if (addUnderstood && !comp.UnderstoodLanguages.Contains(language)) - comp.UnderstoodLanguages.Add(language); + if (addUnderstood && !knowledge.UnderstoodLanguages.Contains(language)) + knowledge.UnderstoodLanguages.Add(language); - RaiseLocalEvent(comp.Owner, new LanguagesUpdateEvent(), true); + UpdateEntityLanguages(uid, speaker); } - public (List spoken, List understood) GetAllLanguages(EntityUid speaker) + /// + /// Removes a language from the respective lists of intrinsically known languages of the given entity. + /// + public void RemoveLanguage( + EntityUid uid, + string language, + bool removeSpoken = true, + bool removeUnderstood = true, + LanguageKnowledgeComponent? knowledge = null, + LanguageSpeakerComponent? speaker = null) { - var languages = GetLanguages(speaker); - // The lists need to be copied because the internal ones are re-used for performance reasons. - return (new List(languages.SpokenLanguages), new List(languages.UnderstoodLanguages)); + if (knowledge == null) + knowledge = EnsureComp(uid); + + if (removeSpoken) + knowledge.SpokenLanguages.Remove(language); + + if (removeUnderstood) + knowledge.UnderstoodLanguages.Remove(language); + + UpdateEntityLanguages(uid, speaker); } /// /// Ensures the given entity has a valid language as its current language. /// If not, sets it to the first entry of its SpokenLanguages list, or universal if it's empty. /// - public void EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) + /// True if the current language was modified, false otherwise. + public bool EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) { if (comp == null && !TryComp(entity, out comp)) - return; + return false; - var langs = GetLanguages(entity, comp); - if (!langs.SpokenLanguages.Contains(comp!.CurrentLanguage, StringComparer.Ordinal)) + if (!comp.SpokenLanguages.Contains(comp.CurrentLanguage)) { - comp.CurrentLanguage = langs.SpokenLanguages.FirstOrDefault(UniversalPrototype); - RaiseLocalEvent(comp.Owner, new LanguagesUpdateEvent(), true); + comp.CurrentLanguage = comp.SpokenLanguages.FirstOrDefault(UniversalPrototype); + RaiseLocalEvent(entity, new LanguagesUpdateEvent()); + return true; } - } - #endregion - #region event handling - private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) - { - if (string.IsNullOrEmpty(component.CurrentLanguage)) - component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); + return false; } - #endregion - #region internal api - misc /// - /// Dynamically resolves the current language of the entity and the list of all languages it speaks. - /// - /// If the entity is not a language speaker, or is a universal language speaker, then it's assumed to speak Universal, - /// aka all languages at once and none at the same time. + /// Immediately refreshes the cached lists of spoken and understood languages for the given entity. /// - /// - /// The returned event is reused and thus must not be held as a reference anywhere but inside the caller function. - /// - private DetermineEntityLanguagesEvent GetLanguages(EntityUid speaker, LanguageSpeakerComponent? comp = null) + public void UpdateEntityLanguages(EntityUid entity, LanguageSpeakerComponent? languages = null) { - // This is a shortcut for ghosts and entities that should not speak normally (admemes) - if (HasComp(speaker) || !TryComp(speaker, out comp)) - return _universalLanguagesEvent; + if (!Resolve(entity, ref languages)) + return; - var ev = _determineLanguagesEvent; - ev.SpokenLanguages.Clear(); - ev.UnderstoodLanguages.Clear(); + var ev = new DetermineEntityLanguagesEvent(); + // We add the intrinsically known languages first so other systems can manipulate them easily + if (TryComp(entity, out var knowledge)) + { + foreach (var spoken in knowledge.SpokenLanguages) + ev.SpokenLanguages.Add(spoken); - ev.CurrentLanguage = comp.CurrentLanguage; - ev.SpokenLanguages.AddRange(comp.SpokenLanguages); - ev.UnderstoodLanguages.AddRange(comp.UnderstoodLanguages); + foreach (var understood in knowledge.UnderstoodLanguages) + ev.UnderstoodLanguages.Add(understood); + } - RaiseLocalEvent(speaker, ev, true); + RaiseLocalEvent(entity, ref ev); - if (ev.CurrentLanguage.Length == 0) - ev.CurrentLanguage = !string.IsNullOrEmpty(comp.CurrentLanguage) ? comp.CurrentLanguage : UniversalPrototype; // Fall back to account for admemes like admins possessing a bread - return ev; + languages.SpokenLanguages.Clear(); + languages.UnderstoodLanguages.Clear(); + + languages.SpokenLanguages.AddRange(ev.SpokenLanguages); + languages.UnderstoodLanguages.AddRange(ev.UnderstoodLanguages); + + if (!EnsureValidLanguage(entity)) + RaiseLocalEvent(entity, new LanguagesUpdateEvent()); } - /// - /// Set CurrentLanguage of the client, the client must be able to Understand the language requested. - /// - private void OnClientSetLanguage(LanguagesSetMessage message, EntitySessionEventArgs args) - { - if (args.SenderSession.AttachedEntity is not {Valid: true} speaker) - return; + #endregion - var language = GetLanguagePrototype(message.CurrentLanguage); + #region event handling - if (language == null || !CanSpeak(speaker, language.ID)) - return; + private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) + { + if (string.IsNullOrEmpty(component.CurrentLanguage)) + component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); - SetLanguage(speaker, language.ID); + UpdateEntityLanguages(uid, component); } + #endregion } diff --git a/Content.Shared/Language/Events/LanguagesUpdateEvent.cs b/Content.Server/Language/LanguagesUpdateEvent.cs similarity index 78% rename from Content.Shared/Language/Events/LanguagesUpdateEvent.cs rename to Content.Server/Language/LanguagesUpdateEvent.cs index 90ce2f4446b..88ea09916bb 100644 --- a/Content.Shared/Language/Events/LanguagesUpdateEvent.cs +++ b/Content.Server/Language/LanguagesUpdateEvent.cs @@ -1,4 +1,4 @@ -namespace Content.Shared.Language.Events; +namespace Content.Server.Language.Events; /// /// Raised on an entity when its list of languages changes. diff --git a/Content.Server/Language/TranslatorImplanterSystem.cs b/Content.Server/Language/TranslatorImplanterSystem.cs index 1e0c13375e4..0d67bbb1279 100644 --- a/Content.Server/Language/TranslatorImplanterSystem.cs +++ b/Content.Server/Language/TranslatorImplanterSystem.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Administration.Logs; +using Content.Server.Language.Events; using Content.Server.Popups; using Content.Shared.Database; using Content.Shared.Interaction; @@ -12,6 +13,7 @@ namespace Content.Server.Language; +// TODO: pending rewrite public sealed class TranslatorImplanterSystem : SharedTranslatorImplanterSystem { [Dependency] private readonly PopupSystem _popup = default!; @@ -35,13 +37,14 @@ private void OnImplant(EntityUid implanter, TranslatorImplanterComponent compone if (!TryComp(target, out var speaker)) return; + // TODO: yes I know this should use whitelist comp, yes this will be changed after rewrite if (component.MobsOnly && !HasComp(target)) { _popup.PopupEntity("translator-implanter-refuse", component.Owner); return; } - var understood = _language.GetAllLanguages(target).understood; + var understood = speaker.UnderstoodLanguages; if (component.RequiredLanguages.Count > 0 && !component.RequiredLanguages.Any(lang => understood.Contains(lang))) { _popup.PopupEntity(Loc.GetString("translator-implanter-refuse", diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index d1e0853d219..b2166b44f13 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.Language.Events; using Content.Server.Popups; using Content.Server.PowerCell; using Content.Shared.Interaction; @@ -35,8 +36,7 @@ public override void Initialize() SubscribeLocalEvent(OnTranslatorDropped); } - private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent component, - DetermineEntityLanguagesEvent ev) + private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent component, DetermineEntityLanguagesEvent ev) { if (!component.Enabled) return; @@ -44,6 +44,11 @@ private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent co if (!_powerCell.HasActivatableCharge(uid)) return; + // The idea here is as follows: + // Required languages are languages that are required to operate the translator. + // The translator has a limited number of languages it can translate to and translate from. + // If the wielder understands the language of the translator, they will be able to understand translations provided by it + // If the wielder also speaks that language, they will be able to use it to translate their own speech by "speaking" in that language var addUnderstood = true; var addSpoken = true; if (component.RequiredLanguages.Count > 0) @@ -53,10 +58,10 @@ private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent co // Add langs when the wielder has all of the required languages foreach (var language in component.RequiredLanguages) { - if (!ev.SpokenLanguages.Contains(language, StringComparer.Ordinal)) + if (!ev.SpokenLanguages.Contains(language)) addSpoken = false; - if (!ev.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) + if (!ev.UnderstoodLanguages.Contains(language)) addUnderstood = false; } } @@ -67,30 +72,25 @@ private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent co addSpoken = false; foreach (var language in component.RequiredLanguages) { - if (ev.SpokenLanguages.Contains(language, StringComparer.Ordinal)) + if (ev.SpokenLanguages.Contains(language)) addSpoken = true; - if (ev.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) + if (ev.UnderstoodLanguages.Contains(language)) addUnderstood = true; } } } if (addSpoken) - { foreach (var language in component.SpokenLanguages) - AddIfNotExists(ev.SpokenLanguages, language); - - if (component.DefaultLanguageOverride != null && ev.CurrentLanguage.Length == 0) - ev.CurrentLanguage = component.DefaultLanguageOverride; - } + ev.SpokenLanguages.Add(language); if (addUnderstood) foreach (var language in component.UnderstoodLanguages) - AddIfNotExists(ev.UnderstoodLanguages, language); + ev.UnderstoodLanguages.Add(language); } - private void OnTranslatorInteract( EntityUid translator, HandheldTranslatorComponent component, InteractHandEvent args) + private void OnTranslatorInteract(EntityUid translator, HandheldTranslatorComponent component, InteractHandEvent args) { var holder = args.User; if (!EntityManager.HasComponent(holder)) @@ -99,7 +99,7 @@ private void OnTranslatorInteract( EntityUid translator, HandheldTranslatorCompo var intrinsic = EnsureComp(holder); UpdateBoundIntrinsicComp(component, intrinsic, component.Enabled); - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + _language.UpdateEntityLanguages(holder); } private void OnTranslatorDropped(EntityUid translator, HandheldTranslatorComponent component, DroppedEvent args) @@ -114,9 +114,7 @@ private void OnTranslatorDropped(EntityUid translator, HandheldTranslatorCompone RemCompDeferred(holder, intrinsic); } - _language.EnsureValidLanguage(holder); - - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + _language.UpdateEntityLanguages(holder); } private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponent translatorComp, ActivateInWorldEvent args) @@ -151,15 +149,11 @@ private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponen if (isEnabled && translatorComp.SetLanguageOnInteract) { var firstNew = translatorComp.SpokenLanguages.FirstOrDefault(it => !languageComp.SpokenLanguages.Contains(it)); - if (firstNew != null) + if (firstNew is {}) _language.SetLanguage(holder, firstNew, languageComp); } - if (!isEnabled) - _language.EnsureValidLanguage(holder, languageComp); - - // TODO this raises at least 2 disctinct events of this type - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + _language.UpdateEntityLanguages(holder, languageComp); } else { @@ -188,7 +182,7 @@ private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorCompon OnAppearanceChange(translator, component); if (Transform(translator).ParentUid is { Valid: true } holder - && EntityManager.HasComponent(holder)) + && TryComp(holder, out var languageComp)) { if (!EntityManager.TryGetComponent(holder, out var intrinsic)) return; @@ -196,11 +190,10 @@ private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorCompon if (intrinsic.Issuer == component) { intrinsic.Enabled = false; - EntityManager.RemoveComponent(holder, intrinsic); + RemComp(holder, intrinsic); } - _language.EnsureValidLanguage(holder); - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + _language.UpdateEntityLanguages(holder, languageComp); } } @@ -211,8 +204,8 @@ private void UpdateBoundIntrinsicComp(HandheldTranslatorComponent comp, HoldsTra { if (isEnabled) { - intrinsic.SpokenLanguages = new List(comp.SpokenLanguages); - intrinsic.UnderstoodLanguages = new List(comp.UnderstoodLanguages); + intrinsic.SpokenLanguages = [..comp.SpokenLanguages]; + intrinsic.UnderstoodLanguages = [..comp.UnderstoodLanguages]; intrinsic.DefaultLanguageOverride = comp.DefaultLanguageOverride; } else @@ -225,11 +218,4 @@ private void UpdateBoundIntrinsicComp(HandheldTranslatorComponent comp, HoldsTra intrinsic.Enabled = isEnabled; intrinsic.Issuer = comp; } - - private static void AddIfNotExists(List list, string item) - { - if (list.Contains(item)) - return; - list.Add(item); - } } diff --git a/Content.Server/Mind/Commands/MakeSentientCommand.cs b/Content.Server/Mind/Commands/MakeSentientCommand.cs index cacd499ab8d..b58d782d9c5 100644 --- a/Content.Server/Mind/Commands/MakeSentientCommand.cs +++ b/Content.Server/Mind/Commands/MakeSentientCommand.cs @@ -61,10 +61,11 @@ public static void MakeSentient(EntityUid uid, IEntityManager entityManager, boo var language = IoCManager.Resolve().GetEntitySystem(); var speaker = entityManager.EnsureComponent(uid); - // If the speaker knows any language (like monkey or robot), they keep those - // Otherwise, we give them the fallback + + // If the entity already speaks some language (like monkey or robot), we do nothing else + // Otherwise, we give them the fallback language if (speaker.SpokenLanguages.Count == 0) - language.AddLanguage(speaker, SharedLanguageSystem.FallbackLanguagePrototype); + language.AddLanguage(uid, SharedLanguageSystem.FallbackLanguagePrototype); } entityManager.EnsureComponent(uid); diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index 53517da6cb4..2500138a238 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -106,7 +106,7 @@ private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref Rad var parent = Transform(uid).ParentUid; if (TryComp(parent, out ActorComponent? actor)) { - var canUnderstand = _language.CanUnderstand(parent, args.Language); + var canUnderstand = _language.CanUnderstand(parent, args.Language.ID); var msg = new MsgChatMessage { Message = canUnderstand ? args.OriginalChatMsg : args.LanguageObfuscatedChatMsg diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 60aa7c2f4fb..0e4da856ce6 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -60,7 +60,7 @@ private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent c // Einstein-Engines - languages mechanic var listener = component.Owner; var msg = args.OriginalChatMsg; - if (listener != null && !_language.CanUnderstand(listener, args.Language)) + if (listener != null && !_language.CanUnderstand(listener, args.Language.ID)) msg = args.LanguageObfuscatedChatMsg; _netMan.ServerSendMessage(new MsgChatMessage { Message = msg}, actor.PlayerSession.Channel); diff --git a/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs b/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs new file mode 100644 index 00000000000..0632f5d9cb2 --- /dev/null +++ b/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.Language.Components; + +/// +/// Stores data about entities' intrinsic language knowledge. +/// +[RegisterComponent] +public sealed partial class LanguageKnowledgeComponent : Component +{ + /// + /// List of languages this entity can speak without any external tools. + /// + [DataField("speaks", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] + public List SpokenLanguages = new(); + + /// + /// List of languages this entity can understand without any external tools. + /// + [DataField("understands", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] + public List UnderstoodLanguages = new(); +} diff --git a/Content.Shared/Language/Components/LanguageSpeakerComponent.cs b/Content.Shared/Language/Components/LanguageSpeakerComponent.cs index 95232ffe6ff..e8ebccb3ddf 100644 --- a/Content.Shared/Language/Components/LanguageSpeakerComponent.cs +++ b/Content.Shared/Language/Components/LanguageSpeakerComponent.cs @@ -1,29 +1,32 @@ -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - namespace Content.Shared.Language; -[RegisterComponent, AutoGenerateComponentState] +// TODO: either move all language speaker-related components to server side, or make everything else shared. +// The current approach leads to confusion, as the server never informs the client of updates in these components. + +/// +/// Stores the current state of the languages the entity can speak and understand. +/// +/// +/// All fields of this component are populated during a DetermineEntityLanguagesEvent. +/// They are not to be modified externally. +/// +[RegisterComponent] public sealed partial class LanguageSpeakerComponent : Component { /// - /// The current language the entity may use to speak. + /// The current language the entity uses when speaking. /// Other listeners will hear the entity speak in this language. /// - [ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public string CurrentLanguage = default!; + [DataField] + public string CurrentLanguage = ""; // The language system will override it on init /// - /// List of languages this entity can speak. + /// List of languages this entity can speak at the current moment. /// - [ViewVariables] - [DataField("speaks", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] - public List SpokenLanguages = new(); + public List SpokenLanguages = []; /// - /// List of languages this entity can understand. + /// List of languages this entity can understand at the current moment. /// - [ViewVariables] - [DataField("understands", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] - public List UnderstoodLanguages = new(); + public List UnderstoodLanguages = []; } From 0e63714cd39cd93de0ac233ee1a4d963f13df53b Mon Sep 17 00:00:00 2001 From: fox Date: Sat, 15 Jun 2024 08:26:18 +0300 Subject: [PATCH 08/17] Update yaml prototypes accordingly --- .../DeltaV/Entities/Mobs/NPCs/animals.yml | 4 +- .../DeltaV/Entities/Mobs/NPCs/familiars.yml | 2 +- .../DeltaV/Entities/Mobs/NPCs/nukiemouse.yml | 2 +- .../DeltaV/Entities/Mobs/Species/harpy.yml | 2 +- .../Entities/Mobs/Species/vulpkanin.yml | 2 +- .../Mobs/Cyborgs/base_borg_chassis.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 38 +++++++++---------- .../Entities/Mobs/NPCs/argocyte.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/pets.yml | 20 +++++----- .../Entities/Mobs/NPCs/regalrat.yml | 4 +- .../Prototypes/Entities/Mobs/NPCs/shadows.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/silicon.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/slimes.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/space.yml | 6 +-- .../Prototypes/Entities/Mobs/NPCs/xeno.yml | 4 +- .../Prototypes/Entities/Mobs/Species/base.yml | 2 +- .../Entities/Mobs/Species/diona.yml | 2 +- .../Entities/Mobs/Species/dwarf.yml | 2 +- .../Entities/Mobs/Species/human.yml | 2 +- .../Prototypes/Entities/Mobs/Species/moth.yml | 2 +- .../Entities/Mobs/Species/reptilian.yml | 2 +- .../Entities/Mobs/Species/slime.yml | 2 +- Resources/Prototypes/Entities/Mobs/base.yml | 1 + .../Structures/Machines/vending_machines.yml | 2 +- .../Nyanotrasen/Entities/Mobs/Species/Oni.yml | 2 +- .../Entities/Mobs/Species/felinid.yml | 2 +- 26 files changed, 58 insertions(+), 57 deletions(-) diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml index e932974a0f4..dd59d74d3f0 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml @@ -66,7 +66,7 @@ - type: Tag tags: - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Fox understands: @@ -179,7 +179,7 @@ tags: - DoorBumpOpener - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml index fa51b99325c..2dad0fe2e65 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml @@ -95,7 +95,7 @@ factions: - PsionicInterloper - NanoTrasen - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon understands: diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml index c2ae33ec0ba..8968f0e77ad 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml @@ -96,7 +96,7 @@ spawned: - id: FoodMeat amount: 1 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml index 18437e074dd..1925067d571 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml @@ -127,7 +127,7 @@ templateId: digitigrade - type: HarpyVisuals - type: UltraVision - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml index 52853d696a2..9e4f80bfb52 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml @@ -97,7 +97,7 @@ Female: FemaleVulpkanin Unsexed: MaleVulpkanin - type: DogVision - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Canilunzt diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index 0645e451af2..196abcfab76 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -213,7 +213,7 @@ visMask: - PsionicInvisibility - Normal - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RobotTalk diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 369544fdc1b..3b02c14d9eb 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -48,7 +48,7 @@ flavorKind: station-event-random-sentience-flavor-organic - type: Bloodstream bloodMaxVolume: 50 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -232,7 +232,7 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Chicken understands: @@ -510,7 +510,7 @@ prob: 0.5 - type: Extractable grindableSolutionName: food - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Moffic understands: @@ -610,7 +610,7 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Duck understands: @@ -851,7 +851,7 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Voice/Arachnid/arachnid_chitter.ogg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Crab understands: @@ -1091,7 +1091,7 @@ - type: Inventory speciesId: kangaroo templateId: kangaroo - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Kangaroo understands: @@ -1284,7 +1284,7 @@ - type: Speech speechSounds: Monkey speechVerb: Monkey - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -1323,7 +1323,7 @@ - type: Speech speechSounds: Monkey speechVerb: Monkey - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -1368,7 +1368,7 @@ - type: NameIdentifier group: Kobold - type: LizardAccent - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Kobold understands: @@ -1601,7 +1601,7 @@ spawned: - id: FoodMeatRat amount: 1 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -1930,7 +1930,7 @@ path: /Audio/Animals/parrot_raught.ogg - type: Bloodstream bloodMaxVolume: 50 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon understands: @@ -2181,7 +2181,7 @@ - type: MeleeChemicalInjector transferAmount: 0.75 solution: melee - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -2516,7 +2516,7 @@ - type: Tag tags: - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Fox understands: @@ -2567,7 +2567,7 @@ spawned: - id: FoodMeat amount: 2 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -2723,7 +2723,7 @@ spawned: - id: FoodMeat amount: 3 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: @@ -2794,7 +2794,7 @@ - type: NpcFactionMember factions: - Syndicate - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -3095,7 +3095,7 @@ spawned: - id: FoodMeat amount: 1 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -3205,7 +3205,7 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Animals/pig_oink.ogg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Pig understands: @@ -3295,7 +3295,7 @@ reformTime: 10 popupText: diona-reform-attempt reformPrototype: MobDionaReformed - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - RootSpeak understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml index 3bcf8e7a16f..f8d0497b971 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml @@ -15,7 +15,7 @@ - type: Sprite sprite: Mobs/Aliens/Argocyte/argocyte_common.rsi - type: SolutionContainerManager - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 8ca1b2d2f0e..a029f038457 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -36,7 +36,7 @@ - VimPilot - type: StealTarget stealGroup: AnimalIan - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -127,7 +127,7 @@ tags: - CannotSuicide - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: @@ -151,7 +151,7 @@ tags: - CannotSuicide - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: @@ -306,7 +306,7 @@ spawned: - id: FoodMeat amount: 2 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -409,7 +409,7 @@ spawned: - id: FoodMeat amount: 3 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -572,7 +572,7 @@ - VimPilot - type: StealTarget stealGroup: AnimalRenault - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Fox understands: @@ -625,7 +625,7 @@ - CannotSuicide - Hamster - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -803,7 +803,7 @@ attributes: proper: true gender: female - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Bubblish understands: @@ -843,7 +843,7 @@ attributes: proper: true gender: male - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -877,7 +877,7 @@ # - type: AlwaysRevolutionaryConvertible - type: StealTarget stealGroup: AnimalTropico - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Crab understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index 50fe3b6765e..75f50f242e1 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -119,7 +119,7 @@ attributes: gender: male - type: PotentialPsionic # Nyano - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Mouse @@ -296,7 +296,7 @@ - type: Food - type: Item size: Tiny # Delta V - Make them eatable and pickable. - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml b/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml index 9559ae3a0c0..08cb776c530 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml @@ -50,7 +50,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: cat - type: Physics - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index e3166c15f6e..f9cf0db5f6f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -107,7 +107,7 @@ - type: TypingIndicator proto: robot - type: ZombieImmune - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RobotTalk diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index 901bf149cbc..9c992545a85 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -111,7 +111,7 @@ successChance: 0.5 interactSuccessString: petting-success-slimes interactFailureString: petting-failure-generic - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Bubblish understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index 9ea2d784dbb..5ccd516a0fb 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -165,7 +165,7 @@ - type: FootstepModifier footstepSoundCollection: collection: FootstepBounce - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Kangaroo understands: @@ -251,7 +251,7 @@ - type: MeleeChemicalInjector solution: melee transferAmount: 4 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -357,7 +357,7 @@ - type: MeleeChemicalInjector solution: melee transferAmount: 6 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 26553a2f1f2..397989643e6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -125,7 +125,7 @@ chance: -2 - type: Psionic #Nyano - Summary: makes psionic by default. removable: false - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -239,7 +239,7 @@ - type: Tag tags: - CannotSuicide - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Xeno diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 67212d416fe..da9858105b4 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -219,7 +219,7 @@ price: 1500 # Kidnapping a living person and selling them for cred is a good move. deathPenalty: 0.01 # However they really ought to be living and intact, otherwise they're worth 100x less. - type: CanEscapeInventory # Carrying system from nyanotrasen. - - type: LanguageSpeaker # This is here so all with no LanguageSpeaker at least spawn with the default languages. + - type: LanguageKnowledge # This is here so even if species doesn't have a defined language, they at least speak GC speaks: - GalacticCommon understands: diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index 5cb3de6f168..de9928f0d67 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -102,7 +102,7 @@ actionPrototype: DionaGibAction allowedStates: - Dead - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RootSpeak diff --git a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml index 7afc5cddd70..a5920bac50b 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml @@ -52,7 +52,7 @@ accent: dwarf - type: Speech speechSounds: Bass - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 7c3f857c001..e00e06279e5 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -17,7 +17,7 @@ - id: FoodMeatHuman amount: 5 - type: PotentialPsionic #Nyano - Summary: makes potentially psionic. - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml index 39aa0ab8dea..192efcd256f 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml @@ -23,7 +23,7 @@ accent: zombieMoth - type: Speech speechVerb: Moth - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Moffic diff --git a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml index bdea4499ed1..35f9e9fa393 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml @@ -59,7 +59,7 @@ types: Heat : 1.5 #per second, scales with temperature & other constants - type: Wagging - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Draconic diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index a601010ef94..6c3dc952957 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -74,7 +74,7 @@ types: Asphyxiation: -1.0 maxSaturation: 15 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Bubblish diff --git a/Resources/Prototypes/Entities/Mobs/base.yml b/Resources/Prototypes/Entities/Mobs/base.yml index ac9aabbeadb..faa77022aac 100644 --- a/Resources/Prototypes/Entities/Mobs/base.yml +++ b/Resources/Prototypes/Entities/Mobs/base.yml @@ -43,6 +43,7 @@ - type: MovementSpeedModifier - type: Polymorphable - type: StatusIcon + - type: LanguageSpeaker # Einstein Engines. This component is required to support speech, although it does not define known languages. # Used for mobs that have health and can take damage. - type: entity diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 6efa5a63711..97182915c24 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -101,7 +101,7 @@ price: 100 - type: Appearance - type: WiresVisuals - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RobotTalk diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml index 8a0e750abd6..0f652c90fad 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml @@ -35,7 +35,7 @@ - MobLayer - type: Stamina critThreshold: 115 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Nekomimetic diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml index 2184926b95a..6b78ba0488a 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml @@ -64,7 +64,7 @@ Unsexed: MaleFelinid - type: Felinid - type: NoShoesSilentFootsteps - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon From 44b625f283363a59344f43096a0ee68dc4ab2716 Mon Sep 17 00:00:00 2001 From: fox Date: Sat, 15 Jun 2024 17:36:50 +0300 Subject: [PATCH 09/17] Move LanguagePrototype.Replacement to ObfuscationMethod and remove ObfuscationMethod's dependency on LanguagePrototype --- Content.Shared/Language/LanguagePrototype.cs | 6 - Content.Shared/Language/ObfuscationMethods.cs | 38 +- .../Language/Systems/SharedLanguageSystem.cs | 2 +- Resources/Prototypes/Language/languages.yml | 764 +++++++++--------- 4 files changed, 405 insertions(+), 405 deletions(-) diff --git a/Content.Shared/Language/LanguagePrototype.cs b/Content.Shared/Language/LanguagePrototype.cs index 15557739f7d..9342c07e91f 100644 --- a/Content.Shared/Language/LanguagePrototype.cs +++ b/Content.Shared/Language/LanguagePrototype.cs @@ -14,12 +14,6 @@ public sealed class LanguagePrototype : IPrototype [DataField("obfuscation")] public ObfuscationMethod Obfuscation = ObfuscationMethod.Default; - /// - /// A list of replacement phrases used in - /// - [DataField(required: true)] - public List Replacement = []; - #region utility /// /// The in-world name of this language, localized. diff --git a/Content.Shared/Language/ObfuscationMethods.cs b/Content.Shared/Language/ObfuscationMethods.cs index 5925279bd78..7bd2a17542b 100644 --- a/Content.Shared/Language/ObfuscationMethods.cs +++ b/Content.Shared/Language/ObfuscationMethods.cs @@ -8,30 +8,36 @@ public abstract partial class ObfuscationMethod { public static readonly ObfuscationMethod Default = new ReplacementObfuscation(); - internal abstract void Obfuscate(StringBuilder builder, string message, LanguagePrototype language, SharedLanguageSystem context); + 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, LanguagePrototype language) + public string Obfuscate(string message) { var builder = new StringBuilder(); - Obfuscate(builder, message, language, IoCManager.Resolve().GetEntitySystem()); + 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. +/// Similar to ReplacementAccent. Base for all replacement-based obfuscation methods. /// -public sealed partial class ReplacementObfuscation : ObfuscationMethod +public partial class ReplacementObfuscation : ObfuscationMethod { - internal override void Obfuscate(StringBuilder builder, string message, LanguagePrototype language, SharedLanguageSystem context) + /// + /// 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(0, 0, language.Replacement.Count - 1); - builder.Append(language.Replacement[idx]); + var idx = context.PseudoRandomNumber(0, 0, Replacement.Count - 1); + builder.Append(Replacement[idx]); } } @@ -43,7 +49,7 @@ internal override void Obfuscate(StringBuilder builder, string message, Language /// 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 : ObfuscationMethod +public sealed partial class SyllableObfuscation : ReplacementObfuscation { [DataField] public int MinSyllables = 1; @@ -51,7 +57,7 @@ public sealed partial class SyllableObfuscation : ObfuscationMethod [DataField] public int MaxSyllables = 4; - internal override void Obfuscate(StringBuilder builder, string message, LanguagePrototype language, SharedLanguageSystem context) + 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 @@ -77,8 +83,8 @@ internal override void Obfuscate(StringBuilder builder, string message, Language for (var j = 0; j < newWordLength; j++) { - var index = context.PseudoRandomNumber(hashCode + j, 0, language.Replacement.Count - 1); - builder.Append(language.Replacement[index]); + var index = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); + builder.Append(Replacement[index]); } } @@ -102,7 +108,7 @@ private static bool IsPunctuation(char ch) /// 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 : ObfuscationMethod +public sealed partial class PhraseObfuscation : ReplacementObfuscation { [DataField] public int MinPhrases = 1; @@ -127,7 +133,7 @@ public sealed partial class PhraseObfuscation : ObfuscationMethod [DataField] public float Proportion = 1f / 3; - internal override void Obfuscate(StringBuilder builder, string message, LanguagePrototype language, SharedLanguageSystem context) + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) { var sentenceBeginIndex = 0; var hashCode = 0; @@ -148,8 +154,8 @@ internal override void Obfuscate(StringBuilder builder, string message, Language for (var j = 0; j < newLength; j++) { - var phraseIdx = context.PseudoRandomNumber(hashCode + j, 0, language.Replacement.Count - 1); - var phrase = language.Replacement[phraseIdx]; + var phraseIdx = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); + var phrase = Replacement[phraseIdx]; builder.Append(phrase); builder.Append(Separator); } diff --git a/Content.Shared/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/Language/Systems/SharedLanguageSystem.cs index b9ea6869ee0..0a03086ebe1 100644 --- a/Content.Shared/Language/Systems/SharedLanguageSystem.cs +++ b/Content.Shared/Language/Systems/SharedLanguageSystem.cs @@ -44,7 +44,7 @@ public string ObfuscateSpeech(string message, LanguagePrototype language) { var builder = new StringBuilder(); var method = language.Obfuscation; - method.Obfuscate(builder, message, language, this); + method.Obfuscate(builder, message, this); return builder.ToString(); } diff --git a/Resources/Prototypes/Language/languages.yml b/Resources/Prototypes/Language/languages.yml index 7b81f9ad050..fabcaad08eb 100644 --- a/Resources/Prototypes/Language/languages.yml +++ b/Resources/Prototypes/Language/languages.yml @@ -4,8 +4,8 @@ id: Universal obfuscation: !type:ReplacementObfuscation # Should never be used anyway - replacement: - - "*incomprehensible*" + replacement: + - "*incomprehensible*" # The common galactic tongue. - type: language @@ -14,26 +14,26 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 3 - replacement: - - blah - - blah - - blah - - dingle-doingle - - dingle - - dangle - - jibber-jabber - - jubber - - bleh - - zippity - - zoop - - wibble - - wobble - - wiggle - - yada - - meh - - neh - - nah - - wah + replacement: + - blah + - blah + - blah + - dingle-doingle + - dingle + - dangle + - jibber-jabber + - jubber + - bleh + - zippity + - zoop + - wibble + - wobble + - wiggle + - yada + - meh + - neh + - nah + - wah # Spoken by slimes. - type: language @@ -42,12 +42,12 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 3 - replacement: - - blob - - plop - - pop - - bop - - boop + replacement: + - blob + - plop + - pop + - bop + - boop # Spoken by moths. - type: language @@ -56,64 +56,64 @@ !type:SyllableObfuscation minSyllables: 2 # Replacements are really short maxSyllables: 4 - replacement: - - år - - i - - går - - sek - - mo - - ff - - ok - - gj - - ø - - gå - - la - - le - - lit - - ygg - - van - - dår - - næ - - møt - - idd - - hvo - - ja - - på - - han - - så - - ån - - det - - att - - nå - - gö - - bra - - int - - tyc - - om - - när - - två - - må - - dag - - sjä - - vii - - vuo - - eil - - tun - - käyt - - teh - - vä - - hei - - huo - - suo - - ää - - ten - - ja - - heu - - stu - - uhr - - kön - - we - - hön + replacement: + - år + - i + - går + - sek + - mo + - ff + - ok + - gj + - ø + - gå + - la + - le + - lit + - ygg + - van + - dår + - næ + - møt + - idd + - hvo + - ja + - på + - han + - så + - ån + - det + - att + - nå + - gö + - bra + - int + - tyc + - om + - när + - två + - må + - dag + - sjä + - vii + - vuo + - eil + - tun + - käyt + - teh + - vä + - hei + - huo + - suo + - ää + - ten + - ja + - heu + - stu + - uhr + - kön + - we + - hön # Spoken by dionas. - type: language @@ -122,12 +122,12 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 5 - replacement: - - hs - - zt - - kr - - st - - sh + replacement: + - hs + - zt + - kr + - st + - sh # A mess of broken Japanese, spoken by Felinds and Oni - type: language @@ -136,55 +136,55 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 3 # May be too long even, we'll see. - replacement: - - neko - - nyan - - mimi - - moe - - mofu - - fuwa - - kyaa - - kawaii - - poka - - munya - - puni - - munyu - - ufufu - - icha - - doki - - kyun - - kusu - - nya - - nyaa - - desu - - kis - - ama - - chuu - - baka - - hewo - - boop - - gato - - kit - - sune - - yori - - sou - - baka - - chan - - san - - kun - - mahou - - yatta - - suki - - usagi - - domo - - ori - - uwa - - zaazaa - - shiku - - puru - - ira - - heto - - etto + replacement: + - neko + - nyan + - mimi + - moe + - mofu + - fuwa + - kyaa + - kawaii + - poka + - munya + - puni + - munyu + - ufufu + - icha + - doki + - kyun + - kusu + - nya + - nyaa + - desu + - kis + - ama + - chuu + - baka + - hewo + - boop + - gato + - kit + - sune + - yori + - sou + - baka + - chan + - san + - kun + - mahou + - yatta + - suki + - usagi + - domo + - ori + - uwa + - zaazaa + - shiku + - puru + - ira + - heto + - etto # Spoken by the Lizard race. - type: language @@ -193,91 +193,91 @@ !type:SyllableObfuscation minSyllables: 2 maxSyllables: 4 - replacement: - - za - - az - - ze - - ez - - zi - - iz - - zo - - oz - - zu - - uz - - zs - - sz - - ha - - ah - - he - - eh - - hi - - ih - - ho - - oh - - hu - - uh - - hs - - sh - - la - - al - - le - - el - - li - - il - - lo - - ol - - lu - - ul - - ls - - sl - - ka - - ak - - ke - - ek - - ki - - ik - - ko - - ok - - ku - - uk - - ks - - sk - - sa - - as - - se - - es - - si - - is - - so - - os - - su - - us - - ss - - ss - - ra - - ar - - re - - er - - ri - - ir - - ro - - or - - ru - - ur - - rs - - sr - - a - - a - - e - - e - - i - - i - - o - - o - - u - - u - - s - - s + replacement: + - za + - az + - ze + - ez + - zi + - iz + - zo + - oz + - zu + - uz + - zs + - sz + - ha + - ah + - he + - eh + - hi + - ih + - ho + - oh + - hu + - uh + - hs + - sh + - la + - al + - le + - el + - li + - il + - lo + - ol + - lu + - ul + - ls + - sl + - ka + - ak + - ke + - ek + - ki + - ik + - ko + - ok + - ku + - uk + - ks + - sk + - sa + - as + - se + - es + - si + - is + - so + - os + - su + - us + - ss + - ss + - ra + - ar + - re + - er + - ri + - ir + - ro + - or + - ru + - ur + - rs + - sr + - a + - a + - e + - e + - i + - i + - o + - o + - u + - u + - s + - s # Spoken by the Vulpkanin race. - type: language @@ -286,65 +286,65 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 4 - replacement: - - rur - - ya - - cen - - rawr - - bar - - kuk - - tek - - qat - - uk - - wu - - vuh - - tah - - tch - - schz - - auch - - ist - - ein - - entch - - zwichs - - tut - - mir - - wo - - bis - - es - - vor - - nic - - gro -# - lll - - enem - - zandt - - tzch - - noch - - hel - - ischt - - far - - wa - - baram - - iereng - - tech - - lach - - sam - - mak - - lich - - gen - - or - - ag - - eck - - gec - - stag - - onn - - bin - - ket - - jarl - - vulf - - einech - - cresthz - - azunein - - ghzth + replacement: + - rur + - ya + - cen + - rawr + - bar + - kuk + - tek + - qat + - uk + - wu + - vuh + - tah + - tch + - schz + - auch + - ist + - ein + - entch + - zwichs + - tut + - mir + - wo + - bis + - es + - vor + - nic + - gro + # - lll + - enem + - zandt + - tzch + - noch + - hel + - ischt + - far + - wa + - baram + - iereng + - tech + - lach + - sam + - mak + - lich + - gen + - or + - ag + - eck + - gec + - stag + - onn + - bin + - ket + - jarl + - vulf + - einech + - cresthz + - azunein + - ghzth # The common language of the Sol system. - type: language @@ -353,24 +353,24 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 4 - replacement: - - tao - - shi - - tzu - - yi - - com - - be - - is - - i - - op - - vi - - ed - - lec - - mo - - cle - - te - - dis - - e + replacement: + - tao + - shi + - tzu + - yi + - com + - be + - is + - i + - op + - vi + - ed + - lec + - mo + - cle + - te + - dis + - e - type: language id: RobotTalk @@ -378,9 +378,9 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 10 # Crazy - replacement: - - 0 - - 1 + replacement: + - 0 + - 1 # Languages spoken by various critters. - type: language @@ -389,11 +389,11 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 2 - replacement: - - murr - - meow - - purr - - mrow + replacement: + - murr + - meow + - purr + - mrow - type: language id: Dog @@ -401,13 +401,13 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 2 - replacement: - - woof - - bark - - ruff - - bork - - raff - - garr + replacement: + - woof + - bark + - ruff + - bork + - raff + - garr - type: language id: Fox @@ -415,13 +415,13 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 2 - replacement: - - ruff - - raff - - garr - - yip - - yap - - myah + replacement: + - ruff + - raff + - garr + - yip + - yap + - myah - type: language id: Xeno @@ -429,9 +429,9 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 8 # I was crazy once - replacement: - - s - - S + replacement: + - s + - S - type: language id: Monkey @@ -439,9 +439,9 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 8 # They locked me in a room... - replacement: - - o - - k + replacement: + - o + - k - type: language id: Mouse @@ -449,15 +449,15 @@ !type:SyllableObfuscation minSyllables: 2 maxSyllables: 3 - replacement: - - squ - - eak - - pi - - ep - - chuu - - ee - - fwi - - he + replacement: + - squ + - eak + - pi + - ep + - chuu + - ee + - fwi + - he - type: language id: Chicken @@ -465,10 +465,10 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 3 - replacement: - - co - - coo - - ot + replacement: + - co + - coo + - ot - type: language id: Duck @@ -476,10 +476,10 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 3 - replacement: - - qu - - ack - - quack + replacement: + - qu + - ack + - quack - type: language id: Cow @@ -487,9 +487,9 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 3 - replacement: - - moo - - mooo + replacement: + - moo + - mooo - type: language id: Sheep @@ -497,10 +497,10 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 3 - replacement: - - ba - - baa - - aa + replacement: + - ba + - baa + - aa - type: language id: Kangaroo @@ -508,11 +508,11 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 3 - replacement: - - shre - - ack - - chuu - - choo + replacement: + - shre + - ack + - chuu + - choo - type: language id: Pig @@ -520,8 +520,8 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 3 - replacement: - - oink # Please someone come up with something better + replacement: + - oink # Please someone come up with something better - type: language id: Crab @@ -529,14 +529,14 @@ !type:SyllableObfuscation minSyllables: 1 maxSyllables: 3 - replacement: - - click - - clack - - ti - - pi - - tap - - cli - - ick + replacement: + - click + - clack + - ti + - pi + - tap + - cli + - ick - type: language id: Kobold @@ -544,15 +544,15 @@ !type:SyllableObfuscation minSyllables: 2 maxSyllables: 4 - replacement: - - yip - - yap - - gar - - grr - - ar - - scre - - et - - gronk - - hiss - - ss - - ee + replacement: + - yip + - yap + - gar + - grr + - ar + - scre + - et + - gronk + - hiss + - ss + - ee From 2f05074008dec5ab770bdbb72eb648487ad9903a Mon Sep 17 00:00:00 2001 From: fox Date: Tue, 18 Jun 2024 18:14:27 +0300 Subject: [PATCH 10/17] Do not log missing LanguageSpeakerComponents --- Content.Server/Language/LanguageSystem.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Content.Server/Language/LanguageSystem.cs b/Content.Server/Language/LanguageSystem.cs index 2774f28374e..e68489e9e28 100644 --- a/Content.Server/Language/LanguageSystem.cs +++ b/Content.Server/Language/LanguageSystem.cs @@ -26,7 +26,7 @@ public bool CanUnderstand(EntityUid listener, string language, LanguageSpeakerCo if (language == UniversalPrototype || HasComp(listener)) return true; - if (!Resolve(listener, ref component)) + if (!Resolve(listener, ref component, logMissing: false)) return false; return component.UnderstoodLanguages.Contains(language); @@ -37,7 +37,7 @@ public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponen if (HasComp(speaker)) return true; - if (!Resolve(speaker, ref component)) + if (!Resolve(speaker, ref component, logMissing: false)) return false; return component.SpokenLanguages.Contains(language); @@ -48,7 +48,7 @@ public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponen /// public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? component = null) { - if (HasComp(speaker) || !Resolve(speaker, ref component)) + if (HasComp(speaker) || !Resolve(speaker, ref component, logMissing: false)) return Universal; // Serves both as a fallback and uhhh something (TODO: fix this comment) if (string.IsNullOrEmpty(component.CurrentLanguage) || !_prototype.TryIndex(component.CurrentLanguage, out var proto)) @@ -152,7 +152,7 @@ public void RemoveLanguage( /// True if the current language was modified, false otherwise. public bool EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) { - if (comp == null && !TryComp(entity, out comp)) + if (!Resolve(entity, ref comp)) return false; if (!comp.SpokenLanguages.Contains(comp.CurrentLanguage)) From 5a2dc1394761de821a4856f629ca0f9b817bca13 Mon Sep 17 00:00:00 2001 From: fox Date: Tue, 18 Jun 2024 18:18:58 +0300 Subject: [PATCH 11/17] Rewrite translator implants to use the existing implant infrastructure --- .../Systems/TranslatorImplanterSystem.cs | 8 - .../Language/TranslatorImplantSystem.cs | 66 +++++++ .../Language/TranslatorImplanterSystem.cs | 75 -------- Content.Server/Language/TranslatorSystem.cs | 52 ++---- .../Components/TranslatorImplantComponent.cs | 21 +++ .../TranslatorImplanterComponent.cs | 35 ---- .../Translators/BaseTranslatorComponent.cs | 9 - .../SharedTranslatorImplanterSystem.cs | 36 ---- .../Objects/Devices/translator_implants.yml | 168 +++++++++--------- .../Objects/Misc/translator_implanters.yml | 77 ++++++++ 10 files changed, 264 insertions(+), 283 deletions(-) delete mode 100644 Content.Client/Language/Systems/TranslatorImplanterSystem.cs create mode 100644 Content.Server/Language/TranslatorImplantSystem.cs delete mode 100644 Content.Server/Language/TranslatorImplanterSystem.cs create mode 100644 Content.Shared/Language/Components/TranslatorImplantComponent.cs delete mode 100644 Content.Shared/Language/Components/TranslatorImplanterComponent.cs delete mode 100644 Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs create mode 100644 Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml diff --git a/Content.Client/Language/Systems/TranslatorImplanterSystem.cs b/Content.Client/Language/Systems/TranslatorImplanterSystem.cs deleted file mode 100644 index da19b3decf9..00000000000 --- a/Content.Client/Language/Systems/TranslatorImplanterSystem.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Content.Shared.Language.Systems; - -namespace Content.Client.Language.Systems; - -public sealed class TranslatorImplanterSystem : SharedTranslatorImplanterSystem -{ - -} diff --git a/Content.Server/Language/TranslatorImplantSystem.cs b/Content.Server/Language/TranslatorImplantSystem.cs new file mode 100644 index 00000000000..4d58144481d --- /dev/null +++ b/Content.Server/Language/TranslatorImplantSystem.cs @@ -0,0 +1,66 @@ +using Content.Shared.Implants.Components; +using Content.Shared.Language; +using Content.Shared.Language.Components; +using Robust.Shared.Containers; + +namespace Content.Server.Language; + +public sealed class TranslatorImplantSystem : EntitySystem +{ + [Dependency] private readonly LanguageSystem _language = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnImplant); + SubscribeLocalEvent(OnDeImplant); + SubscribeLocalEvent(OnDetermineLanguages); + } + + private void OnImplant(EntityUid uid, TranslatorImplantComponent component, EntGotInsertedIntoContainerMessage args) + { + if (args.Container.ID != ImplanterComponent.ImplantSlotId) + return; + + var implantee = Transform(uid).ParentUid; + if (implantee is not { Valid: true } || !TryComp(implantee, out var knowledge)) + return; + + component.Enabled = true; + // To operate an implant, you need to know its required language intrinsically, because like... it connects to your brain or something. + // So external translators or other implants can't help you operate it. + component.SpokenRequirementSatisfied = TranslatorSystem.CheckLanguagesMatch( + component.RequiredLanguages, knowledge.SpokenLanguages, component.RequiresAllLanguages); + + component.UnderstoodRequirementSatisfied = TranslatorSystem.CheckLanguagesMatch( + component.RequiredLanguages, knowledge.UnderstoodLanguages, component.RequiresAllLanguages); + + _language.UpdateEntityLanguages(implantee); + } + + private void OnDeImplant(EntityUid uid, TranslatorImplantComponent component, EntGotRemovedFromContainerMessage args) + { + // Even though the description of this event says it gets raised BEFORE reparenting, that's actually false... + component.Enabled = component.SpokenRequirementSatisfied = component.UnderstoodRequirementSatisfied = false; + + if (TryComp(uid, out var subdermal) && subdermal.ImplantedEntity is { Valid: true} implantee) + _language.UpdateEntityLanguages(implantee); + } + + private void OnDetermineLanguages(EntityUid uid, ImplantedComponent component, ref DetermineEntityLanguagesEvent args) + { + // TODO: might wanna find a better solution, i just can't come up with something viable + foreach (var implant in component.ImplantContainer.ContainedEntities) + { + if (!TryComp(implant, out var translator) || !translator.Enabled) + continue; + + if (translator.SpokenRequirementSatisfied) + foreach (var language in translator.SpokenLanguages) + args.SpokenLanguages.Add(language); + + if (translator.UnderstoodRequirementSatisfied) + foreach (var language in translator.UnderstoodLanguages) + args.UnderstoodLanguages.Add(language); + } + } +} diff --git a/Content.Server/Language/TranslatorImplanterSystem.cs b/Content.Server/Language/TranslatorImplanterSystem.cs deleted file mode 100644 index 0d67bbb1279..00000000000 --- a/Content.Server/Language/TranslatorImplanterSystem.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Linq; -using Content.Server.Administration.Logs; -using Content.Server.Language.Events; -using Content.Server.Popups; -using Content.Shared.Database; -using Content.Shared.Interaction; -using Content.Shared.Language; -using Content.Shared.Language.Components; -using Content.Shared.Language.Events; -using Content.Shared.Language.Systems; -using Content.Shared.Mobs.Components; -using Content.Shared.Language.Components.Translators; - -namespace Content.Server.Language; - -// TODO: pending rewrite -public sealed class TranslatorImplanterSystem : SharedTranslatorImplanterSystem -{ - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly LanguageSystem _language = default!; - - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnImplant); - } - - - private void OnImplant(EntityUid implanter, TranslatorImplanterComponent component, AfterInteractEvent args) - { - if (component.Used || !args.CanReach || args.Target is not { Valid: true } target) - return; - - if (!TryComp(target, out var speaker)) - return; - - // TODO: yes I know this should use whitelist comp, yes this will be changed after rewrite - if (component.MobsOnly && !HasComp(target)) - { - _popup.PopupEntity("translator-implanter-refuse", component.Owner); - return; - } - - var understood = speaker.UnderstoodLanguages; - if (component.RequiredLanguages.Count > 0 && !component.RequiredLanguages.Any(lang => understood.Contains(lang))) - { - _popup.PopupEntity(Loc.GetString("translator-implanter-refuse", - ("implanter", implanter), ("target", target)), implanter); - return; - } - - var intrinsic = EnsureComp(target); - intrinsic.Enabled = true; - - foreach (var lang in component.SpokenLanguages.Where(lang => !intrinsic.SpokenLanguages.Contains(lang))) - intrinsic.SpokenLanguages.Add(lang); - - foreach (var lang in component.UnderstoodLanguages.Where(lang => !intrinsic.UnderstoodLanguages.Contains(lang))) - intrinsic.UnderstoodLanguages.Add(lang); - - component.Used = true; - _popup.PopupEntity(Loc.GetString("translator-implanter-success", - ("implanter", implanter), ("target", target)), implanter); - - _adminLogger.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.User):player} used {ToPrettyString(implanter):implanter} to give {ToPrettyString(target):target} the following languages:" - + $"\nSpoken: {string.Join(", ", component.SpokenLanguages)}; Understood: {string.Join(", ", component.UnderstoodLanguages)}"); - - OnAppearanceChange(implanter, component); - RaiseLocalEvent(target, new LanguagesUpdateEvent(), true); - } -} diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index b2166b44f13..5e1fc0ce227 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -13,6 +13,8 @@ namespace Content.Server.Language; +// NOTE FOR SELF: MAKE SURE LANGUAGE SWITCHING AFTER EQUIPPING A HANDHELD WORKS + // This does not support holding multiple translators at once. // That shouldn't be an issue for now, but it needs to be fixed later. public sealed class TranslatorSystem : SharedTranslatorSystem @@ -38,7 +40,7 @@ public override void Initialize() private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent component, DetermineEntityLanguagesEvent ev) { - if (!component.Enabled) + if (!component.Enabled || !TryComp(uid, out var speaker)) return; if (!_powerCell.HasActivatableCharge(uid)) @@ -49,37 +51,8 @@ private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent co // The translator has a limited number of languages it can translate to and translate from. // If the wielder understands the language of the translator, they will be able to understand translations provided by it // If the wielder also speaks that language, they will be able to use it to translate their own speech by "speaking" in that language - var addUnderstood = true; - var addSpoken = true; - if (component.RequiredLanguages.Count > 0) - { - if (component.RequiresAllLanguages) - { - // Add langs when the wielder has all of the required languages - foreach (var language in component.RequiredLanguages) - { - if (!ev.SpokenLanguages.Contains(language)) - addSpoken = false; - - if (!ev.UnderstoodLanguages.Contains(language)) - addUnderstood = false; - } - } - else - { - // Add langs when the wielder has at least one of the required languages - addUnderstood = false; - addSpoken = false; - foreach (var language in component.RequiredLanguages) - { - if (ev.SpokenLanguages.Contains(language)) - addSpoken = true; - - if (ev.UnderstoodLanguages.Contains(language)) - addUnderstood = true; - } - } - } + var addSpoken = CheckLanguagesMatch(component.RequiredLanguages, speaker.SpokenLanguages, component.RequiresAllLanguages); + var addUnderstood = CheckLanguagesMatch(component.RequiredLanguages, speaker.UnderstoodLanguages, component.RequiresAllLanguages); if (addSpoken) foreach (var language in component.SpokenLanguages) @@ -206,16 +179,27 @@ private void UpdateBoundIntrinsicComp(HandheldTranslatorComponent comp, HoldsTra { intrinsic.SpokenLanguages = [..comp.SpokenLanguages]; intrinsic.UnderstoodLanguages = [..comp.UnderstoodLanguages]; - intrinsic.DefaultLanguageOverride = comp.DefaultLanguageOverride; } else { intrinsic.SpokenLanguages.Clear(); intrinsic.UnderstoodLanguages.Clear(); - intrinsic.DefaultLanguageOverride = null; } intrinsic.Enabled = isEnabled; intrinsic.Issuer = comp; } + + /// + /// Checks whether any OR all required languages are provided. Used for utility purposes. + /// + public static bool CheckLanguagesMatch(ICollection required, ICollection provided, bool requireAll) + { + if (required.Count == 0) + return true; + + return requireAll + ? required.All(provided.Contains) + : required.Any(provided.Contains); + } } diff --git a/Content.Shared/Language/Components/TranslatorImplantComponent.cs b/Content.Shared/Language/Components/TranslatorImplantComponent.cs new file mode 100644 index 00000000000..cb8c666c82f --- /dev/null +++ b/Content.Shared/Language/Components/TranslatorImplantComponent.cs @@ -0,0 +1,21 @@ +using Content.Shared.Language.Components.Translators; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.Language.Components; + +/// +/// An implant that allows the implantee to speak and understand other languages. +/// +[RegisterComponent] +public sealed partial class TranslatorImplantComponent : BaseTranslatorComponent +{ + /// + /// Whether the implantee knows the languages necessary to speak using this implant. + /// + public bool SpokenRequirementSatisfied = false; + + /// + /// Whether the implantee knows the languages necessary to understand translations of this implant. + /// + public bool UnderstoodRequirementSatisfied = false; +} diff --git a/Content.Shared/Language/Components/TranslatorImplanterComponent.cs b/Content.Shared/Language/Components/TranslatorImplanterComponent.cs deleted file mode 100644 index 401e8a8b8aa..00000000000 --- a/Content.Shared/Language/Components/TranslatorImplanterComponent.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Shared.Language.Components; - -/// -/// An item that, when used on a mob, adds an intrinsic translator to it. -/// -[RegisterComponent] -public sealed partial class TranslatorImplanterComponent : Component -{ - [DataField("spoken", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List SpokenLanguages = new(); - - [DataField("understood", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List UnderstoodLanguages = new(); - - /// - /// The list of languages the mob must understand in order for this translator to have effect. - /// Knowing one language is enough. - /// - [DataField("requires", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List RequiredLanguages = new(); - - /// - /// If true, only allows to use this implanter on mobs. - /// - [DataField] - public bool MobsOnly = true; - - /// - /// Whether this implant has been used already. - /// - [DataField] - public bool Used = false; -} diff --git a/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs b/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs index a66c9be082e..072480031d5 100644 --- a/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs +++ b/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs @@ -4,15 +4,6 @@ namespace Content.Shared.Language.Components.Translators; public abstract partial class BaseTranslatorComponent : Component { - // TODO may need to be removed completely, it's a part of legacy code that never ended up being used. - /// - /// The language this translator changes the speaker's language to when they don't specify one. - /// If null, does not modify the default language. - /// - [DataField("defaultLanguage")] - [ViewVariables(VVAccess.ReadWrite)] - public string? DefaultLanguageOverride = null; - /// /// The list of additional languages this translator allows the wielder to speak. /// diff --git a/Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs b/Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs deleted file mode 100644 index a13225378cd..00000000000 --- a/Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Shared.Examine; -using Content.Shared.Implants.Components; -using Content.Shared.Language.Components; -using Robust.Shared.Serialization; - -namespace Content.Shared.Language.Systems; - -public abstract class SharedTranslatorImplanterSystem : EntitySystem -{ - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnExamined); - } - - private void OnExamined(EntityUid uid, TranslatorImplanterComponent component, ExaminedEvent args) - { - if (!args.IsInDetailsRange) - return; - - var text = !component.Used - ? Loc.GetString("translator-implanter-ready") - : Loc.GetString("translator-implanter-used"); - - args.PushText(text); - } - - protected void OnAppearanceChange(EntityUid implanter, TranslatorImplanterComponent component) - { - var used = component.Used; - _appearance.SetData(implanter, ImplanterVisuals.Full, !used); - } -} diff --git a/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml b/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml index fc947efe9a3..da42b2774b1 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml @@ -1,132 +1,128 @@ - type: entity - abstract: true - id: BaseTranslatorImplanter - parent: [ BaseItem ] - name: basic translator implant - description: Translates speech. + parent: BaseSubdermalImplant + id: BasicGalacticCommonTranslatorImplant + name: basic common translator implant + description: Provides your illiterate friends the ability to understand the common galactic tongue. + noSpawn: true components: - - type: Sprite - sprite: Objects/Specific/Medical/implanter.rsi - state: implanter0 - layers: - - state: implanter1 - map: [ "implantFull" ] - visible: true - - state: implanter0 - map: [ "implantBroken" ] - - type: Appearance - - type: GenericVisualizer - visuals: - enum.ImplanterVisuals.Full: - implantFull: - True: {visible: true} - False: {visible: false} - implantBroken: - True: {visible: false} - False: {visible: true} - -- type: entity - id: BasicGalaticCommonTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: basic Galactic Common translator implant - description: An implant giving the ability to understand Galactic Common. - components: - - type: TranslatorImplanter + - type: TranslatorImplant understood: - GalacticCommon - type: entity - id: AdvancedGalaticCommonTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: advanced Galactic Common translator implant - description: An implant giving the ability to understand and speak Galactic Common. + parent: BaseSubdermalImplant + id: GalacticCommonTranslatorImplant + name: advanced common translator implant + description: A more advanced version of the translator implant, teaches your illiterate friends the ability to both speak and understand the galactic tongue! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - GalacticCommon + - type: TranslatorImplant understood: - GalacticCommon + spoken: + - GalacticCommon - type: entity - id: BubblishTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Bubblish translator implant - description: An implant giving the ability to understand and speak Bubblish. + parent: BaseSubdermalImplant + id: BubblishTranslatorImplant + name: bubblish translator implant + description: An implant that helps you speak and understand the language of slimes! Special vocal chords not included. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Bubblish + - type: TranslatorImplant understood: - Bubblish + spoken: + - Bubblish + requires: + - GalacticCommon - type: entity - id: NekomimeticTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Nekomimetic translator implant - description: An implant giving the ability to understand and speak Nekomimetic. Nya~! + parent: BaseSubdermalImplant + id: NekomimeticTranslatorImplant + name: nekomimetic translator implant + description: A translator implant intially designed to help domestic cat owners understand their pets, now granting the ability to understand and speak to Felinids! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Nekomimetic + - type: TranslatorImplant understood: - Nekomimetic + spoken: + - Nekomimetic + requires: + - GalacticCommon - type: entity - id: DraconicTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Draconic translator implant - description: An implant giving the ability to understand and speak Draconic. + parent: BaseSubdermalImplant + id: DraconicTranslatorImplant + name: draconic translator implant + description: A translator implant giving the ability to speak to dragons! Subsequently, also allows to communicate with the Unathi. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Draconic + - type: TranslatorImplant understood: - Draconic + spoken: + - Draconic + requires: + - GalacticCommon - type: entity - id: CanilunztTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Canilunzt translator implant - description: An implant giving the ability to understand and speak Canilunzt. Yeeps! + parent: BaseSubdermalImplant + id: CanilunztTranslatorImplant + name: canilunzt translator implant + description: A translator implant that helps you communicate with your local yeepers. Yeep! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Canilunzt + - type: TranslatorImplant understood: - Canilunzt + spoken: + - Canilunzt + requires: + - GalacticCommon - type: entity - id: SolCommonTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: SolCommon translator implant + parent: BaseSubdermalImplant + id: SolCommonTranslatorImplant + name: sol-common translator implant description: An implant giving the ability to understand and speak SolCommon. Raaagh! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - SolCommon + - type: TranslatorImplant understood: - SolCommon + spoken: + - SolCommon + requires: + - GalacticCommon - type: entity - id: RootSpeakTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: RootSpeak translator implant - description: An implant giving the ability to understand and speak RootSpeak. + parent: BaseSubdermalImplant + id: RootSpeakTranslatorImplant + name: root-speak translator implant + description: An implant that lets you speak for the trees. Or to the trees. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - RootSpeak + - type: TranslatorImplant understood: - RootSpeak + spoken: + - RootSpeak + requires: + - GalacticCommon - type: entity - id: MofficTranslatorImplanter - parent: [ BaseTranslatorImplanter ] + parent: BaseSubdermalImplant + id: MofficTranslatorImplant name: Moffic translator implant - description: An implant giving the ability to understand and speak Moffic. + description: An implant designed to help domesticate mothroaches. Subsequently, allows you to communicate with the moth people. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Moffic + - type: TranslatorImplant understood: - Moffic + spoken: + - Moffic + requires: + - GalacticCommon diff --git a/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml b/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml new file mode 100644 index 00000000000..8b5b262ff8a --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml @@ -0,0 +1,77 @@ +- type: entity + id: BaseTranslatorImplanter + abstract: true + parent: BaseImplantOnlyImplanter + name: basic translator implanter + +- type: entity + id: BasicGalaticCommonTranslatorImplanter + parent: BaseTranslatorImplanter + name: basic common translator implanter + components: + - type: Implanter + implant: BasicGalacticCommonTranslatorImplant + +- type: entity + id: AdvancedGalaticCommonTranslatorImplanter + parent: BaseTranslatorImplanter + name: advanced common translator implanter + components: + - type: Implanter + implant: GalacticCommonTranslatorImplant + +- type: entity + id: BubblishTranslatorImplanter + parent: BaseTranslatorImplanter + name: bubblish translator implant + components: + - type: Implanter + implant: BubblishTranslatorImplant + +- type: entity + id: NekomimeticTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: nekomimetic translator implant + components: + - type: Implanter + implant: NekomimeticTranslatorImplant + +- type: entity + id: DraconicTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: draconic translator implant + components: + - type: Implanter + implant: DraconicTranslatorImplant + +- type: entity + id: CanilunztTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: canilunzt translator implant + components: + - type: Implanter + implant: CanilunztTranslatorImplant + +- type: entity + id: SolCommonTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: sol-common translator implant + components: + - type: Implanter + implant: SolCommonTranslatorImplant + +- type: entity + id: RootSpeakTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: root-speak translator implant + components: + - type: Implanter + implant: RootSpeakTranslatorImplant + +- type: entity + id: MofficTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: moffic translator implant + components: + - type: Implanter + implant: MofficTranslatorImplant From b3545d00677506c65aea1f2f874b16980f33d49f Mon Sep 17 00:00:00 2001 From: fox Date: Tue, 18 Jun 2024 19:00:45 +0300 Subject: [PATCH 12/17] Update the language menu when choosing a new language --- Content.Client/Language/LanguageMenuWindow.xaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Content.Client/Language/LanguageMenuWindow.xaml.cs b/Content.Client/Language/LanguageMenuWindow.xaml.cs index 0f8bb0976fd..11d1c290d16 100644 --- a/Content.Client/Language/LanguageMenuWindow.xaml.cs +++ b/Content.Client/Language/LanguageMenuWindow.xaml.cs @@ -119,6 +119,7 @@ private void OnLanguageChosen(string id) return; _clientLanguageSystem.RequestSetLanguage(proto); + UpdateState(id, _clientLanguageSystem.SpokenLanguages); } From 734e76453d3dfc1f3472a4ed7561a368f04d0184 Mon Sep 17 00:00:00 2001 From: fox Date: Tue, 18 Jun 2024 19:19:56 +0300 Subject: [PATCH 13/17] Fix handheld translators adjusting your current language --- Content.Server/Language/TranslatorSystem.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index 5e1fc0ce227..0e7897f142f 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -118,15 +118,14 @@ private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponen translatorComp.Enabled = isEnabled; _powerCell.SetPowerCellDrawEnabled(translator, isEnabled); - // Update the current language of the entity if necessary - if (isEnabled && translatorComp.SetLanguageOnInteract) - { - var firstNew = translatorComp.SpokenLanguages.FirstOrDefault(it => !languageComp.SpokenLanguages.Contains(it)); - if (firstNew is {}) - _language.SetLanguage(holder, firstNew, languageComp); - } + // The first new spoken language added by this translator, or null + var firstNewLanguage = translatorComp.SpokenLanguages.FirstOrDefault(it => !languageComp.SpokenLanguages.Contains(it)); _language.UpdateEntityLanguages(holder, languageComp); + + // Update the current language of the entity if necessary + if (isEnabled && translatorComp.SetLanguageOnInteract && firstNewLanguage is {}) + _language.SetLanguage(holder, firstNewLanguage, languageComp); } else { From 31d82fb26e411665f1e53d8249a0d3771f77c7e5 Mon Sep 17 00:00:00 2001 From: fox Date: Tue, 18 Jun 2024 21:06:54 +0300 Subject: [PATCH 14/17] I fixes --- .../Language/LanguageSystem.Networking.cs | 14 ++++++++++---- Content.Server/Language/TranslatorSystem.cs | 2 -- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Content.Server/Language/LanguageSystem.Networking.cs b/Content.Server/Language/LanguageSystem.Networking.cs index 038db9d8fcb..572e2961fde 100644 --- a/Content.Server/Language/LanguageSystem.Networking.cs +++ b/Content.Server/Language/LanguageSystem.Networking.cs @@ -1,6 +1,7 @@ using Content.Server.Language.Events; using Content.Server.Mind; using Content.Shared.Language; +using Content.Shared.Language.Components; using Content.Shared.Language.Events; using Content.Shared.Mind; using Content.Shared.Mind.Components; @@ -60,13 +61,18 @@ private void SendLanguageStateToClient(ICommonSession session, LanguageSpeakerCo SendLanguageStateToClient(entity, session, comp); } + // TODO this is really stupid and can be avoided if we just make everything shared... private void SendLanguageStateToClient(EntityUid uid, ICommonSession session, LanguageSpeakerComponent? component = null) { - if (!Resolve(uid, ref component)) - return; + var isUniversal = HasComp(uid); + if (!isUniversal) + Resolve(uid, ref component, logMissing: false); + + // I really don't want to call 3 getter methods here, so we'll just have this slightly hardcoded solution + var message = isUniversal || component == null + ? new LanguagesUpdatedMessage(UniversalPrototype, [UniversalPrototype], [UniversalPrototype]) + : new LanguagesUpdatedMessage(component.CurrentLanguage, component.SpokenLanguages, component.UnderstoodLanguages); - // TODO this is really stupid and can be avoided if we just make everything shared... - var message = new LanguagesUpdatedMessage(component.CurrentLanguage, component.SpokenLanguages, component.UnderstoodLanguages); RaiseNetworkEvent(message, session); } } diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index 0e7897f142f..5022e540960 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -13,8 +13,6 @@ namespace Content.Server.Language; -// NOTE FOR SELF: MAKE SURE LANGUAGE SWITCHING AFTER EQUIPPING A HANDHELD WORKS - // This does not support holding multiple translators at once. // That shouldn't be an issue for now, but it needs to be fixed later. public sealed class TranslatorSystem : SharedTranslatorSystem From d9d74ded88a7565d55cd436cad8890740831e25a Mon Sep 17 00:00:00 2001 From: fox Date: Thu, 27 Jun 2024 12:45:11 +0300 Subject: [PATCH 15/17] Minor improvements --- Content.Shared/Language/ObfuscationMethods.cs | 14 +++++++++++-- .../Entities/Objects/Devices/translators.yml | 20 +++++++++---------- Resources/Prototypes/Language/languages.yml | 6 +++--- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/Content.Shared/Language/ObfuscationMethods.cs b/Content.Shared/Language/ObfuscationMethods.cs index 7bd2a17542b..51230c47970 100644 --- a/Content.Shared/Language/ObfuscationMethods.cs +++ b/Content.Shared/Language/ObfuscationMethods.cs @@ -6,8 +6,18 @@ namespace Content.Shared.Language; [ImplicitDataDefinitionForInheritors] public abstract partial class ObfuscationMethod { - public static readonly ObfuscationMethod Default = new ReplacementObfuscation(); + /// + /// 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); /// @@ -36,7 +46,7 @@ public partial class ReplacementObfuscation : ObfuscationMethod internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) { - var idx = context.PseudoRandomNumber(0, 0, Replacement.Count - 1); + var idx = context.PseudoRandomNumber(message.GetHashCode(), 0, Replacement.Count - 1); builder.Append(Replacement[idx]); } } diff --git a/Resources/Prototypes/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/Entities/Objects/Devices/translators.yml index 18f2dead687..664626ea4b4 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translators.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translators.yml @@ -1,7 +1,7 @@ - type: entity abstract: true id: TranslatorUnpowered - parent: [ BaseItem ] + parent: BaseItem name: translator description: Translates speech. components: @@ -36,7 +36,7 @@ - type: entity abstract: true id: TranslatorEmpty - parent: [ Translator ] + parent: Translator suffix: Empty components: - type: ItemSlots @@ -49,7 +49,7 @@ id: CanilunztTranslator parent: [ TranslatorEmpty ] name: Canilunzt translator - description: Translates speech between Canilunzt and Galactic Common. + description: Translates speech between Canilunzt and Galactic Common, allowing your local yeepers to communicate with the locals and vice versa! components: - type: HandheldTranslator spoken: @@ -66,7 +66,7 @@ id: BubblishTranslator parent: [ TranslatorEmpty ] name: Bubblish translator - description: Translates speech between Bubblish and Galactic Common. + description: Translates speech between Bubblish and Galactic Common, helping communicate with slimes and slime people. components: - type: HandheldTranslator spoken: @@ -83,7 +83,7 @@ id: NekomimeticTranslator parent: [ TranslatorEmpty ] name: Nekomimetic translator - description: Translates speech between Nekomimetic and Galactic Common. Why would you want that? + description: Translates speech between Nekomimetic and Galactic Common, enabling you to communicate with your pet cats. components: - type: HandheldTranslator spoken: @@ -100,7 +100,7 @@ id: DraconicTranslator parent: [ TranslatorEmpty ] name: Draconic translator - description: Translates speech between Draconic and Galactic Common. + description: Translates speech between Draconic and Galactic Common, making it easier to understand your local Uniathi. components: - type: HandheldTranslator spoken: @@ -134,7 +134,7 @@ id: RootSpeakTranslator parent: [ TranslatorEmpty ] name: RootSpeak translator - description: Translates speech between RootSpeak and Galactic Common. Like a true plant? + description: Translates speech between RootSpeak and Galactic Common. You may now speak for the trees. components: - type: HandheldTranslator spoken: @@ -151,7 +151,7 @@ id: MofficTranslator parent: [ TranslatorEmpty ] name: Moffic translator - description: Translates speech between Moffic and Galactic Common. Like a true moth... or bug? + description: Translates speech between Moffic and Galactic Common, helping you understand the buzzes of your pet mothroach! components: - type: HandheldTranslator spoken: @@ -168,7 +168,7 @@ id: XenoTranslator parent: [ TranslatorEmpty ] name: Xeno translator - description: Translates speech between Xeno and Galactic Common. Not sure if that will help. + description: Translates speech between Xeno and Galactic Common. This will probably not help you survive an encounter, though. components: - type: HandheldTranslator spoken: @@ -184,7 +184,7 @@ id: AnimalTranslator parent: [ TranslatorEmpty ] name: Animal translator - description: Translates all the cutes noises that animals make into a more understandable form! + description: Translates all the cutes noises that most animals make into a more understandable form! components: - type: HandheldTranslator understood: diff --git a/Resources/Prototypes/Language/languages.yml b/Resources/Prototypes/Language/languages.yml index fabcaad08eb..1a874612c2f 100644 --- a/Resources/Prototypes/Language/languages.yml +++ b/Resources/Prototypes/Language/languages.yml @@ -1,11 +1,11 @@ # The universal language, assumed if the entity has a UniversalLanguageSpeakerComponent. -# Do not use otherwise. Try to use the respective component instead of this language. +# Do not use otherwise. Making an entity explicitly understand/speak this language will NOT have the desired effect. - type: language id: Universal obfuscation: - !type:ReplacementObfuscation # Should never be used anyway + !type:ReplacementObfuscation replacement: - - "*incomprehensible*" + - "*incomprehensible*" # Never actually used # The common galactic tongue. - type: language From cc4e86da760b8ad000cbd436a6d7dbd024f96c2d Mon Sep 17 00:00:00 2001 From: FoxxoTrystan Date: Thu, 4 Jul 2024 14:28:01 +0200 Subject: [PATCH 16/17] Markers --- Content.Server/Chat/Systems/ChatSystem.cs | 20 +++++++++++-------- .../Radio/EntitySystems/RadioSystem.cs | 11 +++++----- Content.Shared/Language/LanguagePrototype.cs | 9 +++++++++ .../en-US/chat/managers/chat-manager.ftl | 8 ++++---- .../en-US/headset/headset-component.ftl | 4 ++-- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index a6c2223bc6d..02ce12f5380 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -423,11 +423,11 @@ private void SendEntitySpeak( name = FormattedMessage.EscapeText(name); // The chat message wrapped in a "x says y" string - var wrappedMessage = WrapPublicMessage(source, name, message); + var wrappedMessage = WrapPublicMessage(source, name, message, languageOverride: language); // The chat message obfuscated via language obfuscation var obfuscated = SanitizeInGameICMessage(source, _language.ObfuscateSpeech(message, language), out var emoteStr, true, _configurationManager.GetCVar(CCVars.ChatPunctuation), (!CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Parent.Name == "en") || (CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Name == "en")); // The language-obfuscated message wrapped in a "x says y" string - var wrappedObfuscated = WrapPublicMessage(source, name, obfuscated); + var wrappedObfuscated = WrapPublicMessage(source, name, obfuscated, languageOverride: language); SendInVoiceRange(ChatChannel.Local, name, message, wrappedMessage, obfuscated, wrappedObfuscated, source, range, languageOverride: language); @@ -514,6 +514,7 @@ private void SendEntityWhisper( // Scenario 1: the listener can clearly understand the message result = perceivedMessage; wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message", + ("color", language.Color ?? Color.Gray), ("entityName", name), ("message", FormattedMessage.EscapeText(result))); } @@ -523,13 +524,14 @@ private void SendEntityWhisper( // Collisiongroup.Opaque is not ideal for this use. Preferably, there should be a check specifically with "Can Ent1 see Ent2" in mind result = ObfuscateMessageReadability(perceivedMessage); wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message", - ("entityName", nameIdentity), ("message", FormattedMessage.EscapeText(result))); + ("entityName", nameIdentity), ("color", language.Color ?? Color.Gray), ("message", FormattedMessage.EscapeText(result))); } else { // Scenario 3: If listener is too far and has no line of sight, they can't identify the whisperer's identity result = ObfuscateMessageReadability(perceivedMessage); wrappedMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message", + ("color", language.Color ?? Color.Gray), ("message", FormattedMessage.EscapeText(result))); } @@ -537,6 +539,7 @@ private void SendEntityWhisper( } var replayWrap = Loc.GetString("chat-manager-entity-whisper-wrap-message", + ("color", language.Color ?? Color.Gray), ("entityName", name), ("message", FormattedMessage.EscapeText(message))); _replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, replayWrap, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); @@ -837,15 +840,16 @@ public string SanitizeMessageReplaceWords(string message) /// /// Wraps a message sent by the specified entity into an "x says y" string. /// - public string WrapPublicMessage(EntityUid source, string name, string message) + public string WrapPublicMessage(EntityUid source, string name, string message, LanguagePrototype? languageOverride = null) { + var language = languageOverride ?? _language.GetLanguage(source); var speech = GetSpeechVerb(source, message); - var verbName = Loc.GetString(_random.Pick(speech.SpeechVerbStrings)); return Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message", + ("color", language.Color ?? Color.White), ("entityName", name), - ("verb", verbName), - ("fontType", speech.FontId), - ("fontSize", speech.FontSize), + ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), + ("fontType", language.FontId ?? speech.FontId), + ("fontSize", language.FontSize ?? speech.FontSize), ("message", FormattedMessage.EscapeText(message))); } diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 0e4da856ce6..a013dba9516 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -115,12 +115,12 @@ public void SendRadioMessage(EntityUid messageSource, string message, RadioChann ? FormattedMessage.EscapeText(message) : message; - var wrappedMessage = WrapRadioMessage(messageSource, channel, name, content); + var wrappedMessage = WrapRadioMessage(messageSource, channel, name, content, language); var msg = new ChatMessage(ChatChannel.Radio, content, wrappedMessage, NetEntity.Invalid, null); // ... you guess it var obfuscated = _language.ObfuscateSpeech(content, language); - var obfuscatedWrapped = WrapRadioMessage(messageSource, channel, name, obfuscated); + var obfuscatedWrapped = WrapRadioMessage(messageSource, channel, name, obfuscated, language); var notUdsMsg = new ChatMessage(ChatChannel.Radio, obfuscated, obfuscatedWrapped, NetEntity.Invalid, null); var ev = new RadioReceiveEvent(messageSource, channel, msg, notUdsMsg, language); @@ -173,13 +173,14 @@ public void SendRadioMessage(EntityUid messageSource, string message, RadioChann _messages.Remove(message); } - private string WrapRadioMessage(EntityUid source, RadioChannelPrototype channel, string name, string message) + private string WrapRadioMessage(EntityUid source, RadioChannelPrototype channel, string name, string message, LanguagePrototype language) { var speech = _chat.GetSpeechVerb(source, message); return Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap", ("color", channel.Color), - ("fontType", speech.FontId), - ("fontSize", speech.FontSize), + ("languageColor", language.Color ?? channel.Color), + ("fontType", language.FontId ?? speech.FontId), + ("fontSize", language.FontSize ?? speech.FontSize), ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), ("channel", $"\\[{channel.LocalizedName}\\]"), ("name", name), diff --git a/Content.Shared/Language/LanguagePrototype.cs b/Content.Shared/Language/LanguagePrototype.cs index 9342c07e91f..f5f5627bbb1 100644 --- a/Content.Shared/Language/LanguagePrototype.cs +++ b/Content.Shared/Language/LanguagePrototype.cs @@ -8,6 +8,15 @@ public sealed class LanguagePrototype : IPrototype [IdDataField] public string ID { get; private set; } = default!; + [DataField("color")] + public Color? Color; + + [DataField("fontId")] + public string? FontId; + + [DataField("fontSize")] + public int? FontSize; + /// /// Obfuscation method used by this language. By default, uses /// diff --git a/Resources/Locale/en-US/chat/managers/chat-manager.ftl b/Resources/Locale/en-US/chat/managers/chat-manager.ftl index fab815b4f90..61a18cac31d 100644 --- a/Resources/Locale/en-US/chat/managers/chat-manager.ftl +++ b/Resources/Locale/en-US/chat/managers/chat-manager.ftl @@ -21,11 +21,11 @@ chat-manager-whisper-headset-on-message = You can't whisper on the radio! chat-manager-server-wrap-message = [bold]{$message}[/bold] chat-manager-sender-announcement-wrap-message = [font size=14][bold]{$sender} Announcement:[/font][font size=12] {$message}[/bold][/font] -chat-manager-entity-say-wrap-message = [BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] {$verb}, [font={$fontType} size={$fontSize}]"[BubbleContent]{$message}[/BubbleContent]"[/font] -chat-manager-entity-say-bold-wrap-message = [BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] {$verb}, [font={$fontType} size={$fontSize}]"[BubbleContent][bold]{$message}[/bold][/BubbleContent]"[/font] +chat-manager-entity-say-wrap-message = [BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] {$verb}, [font={$fontType} size={$fontSize}]"[color={$color}][BubbleContent]{$message}[/BubbleContent][/color]"[/font] +chat-manager-entity-say-bold-wrap-message = [BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] {$verb}, [font={$fontType} size={$fontSize}]"[color={$color}][BubbleContent][bold]{$message}[/bold][/BubbleContent][/color]"[/font] -chat-manager-entity-whisper-wrap-message = [font size=11][italic][BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] whispers,"[BubbleContent]{$message}[/BubbleContent]"[/italic][/font] -chat-manager-entity-whisper-unknown-wrap-message = [font size=11][italic][BubbleHeader]Someone[/BubbleHeader] whispers, "[BubbleContent]{$message}[/BubbleContent]"[/italic][/font] +chat-manager-entity-whisper-wrap-message = [font size=11][italic][BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] whispers,"[color={$color}][BubbleContent]{$message}[/BubbleContent][/color]"[/italic][/font] +chat-manager-entity-whisper-unknown-wrap-message = [font size=11][italic][BubbleHeader]Someone[/BubbleHeader] whispers, "[color={$color}][BubbleContent]{$message}[/BubbleContent][/color]"[/italic][/font] # THE() is not used here because the entity and its name can technically be disconnected if a nameOverride is passed... chat-manager-entity-me-wrap-message = [italic]{ PROPER($entity) -> diff --git a/Resources/Locale/en-US/headset/headset-component.ftl b/Resources/Locale/en-US/headset/headset-component.ftl index a220737f18a..d62f2f4dae7 100644 --- a/Resources/Locale/en-US/headset/headset-component.ftl +++ b/Resources/Locale/en-US/headset/headset-component.ftl @@ -1,6 +1,6 @@ # Chat window radio wrap (prefix and postfix) -chat-radio-message-wrap = [color={$color}]{$channel} {$name} {$verb}, [font={$fontType} size={$fontSize}]"{$message}"[/font][/color] -chat-radio-message-wrap-bold = [color={$color}]{$channel} {$name} {$verb}, [font={$fontType} size={$fontSize}][bold]"{$message}"[/bold][/font][/color] +chat-radio-message-wrap = [color={$color}]{$channel} {$name} {$verb}, [font={$fontType} size={$fontSize}]"[/color][color={$languageColor}]{$message}[/color][color={$color}]"[/font][/color] +chat-radio-message-wrap-bold = [color={$color}]{$channel} {$name} {$verb}, [font={$fontType} size={$fontSize}][bold]"[/color][color={$languageColor}]{$message}[/color][color={$color}]"[/bold][/font][/color] examine-headset-default-channel = Use {$prefix} for the default channel ([color={$color}]{$channel}[/color]). From 37d95ce4e469b293746b26bda75077e02378b2c8 Mon Sep 17 00:00:00 2001 From: FoxxoTrystan Date: Thu, 4 Jul 2024 17:19:20 +0200 Subject: [PATCH 17/17] The fonts! --- Resources/Fonts/Copperplate.otf | Bin 0 -> 34436 bytes Resources/Fonts/Mangat.ttf | Bin 0 -> 29964 bytes Resources/Fonts/Noganas.ttf | Bin 0 -> 84396 bytes Resources/Fonts/RubikBubbles.ttf | Bin 0 -> 219436 bytes .../en-US/chat/managers/chat-manager.ftl | 4 ++-- .../Locale/en-US/headset/headset-component.ftl | 4 ++-- Resources/Prototypes/Language/languages.yml | 13 ++++++++++++- Resources/Prototypes/fonts.yml | 16 ++++++++++++++++ 8 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 Resources/Fonts/Copperplate.otf create mode 100644 Resources/Fonts/Mangat.ttf create mode 100644 Resources/Fonts/Noganas.ttf create mode 100644 Resources/Fonts/RubikBubbles.ttf diff --git a/Resources/Fonts/Copperplate.otf b/Resources/Fonts/Copperplate.otf new file mode 100644 index 0000000000000000000000000000000000000000..40d45aa46b66ca82debde94961af8d35d803bdb6 GIT binary patch literal 34436 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 literal 0 HcmV?d00001 diff --git a/Resources/Fonts/Noganas.ttf b/Resources/Fonts/Noganas.ttf new file mode 100644 index 0000000000000000000000000000000000000000..afa0c82f03e112f3334dcc457d4323715257ffea GIT binary patch literal 84396 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