diff --git a/Content.Client/Language/LanguageMenuWindow.xaml.cs b/Content.Client/Language/LanguageMenuWindow.xaml.cs index 312814aca35..11d1c290d16 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,11 @@ 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); + UpdateState(id, _clientLanguageSystem.SpokenLanguages); } 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.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/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 6698e1b6453..e5787cba48c 100644 --- a/Content.Server/Language/Commands/ListLanguagesCommand.cs +++ b/Content.Server/Language/Commands/ListLanguagesCommand.cs @@ -1,5 +1,5 @@ -using System.Linq; using Content.Shared.Administration; +using Content.Shared.Language; using Robust.Shared.Console; using Robust.Shared.Enums; @@ -30,10 +30,29 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) } var languages = IoCManager.Resolve().GetEntitySystem(); + var currentLang = languages.GetLanguage(playerEntity).ID; - var (spokenLangs, knownLangs) = languages.GetAllLanguages(playerEntity); + shell.WriteLine(Loc.GetString("command-language-spoken")); + var spoken = languages.GetSpokenLanguages(playerEntity); + for (int i = 0; i < spoken.Count; 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("Spoken:\n" + string.Join("\n", spokenLangs)); - shell.WriteLine("Understood:\n" + string.Join("\n", knownLangs)); + shell.WriteLine(Loc.GetString("command-language-understood")); + var understood = languages.GetUnderstoodLanguages(playerEntity); + for (int i = 0; i < understood.Count; i++) + { + var lang = understood[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..d340135925d 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 language)) { - shell.WriteError($"Language {languageId} is invalid or you cannot speak it!"); + shell.WriteError(failReason); return; } languages.SetLanguage(playerEntity, language.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.GetSpokenLanguages(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/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..572e2961fde 100644 --- a/Content.Server/Language/LanguageSystem.Networking.cs +++ b/Content.Server/Language/LanguageSystem.Networking.cs @@ -1,5 +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; @@ -7,11 +9,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 +16,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 +28,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 @@ -50,10 +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) { - var langs = GetLanguages(uid, component); - var message = new LanguagesUpdatedMessage(langs.CurrentLanguage, langs.SpokenLanguages, langs.UnderstoodLanguages); + 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); + RaiseNetworkEvent(message, session); } } diff --git a/Content.Server/Language/LanguageSystem.cs b/Content.Server/Language/LanguageSystem.cs index f1bf44c1f4f..e68489e9e28 100644 --- a/Content.Server/Language/LanguageSystem.cs +++ b/Content.Server/Language/LanguageSystem.cs @@ -1,289 +1,212 @@ using System.Linq; -using System.Text; -using Content.Server.GameTicking.Events; +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 Robust.Shared.Random; using UniversalLanguageSpeakerComponent = Content.Shared.Language.Components.UniversalLanguageSpeakerComponent; 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 - - /// - /// A random number added to each pseudo-random number's seed. Changes every round. - /// - public int RandomRoundSeed { get; private set; } - - public override void Initialize() { base.Initialize(); + InitializeNet(); - SubscribeNetworkEvent(OnClientSetLanguage); SubscribeLocalEvent(OnInitLanguageSpeaker); - SubscribeLocalEvent(_ => RandomRoundSeed = _random.Next()); - - InitializeNet(); } #region public api - /// - /// Obfuscate a message using an entity's default language. - /// - public string ObfuscateSpeech(EntityUid source, string message) - { - var language = GetLanguage(source) ?? Universal; - return ObfuscateSpeech(message, language); - } - /// - /// Obfuscate a message using the given language. - /// - public string ObfuscateSpeech(string message, LanguagePrototype language) + public bool CanUnderstand(EntityUid listener, string language, LanguageSpeakerComponent? component = null) { - var builder = new StringBuilder(); - if (language.ObfuscateSyllables) - ObfuscateSyllables(builder, message, language); - else - ObfuscatePhrases(builder, message, language); + if (language == UniversalPrototype || HasComp(listener)) + return true; - return builder.ToString(); + if (!Resolve(listener, ref component, logMissing: false)) + return false; + + return component.UnderstoodLanguages.Contains(language); } - public bool CanUnderstand(EntityUid listener, LanguagePrototype language, LanguageSpeakerComponent? listenerLanguageComp = null) + public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? component = null) { - if (language.ID == UniversalPrototype || HasComp(listener)) + if (HasComp(speaker)) return true; - var listenerLanguages = GetLanguages(listener, listenerLanguageComp)?.UnderstoodLanguages; + if (!Resolve(speaker, ref component, logMissing: false)) + return false; - return listenerLanguages?.Contains(language.ID, StringComparer.Ordinal) ?? false; + return component.SpokenLanguages.Contains(language); } - public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? speakerComp = null) + /// + /// Returns the current language of the given entity, assumes Universal if it's not a language speaker. + /// + public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? component = null) { - if (HasComp(speaker)) - return true; + 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)) + return Universal; - var langs = GetLanguages(speaker, speakerComp)?.UnderstoodLanguages; - return langs?.Contains(language, StringComparer.Ordinal) ?? false; + return proto; } /// - /// Returns the current language of the given entity. - /// Assumes Universal if not specified. + /// Returns the list of languages this entity can speak. /// - public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? languageComp = null) + /// Typically, checking is sufficient. + public List GetSpokenLanguages(EntityUid uid) { - var id = GetLanguages(speaker, languageComp)?.CurrentLanguage; - if (id == null) - return Universal; // Fallback + if (HasComp(uid)) + return [UniversalPrototype]; - _prototype.TryIndex(id, out LanguagePrototype? proto); + if (TryComp(uid, out var component)) + return component.SpokenLanguages; - return proto ?? Universal; + return []; } - public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? languageComp = null) + /// + /// Returns the list of languages this entity can understand. + /// + /// Typically, checking is sufficient. + public List GetUnderstoodLanguages(EntityUid uid) { - if (!CanSpeak(speaker, language) || HasComp(speaker)) - return; + if (HasComp(uid)) + return [UniversalPrototype]; // This one is tricky because... well, they understand all of them, not just one. - if (languageComp == null && !TryComp(speaker, out languageComp)) - return; + if (TryComp(uid, out var component)) + return component.UnderstoodLanguages; + + return []; + } - if (languageComp.CurrentLanguage == language) + 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 (addUnderstood && !comp.UnderstoodLanguages.Contains(language)) - comp.UnderstoodLanguages.Add(language); + if (addSpoken && !knowledge.SpokenLanguages.Contains(language)) + knowledge.SpokenLanguages.Add(language); - RaiseLocalEvent(comp.Owner, new LanguagesUpdateEvent(), true); - } + if (addUnderstood && !knowledge.UnderstoodLanguages.Contains(language)) + knowledge.UnderstoodLanguages.Add(language); - public (List spoken, List understood) GetAllLanguages(EntityUid speaker) - { - 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)); + 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. + /// Removes a language from the respective lists of intrinsically known languages of the given entity. /// - public void EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) + public void RemoveLanguage( + EntityUid uid, + string language, + bool removeSpoken = true, + bool removeUnderstood = true, + LanguageKnowledgeComponent? knowledge = null, + LanguageSpeakerComponent? speaker = null) { - if (comp == null && !TryComp(entity, out comp)) - return; + if (knowledge == null) + knowledge = EnsureComp(uid); - var langs = GetLanguages(entity, comp); - if (!langs.SpokenLanguages.Contains(comp!.CurrentLanguage, StringComparer.Ordinal)) - { - comp.CurrentLanguage = langs.SpokenLanguages.FirstOrDefault(UniversalPrototype); - RaiseLocalEvent(comp.Owner, new LanguagesUpdateEvent(), true); - } - } - #endregion + if (removeSpoken) + knowledge.SpokenLanguages.Remove(language); - #region event handling - private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) - { - if (string.IsNullOrEmpty(component.CurrentLanguage)) - component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); - } - #endregion + if (removeUnderstood) + knowledge.UnderstoodLanguages.Remove(language); - #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; - } + UpdateEntityLanguages(uid, speaker); } - private void ObfuscatePhrases(StringBuilder builder, string message, LanguagePrototype language) + /// + /// 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. + /// + /// True if the current language was modified, false otherwise. + public bool EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) { - // 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++) + if (!Resolve(entity, ref comp)) + return false; + + if (!comp.SpokenLanguages.Contains(comp.CurrentLanguage)) { - 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(" "); - } + comp.CurrentLanguage = comp.SpokenLanguages.FirstOrDefault(UniversalPrototype); + RaiseLocalEvent(entity, new LanguagesUpdateEvent()); + return true; } - } - private static bool IsSentenceEnd(char ch) - { - return ch is '.' or '!' or '?'; + 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(); - /// - /// 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; + 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/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 1e0c13375e4..00000000000 --- a/Content.Server/Language/TranslatorImplanterSystem.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Linq; -using Content.Server.Administration.Logs; -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; - -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; - - if (component.MobsOnly && !HasComp(target)) - { - _popup.PopupEntity("translator-implanter-refuse", component.Owner); - return; - } - - var understood = _language.GetAllLanguages(target).understood; - 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 3b7704b9a71..5022e540960 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; @@ -8,6 +9,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 +25,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,67 +32,36 @@ public override void Initialize() SubscribeLocalEvent(OnTranslatorToggle); SubscribeLocalEvent(OnPowerCellSlotEmpty); - // TODO: why does this use InteractHandEvent?? SubscribeLocalEvent(OnTranslatorInteract); SubscribeLocalEvent(OnTranslatorDropped); } - private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent component, - DetermineEntityLanguagesEvent ev) + 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)) return; - 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, StringComparer.Ordinal)) - addSpoken = false; - - if (!ev.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) - 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, StringComparer.Ordinal)) - addSpoken = true; - - if (ev.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) - addUnderstood = true; - } - } - } + // 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 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) - 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)) @@ -100,7 +70,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) @@ -115,59 +85,63 @@ 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 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); - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + // 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 { // 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); } } @@ -178,7 +152,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; @@ -186,11 +160,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); } } @@ -201,25 +174,29 @@ private void UpdateBoundIntrinsicComp(HandheldTranslatorComponent comp, HoldsTra { if (isEnabled) { - intrinsic.SpokenLanguages = new List(comp.SpokenLanguages); - intrinsic.UnderstoodLanguages = new List(comp.UnderstoodLanguages); - intrinsic.DefaultLanguageOverride = comp.DefaultLanguageOverride; + intrinsic.SpokenLanguages = [..comp.SpokenLanguages]; + intrinsic.UnderstoodLanguages = [..comp.UnderstoodLanguages]; } else { intrinsic.SpokenLanguages.Clear(); intrinsic.UnderstoodLanguages.Clear(); - intrinsic.DefaultLanguageOverride = null; } intrinsic.Enabled = isEnabled; intrinsic.Issuer = comp; } - private static void AddIfNotExists(List list, string item) + /// + /// Checks whether any OR all required languages are provided. Used for utility purposes. + /// + public static bool CheckLanguagesMatch(ICollection required, ICollection provided, bool requireAll) { - if (list.Contains(item)) - return; - list.Add(item); + if (required.Count == 0) + return true; + + return requireAll + ? required.All(provided.Contains) + : required.Any(provided.Contains); } } 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 = []; } 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/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/Content.Shared/Language/LanguagePrototype.cs b/Content.Shared/Language/LanguagePrototype.cs index 801ab8a393b..9342c07e91f 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; @@ -10,18 +9,10 @@ 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; - - /// - /// 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. - /// - [DataField(required: true)] - public List Replacement = []; + [DataField("obfuscation")] + public ObfuscationMethod Obfuscation = ObfuscationMethod.Default; #region utility /// diff --git a/Content.Shared/Language/ObfuscationMethods.cs b/Content.Shared/Language/ObfuscationMethods.cs new file mode 100644 index 00000000000..51230c47970 --- /dev/null +++ b/Content.Shared/Language/ObfuscationMethods.cs @@ -0,0 +1,184 @@ +using System.Text; +using Content.Shared.Language.Systems; + +namespace Content.Shared.Language; + +[ImplicitDataDefinitionForInheritors] +public abstract partial class ObfuscationMethod +{ + /// + /// The fallback obfuscation method, replaces the message with the string "<?>". + /// + public static readonly ObfuscationMethod Default = new ReplacementObfuscation + { + Replacement = new List { "" } + }; + + /// + /// Obfuscates the provided message and writes the result into the provided StringBuilder. + /// Implementations should use the context's pseudo-random number generator and provide stable obfuscations. + /// + internal abstract void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context); + + /// + /// Obfuscates the provided message. This method should only be used for debugging purposes. + /// For all other purposes, use instead. + /// + public string Obfuscate(string message) + { + var builder = new StringBuilder(); + Obfuscate(builder, message, IoCManager.Resolve().GetEntitySystem()); + return builder.ToString(); + } +} + +/// +/// The most primitive method of obfuscation - replaces the entire message with one random replacement phrase. +/// Similar to ReplacementAccent. Base for all replacement-based obfuscation methods. +/// +public partial class ReplacementObfuscation : ObfuscationMethod +{ + /// + /// A list of replacement phrases used in the obfuscation process. + /// + [DataField(required: true)] + public List Replacement = []; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + var idx = context.PseudoRandomNumber(message.GetHashCode(), 0, Replacement.Count - 1); + builder.Append(Replacement[idx]); + } +} + +/// +/// Obfuscates the provided message by replacing each word with a random number of syllables in the range (min, max), +/// preserving the original punctuation to a resonable extent. +/// +/// +/// The words are obfuscated in a stable manner, such that every particular word will be obfuscated the same way throughout one round. +/// This means that particular words can be memorized within a round, but not across rounds. +/// +public sealed partial class SyllableObfuscation : ReplacementObfuscation +{ + [DataField] + public int MinSyllables = 1; + + [DataField] + public int MaxSyllables = 4; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + const char eof = (char) 0; // Special character to mark the end of the message in the code below + + var wordBeginIndex = 0; + var hashCode = 0; + + for (var i = 0; i <= message.Length; i++) + { + var ch = i < message.Length ? char.ToLower(message[i]) : eof; + var isWordEnd = char.IsWhiteSpace(ch) || IsPunctuation(ch) || ch == eof; + + // If this is a normal char, add it to the hash sum + if (!isWordEnd) + hashCode = hashCode * 31 + ch; + + // If a word ends before this character, construct a new word and append it to the new message. + if (isWordEnd) + { + var wordLength = i - wordBeginIndex; + if (wordLength > 0) + { + var newWordLength = context.PseudoRandomNumber(hashCode, MinSyllables, MaxSyllables); + + for (var j = 0; j < newWordLength; j++) + { + var index = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); + builder.Append(Replacement[index]); + } + } + + hashCode = 0; + wordBeginIndex = i + 1; + } + + // If this message concludes a word (i.e. is a whitespace or a punctuation mark), append it to the message + if (isWordEnd && ch != eof) + builder.Append(ch); + } + } + + private static bool IsPunctuation(char ch) + { + return ch is '.' or '!' or '?' or ',' or ':'; + } +} + +/// +/// Obfuscates each sentence in the message by concatenating a number of obfuscation phrases. +/// The number of phrases in the obfuscated message is proportional to the length of the original message. +/// +public sealed partial class PhraseObfuscation : ReplacementObfuscation +{ + [DataField] + public int MinPhrases = 1; + + [DataField] + public int MaxPhrases = 4; + + /// + /// A string used to separate individual phrases within one sentence. Default is a space. + /// + [DataField] + public string Separator = " "; + + /// + /// A power to which the number of characters in the original message is raised to determine the number of phrases in the result. + /// Default is 1/3, i.e. the cubic root of the original number. + /// + /// + /// Using the default proportion, you will need at least 27 characters for 2 phrases, at least 64 for 3, at least 125 for 4, etc. + /// Increasing the proportion to 1/4 will result in the numbers changing to 81, 256, 625, etc. + /// + [DataField] + public float Proportion = 1f / 3; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + var sentenceBeginIndex = 0; + var hashCode = 0; + + for (var i = 0; i < message.Length; i++) + { + var ch = char.ToLower(message[i]); + if (!IsPunctuation(ch) && i != message.Length - 1) + { + hashCode = hashCode * 31 + ch; + continue; + } + + var length = i - sentenceBeginIndex; + if (length > 0) + { + var newLength = (int) Math.Clamp(Math.Pow(length, Proportion) - 1, MinPhrases, MaxPhrases); + + for (var j = 0; j < newLength; j++) + { + var phraseIdx = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); + var phrase = Replacement[phraseIdx]; + builder.Append(phrase); + builder.Append(Separator); + } + } + sentenceBeginIndex = i + 1; + + if (IsPunctuation(ch)) + builder.Append(ch).Append(' '); // TODO: this will turn '...' into '. . . ' + } + } + + private static bool IsPunctuation(char ch) + { + return ch is '.' or '!' or '?'; // Doesn't include mid-sentence punctuation like the comma + } +} diff --git a/Content.Shared/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/Language/Systems/SharedLanguageSystem.cs index e2eeb8bb493..0a03086ebe1 100644 --- a/Content.Shared/Language/Systems/SharedLanguageSystem.cs +++ b/Content.Shared/Language/Systems/SharedLanguageSystem.cs @@ -1,6 +1,6 @@ -using Content.Shared.Actions; +using System.Text; +using Content.Shared.GameTicking; using Robust.Shared.Prototypes; -using Robust.Shared.Random; namespace Content.Shared.Language.Systems; @@ -24,7 +24,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 +36,32 @@ 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, this); + + 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); + } } 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/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. 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 e51ae91d12c..9b851ffa522 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml @@ -133,7 +133,7 @@ - FootstepSound - DoorBumpOpener - ShoesRequiredStepTriggerImmune - - 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 e311681ce5f..555f653737e 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: @@ -240,7 +240,7 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Chicken understands: @@ -519,7 +519,7 @@ prob: 0.5 - type: Extractable grindableSolutionName: food - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Moffic understands: @@ -619,7 +619,7 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Duck understands: @@ -872,7 +872,7 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Voice/Arachnid/arachnid_chitter.ogg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Crab understands: @@ -1118,7 +1118,7 @@ - type: Inventory speciesId: kangaroo templateId: kangaroo - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Kangaroo understands: @@ -1311,7 +1311,7 @@ - type: Speech speechSounds: Monkey speechVerb: Monkey - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -1350,7 +1350,7 @@ - type: Speech speechSounds: Monkey speechVerb: Monkey - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -1395,7 +1395,7 @@ - type: NameIdentifier group: Kobold - type: LizardAccent - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Kobold understands: @@ -1630,7 +1630,7 @@ spawned: - id: FoodMeatRat amount: 1 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -1973,7 +1973,7 @@ path: /Audio/Animals/parrot_raught.ogg - type: Bloodstream bloodMaxVolume: 50 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon understands: @@ -2236,7 +2236,7 @@ - type: MeleeChemicalInjector transferAmount: 0.75 solution: melee - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -2572,7 +2572,7 @@ - type: Tag tags: - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Fox understands: @@ -2623,7 +2623,7 @@ spawned: - id: FoodMeat amount: 2 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -2780,7 +2780,7 @@ spawned: - id: FoodMeat amount: 3 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: @@ -2852,7 +2852,7 @@ - type: NpcFactionMember factions: - Syndicate - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -3157,7 +3157,7 @@ spawned: - id: FoodMeat amount: 1 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -3269,7 +3269,7 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Animals/pig_oink.ogg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Pig understands: @@ -3359,7 +3359,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 5141811a8ea..6eb43fb89ae 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: @@ -411,7 +411,7 @@ spawned: - id: FoodMeat amount: 3 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -576,7 +576,7 @@ - VimPilot - type: StealTarget stealGroup: AnimalRenault - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Fox understands: @@ -629,7 +629,7 @@ - CannotSuicide - Hamster - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -807,7 +807,7 @@ attributes: proper: true gender: female - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Bubblish understands: @@ -847,7 +847,7 @@ attributes: proper: true gender: male - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -881,7 +881,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 c5395360187..b38ea2634fd 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml @@ -51,7 +51,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 5959c497186..1c55dcf0df1 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 d5be77eef1c..40cb0da95a1 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. - type: RequireProjectileTarget active: False 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/Devices/translators.yml b/Resources/Prototypes/Entities/Objects/Devices/translators.yml index e5ad824c5d9..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: @@ -203,3 +203,4 @@ - Kobold requires: - GalacticCommon + setLanguageOnInteract: false 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 diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 380f7e012d1..2c4fccb8b3e 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -104,7 +104,7 @@ price: 100 - type: Appearance - type: WiresVisuals - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RobotTalk diff --git a/Resources/Prototypes/Language/languages.yml b/Resources/Prototypes/Language/languages.yml index 90bce1baed2..1a874612c2f 100644 --- a/Resources/Prototypes/Language/languages.yml +++ b/Resources/Prototypes/Language/languages.yml @@ -1,493 +1,558 @@ # 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 - obfuscateSyllables: false - replacement: - - "*incomprehensible*" + obfuscation: + !type:ReplacementObfuscation + replacement: + - "*incomprehensible*" # Never actually used # The common galactic tongue. - type: language id: GalacticCommon - obfuscateSyllables: true - replacement: - - Blah - - Blah - - Blah - - dingle-doingle - - dingle - - dangle - - jibber-jabber - - jubber - - bleh - - zippity - - zoop - - wibble - - wobble - - wiggle - - yada - - meh - - neh - - nah - - wah + obfuscation: + !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 # Spoken by slimes. - type: language id: Bubblish - obfuscateSyllables: true - replacement: - - blob - - plop - - pop - - bop - - boop + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - blob + - plop + - pop + - bop + - boop # Spoken by moths. - type: language id: Moffic - obfuscateSyllables: true - 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 + obfuscation: + !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 - # Spoken by dionas. +# Spoken by dionas. - type: language id: RootSpeak - obfuscateSyllables: true - replacement: - - hs - - zt - - kr - - st - - sh + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 5 + replacement: + - hs + - zt + - kr + - st + - sh # A mess of broken Japanese, spoken by Felinds and Oni - type: language id: Nekomimetic - obfuscateSyllables: true - 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 + obfuscation: + !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 # Spoken by the Lizard race. - type: language id: Draconic - obfuscateSyllables: true - 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 + obfuscation: + !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 # Spoken by the Vulpkanin race. - type: language id: Canilunzt - obfuscateSyllables: true - 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 + obfuscation: + !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 # The common language of the Sol system. - type: language id: SolCommon - obfuscateSyllables: true - replacement: - - tao - - shi - - tzu - - yi - - com - - be - - is - - i - - op - - vi - - ed - - lec - - mo - - cle - - te - - dis - - e + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 + replacement: + - tao + - shi + - tzu + - yi + - com + - be + - is + - i + - op + - vi + - ed + - lec + - mo + - cle + - te + - dis + - e - type: language id: RobotTalk - obfuscateSyllables: true - replacement: - - 0 - - 1 - - 01 - - 10 - - 001 - - 100 - - 011 - - 110 - - 101 - - 010 + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 10 # Crazy + replacement: + - 0 + - 1 # Languages spoken by various critters. - type: language id: Cat - obfuscateSyllables: true - replacement: - - murr - - meow - - purr - - mrow + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - murr + - meow + - purr + - mrow - type: language id: Dog - obfuscateSyllables: true - replacement: - - woof - - bark - - ruff - - bork - - raff - - garr + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - woof + - bark + - ruff + - bork + - raff + - garr - type: language id: Fox - obfuscateSyllables: true - replacement: - - bark - - gecker - - ruff - - raff - - garr + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - ruff + - raff + - garr + - yip + - yap + - myah - type: language id: Xeno - obfuscateSyllables: true - replacement: - - sss - - sSs - - SSS + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # I was crazy once + replacement: + - s + - S - type: language id: Monkey - obfuscateSyllables: true - replacement: - - ok - - ook - - oook - - ooook - - oooook + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # They locked me in a room... + replacement: + - o + - k - type: language id: Mouse - obfuscateSyllables: true - replacement: - - Squeak - - Piep - - Chuu - - Eeee - - Pip - - Fwiep - - Heep + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 3 + replacement: + - squ + - eak + - pi + - ep + - chuu + - ee + - fwi + - he - type: language id: Chicken - obfuscateSyllables: true - replacement: - - Coo - - Coot - - Cooot + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - co + - coo + - ot - type: language id: Duck - obfuscateSyllables: true - replacement: - - Quack - - Quack quack + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - qu + - ack + - quack - type: language id: Cow - obfuscateSyllables: true - replacement: - - Moo - - Mooo + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - moo + - mooo - type: language id: Sheep - obfuscateSyllables: true - replacement: - - Ba - - Baa - - Baaa + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - ba + - baa + - aa - type: language id: Kangaroo - obfuscateSyllables: true - replacement: - - Shreak - - Chuu + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - shre + - ack + - chuu + - choo - type: language id: Pig - obfuscateSyllables: true - replacement: - - Oink - - Oink oink + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - oink # Please someone come up with something better - type: language id: Crab - obfuscateSyllables: true - replacement: - - Click - - Click-clack - - Clack - - Tipi-tap - - Clik-tap - - Cliliick + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - click + - clack + - ti + - pi + - tap + - cli + - ick - type: language id: Kobold - obfuscateSyllables: true - replacement: - - Yip - - Grrar. - - Yap - - Bip - - Screet - - Gronk - - Hiss - - Eeee - - Yip + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 + replacement: + - yip + - yap + - gar + - grr + - ar + - scre + - et + - gronk + - hiss + - ss + - ee diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml index ab3f6f3d1c1..e93117b1aad 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 5bc02461eed..f032c527f19 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml @@ -65,7 +65,7 @@ Unsexed: MaleFelinid - type: Felinid - type: NoShoesSilentFootsteps - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon