diff --git a/Content.Client/Language/LanguageMenuWindow.xaml b/Content.Client/Language/LanguageMenuWindow.xaml index c92eb2ffa33..ff33a6ddf56 100644 --- a/Content.Client/Language/LanguageMenuWindow.xaml +++ b/Content.Client/Language/LanguageMenuWindow.xaml @@ -1,6 +1,6 @@ diff --git a/Content.Client/Language/LanguageMenuWindow.xaml.cs b/Content.Client/Language/LanguageMenuWindow.xaml.cs index ea1c981b266..e6744b5770a 100644 --- a/Content.Client/Language/LanguageMenuWindow.xaml.cs +++ b/Content.Client/Language/LanguageMenuWindow.xaml.cs @@ -21,14 +21,12 @@ public LanguageMenuWindow() { RobustXamlLoader.Load(this); _clientLanguageSystem = IoCManager.Resolve().GetEntitySystem(); - - Title = Loc.GetString("language-menu-window-title"); } public void UpdateState(string currentLanguage, List spokenLanguages) { - var langName = LanguagePrototype.GetLocalizedName(currentLanguage); - CurrentLanguageLabel.Text = Loc.GetString("language-menu-current-language", ("language", langName ?? "")); + var langName = Loc.GetString($"language-{currentLanguage}-name"); + CurrentLanguageLabel.Text = Loc.GetString("language-menu-current-language", ("language", langName)); OptionsList.RemoveAllChildren(); _entries.Clear(); @@ -64,7 +62,7 @@ private void AddLanguageEntry(string language) header.SeparationOverride = 2; var name = new Label(); - name.Text = proto?.LocalizedName ?? ""; + name.Text = proto?.Name ?? ""; name.MinWidth = 50; name.HorizontalExpand = true; @@ -86,7 +84,7 @@ private void AddLanguageEntry(string language) body.Margin = new Thickness(4f, 4f); var description = new RichTextLabel(); - description.SetMessage(proto?.LocalizedDescription ?? ""); + description.SetMessage(proto?.Description ?? ""); description.HorizontalExpand = true; body.AddChild(description); diff --git a/Content.Client/Language/Systems/Chat/Controls/LanguageSelectorButton.cs b/Content.Client/Language/Systems/Chat/Controls/LanguageSelectorButton.cs index fff62cea1be..1818d32ef42 100644 --- a/Content.Client/Language/Systems/Chat/Controls/LanguageSelectorButton.cs +++ b/Content.Client/Language/Systems/Chat/Controls/LanguageSelectorButton.cs @@ -30,15 +30,6 @@ public LanguageSelectorButton() IoCManager.Resolve().GetUIController().LanguagesUpdatedHook += UpdateLanguage; } - private void UpdateLanguage((string current, List spoken, List understood) args) - { - Popup.SetLanguages(args.spoken); - - // Kill me please - SelectedLanguage = IoCManager.Resolve().GetEntitySystem().GetLanguage(args.current); - Text = LanguageSelectorName(SelectedLanguage!); - } - protected override UIBox2 GetPopupPosition() { var globalLeft = GlobalPosition.X; @@ -48,24 +39,9 @@ protected override UIBox2 GetPopupPosition() new Vector2(SizeBox.Width, SelectorDropdownOffset)); } - public void Select(LanguagePrototype language) - { - if (Popup.Visible) - { - Popup.Close(); - } - - if (SelectedLanguage == language) - return; - SelectedLanguage = language; - IoCManager.Resolve().GetEntitySystem().RequestSetLanguage(language); - - Text = LanguageSelectorName(language); - } - public static string LanguageSelectorName(LanguagePrototype language, bool full = false) { - var name = language.LocalizedName; + var name = language.Name; // if the language name is short enough, just return it if (full || name.Length < 5) @@ -75,10 +51,10 @@ public static string LanguageSelectorName(LanguagePrototype language, bool full if (name.Contains(' ')) { var result = name - .Split(" ") - .Select(it => it.FirstOrNull()) - .Where(it => it != null) - .Select(it => char.ToUpper(it!.Value)); + .Split(" ") // split by words + .Select(it => it.FirstOrNull()) // take the first letter from each + .Where(it => it != null) // ignore empty words (double spaces) + .Select(it => char.ToUpper(it!.Value)); // capitalize return new string(result.ToArray()); } @@ -86,4 +62,28 @@ public static string LanguageSelectorName(LanguagePrototype language, bool full // Alternatively, take the first 5 letters return name[..5]; } + + public void Select(LanguagePrototype language) + { + if (Popup.Visible) + { + Popup.Close(); + } + + if (SelectedLanguage == language) + return; + SelectedLanguage = language; + IoCManager.Resolve().GetEntitySystem().RequestSetLanguage(language); + + Text = LanguageSelectorName(language); + } + + private void UpdateLanguage((string current, List spoken, List understood) args) + { + Popup.SetLanguages(args.spoken); + + // Kill me please + SelectedLanguage = IoCManager.Resolve().GetEntitySystem().GetLanguage(args.current); + Text = LanguageSelectorName(SelectedLanguage!); + } } diff --git a/Content.Client/Language/Systems/LanguageSystem.cs b/Content.Client/Language/Systems/LanguageSystem.cs index f7f1d1ebb48..df62173a4db 100644 --- a/Content.Client/Language/Systems/LanguageSystem.cs +++ b/Content.Client/Language/Systems/LanguageSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.Language; +using Content.Shared.Language.Events; using Content.Shared.Language.Systems; using Content.Shared.Mind.Components; using Robust.Shared.Console; @@ -7,10 +8,11 @@ namespace Content.Client.Language.Systems; /// /// Client-side language system. -/// +/// +/// /// Unlike the server, the client is not aware of other entities' languages; it's only notified about the entity that it posesses. /// Due to that, this system stores such information in a static manner. -/// +/// public sealed class LanguageSystem : SharedLanguageSystem { /// @@ -51,7 +53,7 @@ public void RequestSetLanguage(LanguagePrototype language) return; // (This is dumb. This is very dumb. It should be a message instead.) - _consoleHost.ExecuteCommand("lsselectlang " + language.ID); + _consoleHost.ExecuteCommand("languageselect " + language.ID); // So to reduce the probability of desync, we replicate the change locally too if (SpokenLanguages.Contains(language.ID)) diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 83ce085bca2..43a1e72a171 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -209,10 +209,10 @@ void AddCheckBox(string checkBoxName, bool currentState, Action(uid); var speaker = entityManager.EnsureComponent(uid); - var gc = SharedLanguageSystem.GalacticCommon.ID; + var fallback = SharedLanguageSystem.FallbackLanguagePrototype; - if (!speaker.UnderstoodLanguages.Contains(gc)) - speaker.UnderstoodLanguages.Add(gc); + if (!speaker.UnderstoodLanguages.Contains(fallback)) + speaker.UnderstoodLanguages.Add(fallback); - if (!speaker.SpokenLanguages.Contains(gc)) + if (!speaker.SpokenLanguages.Contains(fallback)) { - speaker.CurrentLanguage = gc; - speaker.SpokenLanguages.Add(gc); + speaker.CurrentLanguage = fallback; + speaker.SpokenLanguages.Add(fallback); - args.EntityManager.EventBus.RaiseLocalEvent(uid, new SharedLanguageSystem.LanguagesUpdateEvent(), true); + args.EntityManager.EventBus.RaiseLocalEvent(uid, new LanguagesUpdateEvent(), true); } // Stops from adding a ghost role to things like people who already have a mind diff --git a/Content.Server/Language/Commands/ListLanguagesCommand.cs b/Content.Server/Language/Commands/ListLanguagesCommand.cs index d64573daeba..2d55e3b36d8 100644 --- a/Content.Server/Language/Commands/ListLanguagesCommand.cs +++ b/Content.Server/Language/Commands/ListLanguagesCommand.cs @@ -8,9 +8,9 @@ namespace Content.Server.Language.Commands; [AnyCommand] public sealed class ListLanguagesCommand : IConsoleCommand { - public string Command => "lslangs"; - public string Description => "List languages your current entity can speak at the current moment."; - public string Help => "lslangs"; + public string Command => "languagelist"; + public string Description => Loc.GetString("command-list-langs-desc"); + public string Help => Loc.GetString("command-list-langs-help", ("command", Command)); public void Execute(IConsoleShell shell, string argStr, string[] args) { @@ -33,7 +33,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) var (spokenLangs, knownLangs) = languages.GetAllLanguages(playerEntity); - shell.WriteLine("Spoken: " + string.Join(", ", spokenLangs)); - shell.WriteLine("Understood: " + string.Join(", ", knownLangs)); + shell.WriteLine("Spoken:\n" + string.Join("\n", spokenLangs)); + shell.WriteLine("Understood:\n" + string.Join("\n", knownLangs)); } } diff --git a/Content.Server/Language/Commands/SayLanguageCommand.cs b/Content.Server/Language/Commands/SayLanguageCommand.cs index 8acbe5c78c4..ec321617f14 100644 --- a/Content.Server/Language/Commands/SayLanguageCommand.cs +++ b/Content.Server/Language/Commands/SayLanguageCommand.cs @@ -8,9 +8,9 @@ namespace Content.Server.Language.Commands; [AnyCommand] public sealed class SayLanguageCommand : IConsoleCommand { - public string Command => "lsay"; - public string Description => "Send chat languages to the local channel or a specific chat channel, in a specific language."; - public string Help => "lsay "; + public string Command => "saylang"; + public string Description => Loc.GetString("command-saylang-desc"); + public string Help => Loc.GetString("command-saylang-help", ("command", Command)); public void Execute(IConsoleShell shell, string argStr, string[] args) { diff --git a/Content.Server/Language/Commands/SelectLanguageCommand.cs b/Content.Server/Language/Commands/SelectLanguageCommand.cs index e489267cd31..a7f6751fe16 100644 --- a/Content.Server/Language/Commands/SelectLanguageCommand.cs +++ b/Content.Server/Language/Commands/SelectLanguageCommand.cs @@ -8,9 +8,9 @@ namespace Content.Server.Language.Commands; [AnyCommand] public sealed class SelectLanguageCommand : IConsoleCommand { - public string Command => "lsselectlang"; - public string Description => "Open a menu to select a language to speak."; - public string Help => "lsselectlang"; + public string Command => "languageselect"; + public string Description => Loc.GetString("command-language-select-desc"); + public string Help => Loc.GetString("command-language-select-help", ("command", Command)); public void Execute(IConsoleShell shell, string argStr, string[] args) { diff --git a/Content.Server/Language/DetermineEntityLanguagesEvent.cs b/Content.Server/Language/DetermineEntityLanguagesEvent.cs new file mode 100644 index 00000000000..13ab2cac279 --- /dev/null +++ b/Content.Server/Language/DetermineEntityLanguagesEvent.cs @@ -0,0 +1,29 @@ +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. +/// +public sealed class DetermineEntityLanguagesEvent : EntityEventArgs +{ + /// + /// 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! + /// + public List SpokenLanguages; + /// + /// The list of all languages the entity may understand. Must NOT be held as a reference! + /// + public List UnderstoodLanguages; + + public DetermineEntityLanguagesEvent(string currentLanguage, List spokenLanguages, List understoodLanguages) + { + CurrentLanguage = currentLanguage; + SpokenLanguages = spokenLanguages; + UnderstoodLanguages = understoodLanguages; + } +} diff --git a/Content.Server/Language/LanguageSystem.Networking.cs b/Content.Server/Language/LanguageSystem.Networking.cs index f91a411b6b1..94f7c19f500 100644 --- a/Content.Server/Language/LanguageSystem.Networking.cs +++ b/Content.Server/Language/LanguageSystem.Networking.cs @@ -1,5 +1,7 @@ using Content.Server.Mind; using Content.Shared.Language; +using Content.Shared.Language.Events; +using Content.Shared.Language.Systems; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Robust.Shared.Player; diff --git a/Content.Server/Language/LanguageSystem.cs b/Content.Server/Language/LanguageSystem.cs index 87d9c5b3f12..26b46e63cd3 100644 --- a/Content.Server/Language/LanguageSystem.cs +++ b/Content.Server/Language/LanguageSystem.cs @@ -2,6 +2,7 @@ using System.Text; using Content.Server.GameTicking.Events; using Content.Shared.Language; +using Content.Shared.Language.Events; using Content.Shared.Language.Systems; using Robust.Shared.Random; using UniversalLanguageSpeakerComponent = Content.Shared.Language.Components.UniversalLanguageSpeakerComponent; @@ -10,6 +11,12 @@ 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. /// @@ -20,35 +27,26 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnInitLanguageSpeaker); - SubscribeAllEvent(it => RandomRoundSeed = _random.Next()); + SubscribeLocalEvent(_ => RandomRoundSeed = _random.Next()); InitializeNet(); } - private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) + #region public api + /// + /// Obfuscate the speech of the given entity using its default language. + /// + public string ObfuscateSpeech(EntityUid source, string message) { - if (string.IsNullOrEmpty(component.CurrentLanguage)) - { - component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); - } + var language = GetLanguage(source) ?? Universal; + return ObfuscateSpeech(message, language); } /// - /// Obfuscate speech of the given entity, or using the given language. + /// Obfuscate a message using the given language. /// - /// The speaker whose message needs to be obfuscated. Must not be null if "language" is not set. - /// The language for obfuscation. Must not be null if "source" is null. - public string ObfuscateSpeech(EntityUid? source, string message, LanguagePrototype? language = null) + public string ObfuscateSpeech(string message, LanguagePrototype language) { - if (language == null) - { - if (source is not { Valid: true }) - { - throw new NullReferenceException("Either source or language must be set."); - } - language = GetLanguage(source.Value); - } - var builder = new StringBuilder(); if (language.ObfuscateSyllables) { @@ -59,79 +57,10 @@ public string ObfuscateSpeech(EntityUid? source, string message, LanguagePrototy ObfuscatePhrases(builder, message, language); } - //_sawmill.Info($"Got {message}, obfuscated to {builder}. Language: {language.ID}"); - return builder.ToString(); } - private void ObfuscateSyllables(StringBuilder builder, string message, LanguagePrototype language) - { - // Go through each word. Calculate its hash sum and count the number of letters. - // Replicate it with pseudo-random syllables of pseudo-random (but similar) length. Use the hash code as the seed. - // This means that identical words will be obfuscated identically. Simple words like "hello" or "yes" in different langs can be memorized. - var wordBeginIndex = 0; - var hashCode = 0; - for (var i = 0; i < message.Length; i++) - { - var ch = char.ToLower(message[i]); - // A word ends when one of the following is found: a space, a sentence end, or EOM - if (char.IsWhiteSpace(ch) || IsSentenceEnd(ch) || i == message.Length - 1) - { - var wordLength = i - wordBeginIndex; - if (wordLength > 0) - { - var newWordLength = PseudoRandomNumber(hashCode, 1, 4); - - for (var j = 0; j < newWordLength; j++) - { - var index = PseudoRandomNumber(hashCode + j, 0, language.Replacement.Count); - builder.Append(language.Replacement[index]); - } - } - - builder.Append(ch); - hashCode = 0; - wordBeginIndex = i + 1; - } - else - { - hashCode = hashCode * 31 + ch; - } - } - } - - private void ObfuscatePhrases(StringBuilder builder, string message, LanguagePrototype language) - { - // In a similar manner, each phrase is obfuscated with a random number of conjoined obfuscation phrases. - // However, the number of phrases depends on the number of characters in the original phrase. - var sentenceBeginIndex = 0; - for (var i = 0; i < message.Length; i++) - { - var ch = char.ToLower(message[i]); - if (IsSentenceEnd(ch) || i == message.Length - 1) - { - var length = i - sentenceBeginIndex; - if (length > 0) - { - var newLength = (int) Math.Clamp(Math.Cbrt(length) - 1, 1, 4); // 27+ chars for 2 phrases, 64+ for 3, 125+ for 4. - - for (var j = 0; j < newLength; j++) - { - var phrase = _random.Pick(language.Replacement); - builder.Append(phrase); - } - } - sentenceBeginIndex = i + 1; - - if (IsSentenceEnd(ch)) - builder.Append(ch).Append(" "); - } - } - } - - public bool CanUnderstand(EntityUid listener, - LanguagePrototype language, - LanguageSpeakerComponent? listenerLanguageComp = null) + public bool CanUnderstand(EntityUid listener, LanguagePrototype language, LanguageSpeakerComponent? listenerLanguageComp = null) { if (language.ID == UniversalPrototype || HasComp(listener)) return true; @@ -188,34 +117,126 @@ public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerCompo /// public void AddLanguage(LanguageSpeakerComponent comp, string language, bool addSpoken = true, bool addUnderstood = true) { - if (addSpoken && !comp.SpokenLanguages.Contains(language, StringComparer.Ordinal)) + if (addSpoken && !comp.SpokenLanguages.Contains(language)) comp.SpokenLanguages.Add(language); - if (addUnderstood && !comp.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) + if (addUnderstood && !comp.UnderstoodLanguages.Contains(language)) comp.UnderstoodLanguages.Add(language); RaiseLocalEvent(comp.Owner, new LanguagesUpdateEvent(), true); } - private static bool IsSentenceEnd(char ch) - { - return ch is '.' or '!' or '?'; - } - /// /// Returns a pair of (spoken, understood) languages of the given entity. /// - public (List, List) GetAllLanguages(EntityUid speaker) + 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)); } - // This event is reused because re-allocating it each time is way too costly. - private readonly DetermineEntityLanguagesEvent _determineLanguagesEvent = new(string.Empty, new(), new()), - _universalLanguagesEvent = new(UniversalPrototype, [UniversalPrototype], [UniversalPrototype]); // Used for universal speakers only; never mutated + /// + /// Ensures the given entity has a valid language as its current language. + /// If not, sets it to the first entry of its SpokenLanguages list, or universal if it's empty. + /// + public void EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) + { + if (comp == null && !TryComp(entity, out comp)) + return; + + 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 + #region event handling + private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) + { + if (string.IsNullOrEmpty(component.CurrentLanguage)) + { + component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); + } + } + #endregion + + #region internal api - obfuscation + private void ObfuscateSyllables(StringBuilder builder, string message, LanguagePrototype language) + { + // Go through each word. Calculate its hash sum and count the number of letters. + // Replicate it with pseudo-random syllables of pseudo-random (but similar) length. Use the hash code as the seed. + // This means that identical words will be obfuscated identically. Simple words like "hello" or "yes" in different langs can be memorized. + var wordBeginIndex = 0; + var hashCode = 0; + for (var i = 0; i < message.Length; i++) + { + var ch = char.ToLower(message[i]); + // A word ends when one of the following is found: a space, a sentence end, or EOM + if (char.IsWhiteSpace(ch) || IsSentenceEnd(ch) || i == message.Length - 1) + { + var wordLength = i - wordBeginIndex; + if (wordLength > 0) + { + var newWordLength = PseudoRandomNumber(hashCode, 1, 4); + + for (var j = 0; j < newWordLength; j++) + { + var index = PseudoRandomNumber(hashCode + j, 0, language.Replacement.Count); + builder.Append(language.Replacement[index]); + } + } + + builder.Append(ch); + hashCode = 0; + wordBeginIndex = i + 1; + } + else + { + hashCode = hashCode * 31 + ch; + } + } + } + + private void ObfuscatePhrases(StringBuilder builder, string message, LanguagePrototype language) + { + // In a similar manner, each phrase is obfuscated with a random number of conjoined obfuscation phrases. + // However, the number of phrases depends on the number of characters in the original phrase. + var sentenceBeginIndex = 0; + for (var i = 0; i < message.Length; i++) + { + var ch = char.ToLower(message[i]); + if (IsSentenceEnd(ch) || i == message.Length - 1) + { + var length = i - sentenceBeginIndex; + if (length > 0) + { + var newLength = (int) Math.Clamp(Math.Cbrt(length) - 1, 1, 4); // 27+ chars for 2 phrases, 64+ for 3, 125+ for 4. + + for (var j = 0; j < newLength; j++) + { + var phrase = _random.Pick(language.Replacement); + builder.Append(phrase); + } + } + sentenceBeginIndex = i + 1; + + if (IsSentenceEnd(ch)) + builder.Append(ch).Append(" "); + } + } + } + + private static bool IsSentenceEnd(char ch) + { + return ch is '.' or '!' or '?'; + } + #endregion + + #region internal api - misc /// /// Dynamically resolves the current language of the entity and the list of all languages it speaks. /// The returned event is reused and thus must not be held as a reference anywhere but inside the caller function. @@ -255,50 +276,5 @@ private int PseudoRandomNumber(int seed, int min, int max) 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; } - - /// - /// Ensures the given entity has a valid language as its current language. - /// If not, sets it to the first entry of its SpokenLanguages list, or universal if it's empty. - /// - public void EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) - { - if (comp == null && !TryComp(entity, out comp)) - return; - - var langs = GetLanguages(entity, comp); - - if (langs != null && !langs.SpokenLanguages.Contains(comp!.CurrentLanguage, StringComparer.Ordinal)) - { - comp.CurrentLanguage = langs.SpokenLanguages.FirstOrDefault(UniversalPrototype); - RaiseLocalEvent(comp.Owner, new LanguagesUpdateEvent(), true); - } - } - - /// - /// 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. - /// - public sealed class DetermineEntityLanguagesEvent : EntityEventArgs - { - /// - /// 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! - /// - public List SpokenLanguages; - /// - /// The list of all languages the entity may understand. Must NOT be held as a reference! - /// - public List UnderstoodLanguages; - - public DetermineEntityLanguagesEvent(string currentLanguage, List spokenLanguages, List understoodLanguages) - { - CurrentLanguage = currentLanguage; - SpokenLanguages = spokenLanguages; - UnderstoodLanguages = understoodLanguages; - } - } + #endregion } diff --git a/Content.Server/Language/TranslatorImplanterSystem.cs b/Content.Server/Language/TranslatorImplanterSystem.cs index 0886dffdbcf..ab1b79b9e0e 100644 --- a/Content.Server/Language/TranslatorImplanterSystem.cs +++ b/Content.Server/Language/TranslatorImplanterSystem.cs @@ -5,6 +5,7 @@ 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; @@ -36,7 +37,7 @@ private void OnImplant(EntityUid implanter, TranslatorImplanterComponent compone return; } - var (_, understood) = _language.GetAllLanguages(target); + var understood = _language.GetAllLanguages(target).understood; if (component.RequiredLanguages.Count > 0 && !component.RequiredLanguages.Any(lang => understood.Contains(lang))) { RefusesPopup(implanter, target); @@ -60,7 +61,7 @@ private void OnImplant(EntityUid implanter, TranslatorImplanterComponent compone + $"\nSpoken: {string.Join(", ", component.SpokenLanguages)}; Understood: {string.Join(", ", component.UnderstoodLanguages)}"); OnAppearanceChange(implanter, component); - RaiseLocalEvent(target, new SharedLanguageSystem.LanguagesUpdateEvent(), true); + RaiseLocalEvent(target, new LanguagesUpdateEvent(), true); } private void RefusesPopup(EntityUid implanter, EntityUid target) diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index 98588c0551d..75902c9dc85 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Inventory.Events; using Content.Shared.Language; using Content.Shared.Language.Components; +using Content.Shared.Language.Events; using Content.Shared.Language.Systems; using Content.Shared.PowerCell; using static Content.Server.Language.LanguageSystem; @@ -33,22 +34,19 @@ public override void Initialize() _sawmill = Logger.GetSawmill("translator"); // I wanna die. But my death won't help us discover polymorphism. - SubscribeLocalEvent(ApplyTranslation); - SubscribeLocalEvent(ApplyTranslation); - SubscribeLocalEvent(ApplyTranslation); - // TODO: make this thing draw power - // SubscribeLocalEvent(...); + SubscribeLocalEvent(OnDetermineLanguages); + SubscribeLocalEvent(OnDetermineLanguages); + SubscribeLocalEvent(OnDetermineLanguages); SubscribeLocalEvent(OnTranslatorToggle); SubscribeLocalEvent(OnPowerCellSlotEmpty); - SubscribeLocalEvent( - (uid, component, args) => TranslatorEquipped(args.User, uid, component)); - SubscribeLocalEvent( - (uid, component, args) => TranslatorUnequipped(args.User, uid, component)); + // TODO: why does this use InteractHandEvent?? + SubscribeLocalEvent(OnTranslatorInteract); + SubscribeLocalEvent(OnTranslatorDropped); } - private void ApplyTranslation(EntityUid uid, IntrinsicTranslatorComponent component, + private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent component, DetermineEntityLanguagesEvent ev) { if (!component.Enabled) @@ -111,19 +109,21 @@ private void ApplyTranslation(EntityUid uid, IntrinsicTranslatorComponent compon } } - private void TranslatorEquipped(EntityUid holder, EntityUid translator, HandheldTranslatorComponent component) + private void OnTranslatorInteract( EntityUid translator, HandheldTranslatorComponent component, InteractHandEvent args) { + var holder = args.User; if (!EntityManager.HasComponent(holder)) return; - var intrinsic = EntityManager.EnsureComponent(holder); + var intrinsic = EnsureComp(holder); UpdateBoundIntrinsicComp(component, intrinsic, component.Enabled); UpdatedLanguages(holder); } - private void TranslatorUnequipped(EntityUid holder, EntityUid translator, HandheldTranslatorComponent component) + private void OnTranslatorDropped(EntityUid translator, HandheldTranslatorComponent component, DroppedEvent args) { + var holder = args.User; if (!EntityManager.TryGetComponent(holder, out var intrinsic)) return; @@ -131,7 +131,7 @@ private void TranslatorUnequipped(EntityUid holder, EntityUid translator, Handhe { intrinsic.Enabled = false; - EntityManager.RemoveComponent(holder, intrinsic); + RemCompDeferred(holder, intrinsic); } _language.EnsureValidLanguage(holder); @@ -149,7 +149,7 @@ private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponen if (Transform(args.Target).ParentUid is { Valid: true } holder && EntityManager.HasComponent(holder)) { // 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 = EntityManager.EnsureComponent(holder); + var intrinsic = EnsureComp(holder); var isEnabled = !component.Enabled; if (intrinsic.Issuer != component) { @@ -238,6 +238,6 @@ private static void AddIfNotExists(List list, string item) private void UpdatedLanguages(EntityUid uid) { - RaiseLocalEvent(uid, new SharedLanguageSystem.LanguagesUpdateEvent(), true); + RaiseLocalEvent(uid, new LanguagesUpdateEvent(), true); } } diff --git a/Content.Server/Mind/Commands/MakeSentientCommand.cs b/Content.Server/Mind/Commands/MakeSentientCommand.cs index 5efeb563bdc..ecd04b3b2b0 100644 --- a/Content.Server/Mind/Commands/MakeSentientCommand.cs +++ b/Content.Server/Mind/Commands/MakeSentientCommand.cs @@ -63,9 +63,7 @@ public static void MakeSentient(EntityUid uid, IEntityManager entityManager, boo var speaker = entityManager.EnsureComponent(uid); // The logic is simple, if the speaker knows any language (like monkey or robot), it should keep speaking that language if (speaker.SpokenLanguages.Count == 0) - { - language.AddLanguage(speaker, SharedLanguageSystem.GalacticCommon.ID); - } + language.AddLanguage(speaker, SharedLanguageSystem.FallbackLanguagePrototype); } entityManager.EnsureComponent(uid); diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 7a6b18490e8..b5d38a7001b 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -56,8 +56,8 @@ private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent component, ref RadioReceiveEvent args) { if (TryComp(uid, out ActorComponent? actor)) - { - // Frontier - languages mechanic + { + // Einstein-Engines - languages mechanic var listener = component.Owner; var msg = args.UnderstoodChatMsg; if (listener != null && !_language.CanUnderstand(listener, args.Language)) @@ -85,9 +85,7 @@ public void SendRadioMessage(EntityUid messageSource, string message, ProtoId [ByRefEvent] public readonly record struct RadioReceiveEvent( - // Frontier - languages mechanic + // Einstein-Engines - languages mechanic EntityUid MessageSource, RadioChannelPrototype Channel, ChatMessage UnderstoodChatMsg, diff --git a/Content.Shared/Language/Components/TranslatorComponent.cs b/Content.Shared/Language/Components/BaseTranslatorComponent.cs similarity index 91% rename from Content.Shared/Language/Components/TranslatorComponent.cs rename to Content.Shared/Language/Components/BaseTranslatorComponent.cs index 196363a8c9e..79c4c931070 100644 --- a/Content.Shared/Language/Components/TranslatorComponent.cs +++ b/Content.Shared/Language/Components/BaseTranslatorComponent.cs @@ -1,6 +1,3 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; namespace Content.Shared.Language.Components; @@ -19,14 +16,12 @@ public abstract partial class BaseTranslatorComponent : Component /// The list of additional languages this translator allows the wielder to speak. /// [DataField("spoken", customTypeSerializer: typeof(PrototypeIdListSerializer))] - [ViewVariables(VVAccess.ReadWrite)] public List SpokenLanguages = new(); /// /// The list of additional languages this translator allows the wielder to understand. /// [DataField("understood", customTypeSerializer: typeof(PrototypeIdListSerializer))] - [ViewVariables(VVAccess.ReadWrite)] public List UnderstoodLanguages = new(); /// @@ -34,7 +29,6 @@ public abstract partial class BaseTranslatorComponent : Component /// The field [RequiresAllLanguages] indicates whether all of them are required, or just one. /// [DataField("requires", customTypeSerializer: typeof(PrototypeIdListSerializer))] - [ViewVariables(VVAccess.ReadWrite)] public List RequiredLanguages = new(); /// @@ -47,7 +41,7 @@ public abstract partial class BaseTranslatorComponent : Component [ViewVariables(VVAccess.ReadWrite)] public bool RequiresAllLanguages = false; - [DataField("enabled")] + [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)] public bool Enabled = true; } diff --git a/Content.Shared/Language/Components/TranslatorImplanterComponent.cs b/Content.Shared/Language/Components/TranslatorImplanterComponent.cs index d198d1e5812..a2bdc8eeba0 100644 --- a/Content.Shared/Language/Components/TranslatorImplanterComponent.cs +++ b/Content.Shared/Language/Components/TranslatorImplanterComponent.cs @@ -24,7 +24,7 @@ public sealed partial class TranslatorImplanterComponent : Component /// /// If true, only allows to use this implanter on mobs. /// - [DataField("mobs-only")] + [DataField("mobsOnly")] public bool MobsOnly = true; /// diff --git a/Content.Shared/Language/Components/UniversalLanguageSpeakerComponent.cs b/Content.Shared/Language/Components/UniversalLanguageSpeakerComponent.cs index 68025546935..6f5ad1178b8 100644 --- a/Content.Shared/Language/Components/UniversalLanguageSpeakerComponent.cs +++ b/Content.Shared/Language/Components/UniversalLanguageSpeakerComponent.cs @@ -1,8 +1,8 @@ namespace Content.Shared.Language.Components; // -// Signifies that this entity can speak and understand any language. -// Applies to such entities as ghosts. +// Signifies that this entity can speak and understand any language. +// Applies to such entities as ghosts. // [RegisterComponent] public sealed partial class UniversalLanguageSpeakerComponent : Component diff --git a/Content.Shared/Language/Events/LanguagesUpdateEvent.cs b/Content.Shared/Language/Events/LanguagesUpdateEvent.cs new file mode 100644 index 00000000000..90ce2f4446b --- /dev/null +++ b/Content.Shared/Language/Events/LanguagesUpdateEvent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.Language.Events; + +/// +/// Raised on an entity when its list of languages changes. +/// +public sealed class LanguagesUpdateEvent : EntityEventArgs +{ +} diff --git a/Content.Shared/Language/Events/LanguagesUpdatedMessage.cs b/Content.Shared/Language/Events/LanguagesUpdatedMessage.cs new file mode 100644 index 00000000000..273659b86d3 --- /dev/null +++ b/Content.Shared/Language/Events/LanguagesUpdatedMessage.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Language.Events; + +/// +/// Sent to the client when its list of languages changes. The client should in turn update its HUD and relevant systems. +/// +[Serializable, NetSerializable] +public sealed class LanguagesUpdatedMessage(string currentLanguage, List spoken, List understood) : EntityEventArgs +{ + public string CurrentLanguage = currentLanguage; + public List Spoken = spoken; + public List Understood = understood; +} diff --git a/Content.Shared/Language/Events/RequestLanguagesMessage.cs b/Content.Shared/Language/Events/RequestLanguagesMessage.cs new file mode 100644 index 00000000000..aead1f4cd1a --- /dev/null +++ b/Content.Shared/Language/Events/RequestLanguagesMessage.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Language.Events; + +/// +/// Sent from the client to the server when it needs to learn the list of languages its entity knows. +/// This event should always be followed by a , unless the client doesn't have an entity. +/// +[Serializable, NetSerializable] +public sealed class RequestLanguagesMessage : EntityEventArgs; diff --git a/Content.Shared/Language/LanguagePrototype.cs b/Content.Shared/Language/LanguagePrototype.cs index 25b9391ecec..2dccf393cbe 100644 --- a/Content.Shared/Language/LanguagePrototype.cs +++ b/Content.Shared/Language/LanguagePrototype.cs @@ -13,29 +13,25 @@ public sealed class LanguagePrototype : IPrototype // If true, obfuscated phrases of creatures speaking this language will have their syllables replaced with "replacement" syllables. // Otherwise entire sentences will be replaced. // - [DataField("obfuscateSyllables", required: true)] - public bool ObfuscateSyllables { get; private set; } = false; + [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("replacement", required: true)] - public List Replacement = new(); + [DataField(required: true)] + public List Replacement = []; #region utility - - public string LocalizedName => GetLocalizedName(ID); - - public string LocalizedDescription => GetLocalizedDescription(ID); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetLocalizedName(string languageId) => - Loc.GetString("language-" + languageId + "-name"); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetLocalizedDescription(string languageId) => - Loc.GetString("language-" + languageId + "-description"); - + /// + /// The in-world name of this language, localized. + /// + public string Name => Loc.GetString($"language-{ID}-name"); + + /// + /// The in-world description of this language, localized. + /// + public string Description => Loc.GetString($"language-{ID}-description"); #endregion utility } diff --git a/Content.Shared/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/Language/Systems/SharedLanguageSystem.cs index 3794f3bf14d..55cf5980461 100644 --- a/Content.Shared/Language/Systems/SharedLanguageSystem.cs +++ b/Content.Shared/Language/Systems/SharedLanguageSystem.cs @@ -1,27 +1,33 @@ using Content.Shared.Actions; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Serialization; namespace Content.Shared.Language.Systems; public abstract class SharedLanguageSystem : EntitySystem { + /// + /// The language used as a fallback in cases where an entity suddenly becomes a language speaker (e.g. the usage of make-sentient) + /// [ValidatePrototypeId] - public static readonly string GalacticCommonPrototype = "GalacticCommon"; + public static readonly string FallbackLanguagePrototype = "GalacticCommon"; + /// + /// The language whose speakers are assumed to understand and speak every language. Should never be added directly. + /// [ValidatePrototypeId] public static readonly string UniversalPrototype = "Universal"; - public static LanguagePrototype GalacticCommon { get; private set; } = default!; + + /// + /// A cached instance of + /// public static LanguagePrototype Universal { get; private set; } = default!; + [Dependency] protected readonly IPrototypeManager _prototype = default!; [Dependency] protected readonly IRobustRandom _random = default!; - protected ISawmill _sawmill = default!; public override void Initialize() { - GalacticCommon = _prototype.Index("GalacticCommon"); Universal = _prototype.Index("Universal"); - _sawmill = Logger.GetSawmill("language"); } public LanguagePrototype? GetLanguage(string id) @@ -29,29 +35,4 @@ public override void Initialize() _prototype.TryIndex(id, out var proto); return proto; } - - /// - /// Raised on an entity when its list of languages changes. - /// - public sealed class LanguagesUpdateEvent : EntityEventArgs - { - } - - /// - /// Sent from the client to the server when it needs to learn the list of languages its entity knows. - /// This event should always be followed by a , unless the client doesn't have an entity. - /// - [Serializable, NetSerializable] - public sealed class RequestLanguagesMessage : EntityEventArgs; - - /// - /// Sent to the client when its list of languages changes. The client should in turn update its HUD and relevant systems. - /// - [Serializable, NetSerializable] - public sealed class LanguagesUpdatedMessage(string currentLanguage, List spoken, List understood) : EntityEventArgs - { - public string CurrentLanguage = currentLanguage; - public List Spoken = spoken; - public List Understood = understood; - } } diff --git a/Resources/Locale/en-US/language/commands.ftl b/Resources/Locale/en-US/language/commands.ftl new file mode 100644 index 00000000000..32fa5415b8c --- /dev/null +++ b/Resources/Locale/en-US/language/commands.ftl @@ -0,0 +1,8 @@ +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-language-select-desc = Select the currently spoken language of your entity. +command-language-select-help = Usage: {$command} . Example: {$command} GalacticCommon