From 45c1bf0efd8e418b5d467fb3409b98edf4f27bdd Mon Sep 17 00:00:00 2001 From: fox Date: Fri, 14 Jun 2024 02:59:25 +0300 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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