diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 03f4f3f38b7..e8211060010 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -55,6 +55,7 @@ public static void SetupContexts(IInputContextContainer contexts) human.AddFunction(ContentKeyFunctions.UseItemInHand); human.AddFunction(ContentKeyFunctions.AltUseItemInHand); human.AddFunction(ContentKeyFunctions.OpenCharacterMenu); + human.AddFunction(ContentKeyFunctions.OpenLanguageMenu); // Frontier - Langauges human.AddFunction(ContentKeyFunctions.ActivateItemInWorld); human.AddFunction(ContentKeyFunctions.ThrowItemInHand); human.AddFunction(ContentKeyFunctions.AltActivateItemInWorld); diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index ce5cf421aef..d0aa57983a6 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -209,6 +209,7 @@ void AddCheckBox(string checkBoxName, bool currentState, Action UIManager.GetActiveUIWidgetOrNull(); @@ -44,6 +46,7 @@ public void UnloadButtons() _ahelp.UnloadButton(); _action.UnloadButton(); _sandbox.UnloadButton(); + _language.UnloadButton(); // Frontier - Languages } public void LoadButtons() @@ -56,5 +59,6 @@ public void LoadButtons() _ahelp.LoadButton(); _action.LoadButton(); _sandbox.LoadButton(); + _language.LoadButton(); // Frontier - Languages } } diff --git a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml index 3c8cd1d164f..2f4eaa63d02 100644 --- a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml +++ b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml @@ -63,6 +63,18 @@ HorizontalExpand="True" AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}" /> + + + + + + + + + + + + + + + + + diff --git a/Content.Client/_NF/Language/LanguageMenuWindow.xaml.cs b/Content.Client/_NF/Language/LanguageMenuWindow.xaml.cs new file mode 100644 index 00000000000..324deaaf527 --- /dev/null +++ b/Content.Client/_NF/Language/LanguageMenuWindow.xaml.cs @@ -0,0 +1,123 @@ +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 static Content.Shared.Language.Systems.SharedLanguageSystem; + +namespace Content.Client._NF.Language; // This EXACT class must have the _NF part because of xaml linking + +[GenerateTypedNameReferences] +public sealed partial class LanguageMenuWindow : DefaultWindow +{ + private readonly LanguageSystem _clientLanguageSystem; + private readonly List _entries = new(); + + 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 ?? "")); + + OptionsList.RemoveAllChildren(); + _entries.Clear(); + + foreach (var language in spokenLanguages) + { + AddLanguageEntry(language); + } + + // Disable the button for the currently chosen language + foreach (var entry in _entries) + { + if (entry.button != null) + entry.button.Disabled = entry.language == currentLanguage; + } + } + + private void AddLanguageEntry(string language) + { + var proto = _clientLanguageSystem.GetLanguage(language); + var state = new EntryState { language = language }; + + var container = new BoxContainer(); + container.Orientation = BoxContainer.LayoutOrientation.Vertical; + + // Create and add a header with the name and the button to select the language + { + var header = new BoxContainer(); + header.Orientation = BoxContainer.LayoutOrientation.Horizontal; + + header.Orientation = BoxContainer.LayoutOrientation.Horizontal; + header.HorizontalExpand = true; + header.SeparationOverride = 2; + + var name = new Label(); + name.Text = proto?.LocalizedName ?? ""; + name.MinWidth = 50; + name.HorizontalExpand = true; + + var button = new Button(); + button.Text = "Choose"; + button.OnPressed += _ => OnLanguageChosen(language); + state.button = button; + + header.AddChild(name); + header.AddChild(button); + + container.AddChild(header); + } + + // Create and add a collapsible description + { + var body = new CollapsibleBody(); + body.HorizontalExpand = true; + body.Margin = new Thickness(4f, 4f); + + var description = new RichTextLabel(); + description.SetMessage(proto?.LocalizedDescription ?? ""); + description.HorizontalExpand = true; + + body.AddChild(description); + + var collapser = new Collapsible(Loc.GetString("language-menu-description-header"), body); + collapser.Orientation = BoxContainer.LayoutOrientation.Vertical; + collapser.HorizontalExpand = true; + + container.AddChild(collapser); + } + + // Before adding, wrap the new container in a PanelContainer to give it a distinct look + var wrapper = new PanelContainer(); + wrapper.StyleClasses.Add("PdaBorderRect"); + + wrapper.AddChild(container); + OptionsList.AddChild(wrapper); + + _entries.Add(state); + } + + private void OnLanguageChosen(string id) + { + var proto = _clientLanguageSystem.GetLanguage(id); + if (proto != null) + _clientLanguageSystem.RequestSetLanguage(proto); + } + + private struct EntryState + { + public string language; + public Button? button; + } +} diff --git a/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorButton.cs b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorButton.cs new file mode 100644 index 00000000000..1e1cc65b8db --- /dev/null +++ b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorButton.cs @@ -0,0 +1,102 @@ +using System.Linq; +using System.Numerics; +using Content.Client.Language.Systems; +using Content.Client.UserInterface.Systems.Chat.Controls; +using Content.Client.UserInterface.Systems.Language; +using Content.Shared.Language; +using Robust.Client.UserInterface; +using Robust.Shared.Utility; + +namespace Content.Client._NF.Language.Systems.Chat.Controls; + +// Mostly copied from ChannelSelectorButton +public sealed class LanguageSelectorButton : ChatPopupButton +{ + public LanguagePrototype? SelectedLanguage { get; private set; } + + private const int SelectorDropdownOffset = 38; + + public LanguageSelectorButton() + { + Name = "LanguageSelector"; + + Popup.Selected += Select; + + if (Popup.FirstLanguage is { } firstSelector) + { + Select(firstSelector); + } + + 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; + var globalBot = GlobalPosition.Y + Height; + return UIBox2.FromDimensions( + new Vector2(globalLeft, globalBot), + 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; + + // if the language name is short enough, just return it + if (full || name.Length < 5) + return name; + + // If the language name is multi-word, collect first letters and capitalize them + if (name.Contains(' ')) + { + var result = name + .Split(" ") + .Select(it => it.FirstOrNull()) + .Where(it => it != null) + .Select(it => char.ToUpper(it!.Value)); + + return new string(result.ToArray()); + } + + // Alternatively, take the first 5 letters + return name[..5]; + } + + // public Color ChannelSelectColor(ChatSelectChannel channel) + // { + // return channel switch + // { + // ChatSelectChannel.Radio => Color.LimeGreen, + // ChatSelectChannel.LOOC => Color.MediumTurquoise, + // ChatSelectChannel.OOC => Color.LightSkyBlue, + // ChatSelectChannel.Dead => Color.MediumPurple, + // ChatSelectChannel.Admin => Color.HotPink, + // _ => Color.DarkGray + // }; + // } +} diff --git a/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorItemButton.cs b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorItemButton.cs new file mode 100644 index 00000000000..caa5eb31219 --- /dev/null +++ b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorItemButton.cs @@ -0,0 +1,28 @@ +using Content.Client.Stylesheets; +using Content.Client.UserInterface.Systems.Chat; +using Content.Client.UserInterface.Systems.Chat.Controls; +using Content.Shared.Chat; +using Content.Shared.Language; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client._NF.Language.Systems.Chat.Controls; + +// Mostly copied from ChannelSelectorItemButton +public sealed class LanguageSelectorItemButton : Button +{ + public readonly LanguagePrototype Language; + + public bool IsHidden => Parent == null; + + public LanguageSelectorItemButton(LanguagePrototype language) + { + Language = language; + AddStyleClass(StyleNano.StyleClassChatChannelSelectorButton); + + Text = LanguageSelectorButton.LanguageSelectorName(language, full: true); + + // var prefix = ChatUIController.ChannelPrefixes[selector]; + // if (prefix != default) + // Text = Loc.GetString("hud-chatbox-select-name-prefixed", ("name", Text), ("prefix", prefix)); + } +} diff --git a/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorPopup.cs b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorPopup.cs new file mode 100644 index 00000000000..a536fc30795 --- /dev/null +++ b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorPopup.cs @@ -0,0 +1,78 @@ +using Content.Client.Language.Systems; +using Content.Client.UserInterface.Systems.Language; +using Content.Shared.Language; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using static Robust.Client.UserInterface.Controls.BaseButton; + +namespace Content.Client._NF.Language.Systems.Chat.Controls; + +// Mostly copied from LanguageSelectorPopup +public sealed class LanguageSelectorPopup : Popup +{ + private readonly BoxContainer _channelSelectorHBox; + private readonly Dictionary _selectorStates = new(); + + public event Action? Selected; + + public LanguageSelectorPopup() + { + _channelSelectorHBox = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Horizontal, + SeparationOverride = 1 + }; + + AddChild(_channelSelectorHBox); + } + + public LanguagePrototype? FirstLanguage + { + get + { + foreach (var selector in _selectorStates.Values) + { + if (!selector.IsHidden) + return selector.Language; + } + + return null; + } + } + + public void SetLanguages(List languages) + { + var languageSystem = IoCManager.Resolve().GetEntitySystem(); + _channelSelectorHBox.RemoveAllChildren(); + + foreach (var language in languages) + { + if (!_selectorStates.TryGetValue(language, out var selector)) + { + var proto = languageSystem.GetLanguage(language); + if (proto == null) + continue; + + selector = new LanguageSelectorItemButton(proto); + _selectorStates[language] = selector; + selector.OnPressed += OnSelectorPressed; + } + + if (selector.IsHidden) + { + _channelSelectorHBox.AddChild(selector); + } + } + } + + private void OnSelectorPressed(ButtonEventArgs args) + { + var button = (LanguageSelectorItemButton) args.Button; + Select(button.Language); + } + + private void Select(LanguagePrototype language) + { + Selected?.Invoke(language); + } +} diff --git a/Content.Client/_NF/Language/Systems/LanguageSystem.cs b/Content.Client/_NF/Language/Systems/LanguageSystem.cs new file mode 100644 index 00000000000..f7f1d1ebb48 --- /dev/null +++ b/Content.Client/_NF/Language/Systems/LanguageSystem.cs @@ -0,0 +1,70 @@ +using Content.Shared.Language; +using Content.Shared.Language.Systems; +using Content.Shared.Mind.Components; +using Robust.Shared.Console; + +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 +{ + /// + /// The current language of the entity currently possessed by the player. + /// + public string CurrentLanguage { get; private set; } = default!; + /// + /// The list of languages the currently possessed entity can speak. + /// + public List SpokenLanguages { get; private set; } = new(); + /// + /// The list of languages the currently possessed entity can understand. + /// + public List UnderstoodLanguages { get; private set; } = new(); + + public Action<(string current, List spoken, List understood)>? LanguagesUpdatedHook; + + [Dependency] private readonly IConsoleHost _consoleHost = default!; + + public override void Initialize() + { + SubscribeNetworkEvent(OnLanguagesUpdated); + } + + /// + /// Sends a network request to the server to update this system's state. + /// The server may ignore the said request if the player is not possessing an entity. + /// + public void RequestStateUpdate() + { + RaiseNetworkEvent(new RequestLanguagesMessage()); + } + + public void RequestSetLanguage(LanguagePrototype language) + { + // May cause some minor desync... + if (language.ID == CurrentLanguage) + return; + + // (This is dumb. This is very dumb. It should be a message instead.) + _consoleHost.ExecuteCommand("lsselectlang " + language.ID); + + // So to reduce the probability of desync, we replicate the change locally too + if (SpokenLanguages.Contains(language.ID)) + CurrentLanguage = language.ID; + } + + private void OnLanguagesUpdated(LanguagesUpdatedMessage message) + { + CurrentLanguage = message.CurrentLanguage; + SpokenLanguages = message.Spoken; + UnderstoodLanguages = message.Understood; + + // Pleeease do not mutate it inside the hook, or the universe will crash and collapse and I will come to your house at 3 am and then the police will never find your body + LanguagesUpdatedHook?.Invoke((CurrentLanguage, SpokenLanguages, UnderstoodLanguages)); + } +} diff --git a/Content.Client/_NF/Language/Systems/TranslatorImplanterSystem.cs b/Content.Client/_NF/Language/Systems/TranslatorImplanterSystem.cs new file mode 100644 index 00000000000..da19b3decf9 --- /dev/null +++ b/Content.Client/_NF/Language/Systems/TranslatorImplanterSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Language.Systems; + +namespace Content.Client.Language.Systems; + +public sealed class TranslatorImplanterSystem : SharedTranslatorImplanterSystem +{ + +} diff --git a/Content.Client/_NF/UserInterface/Systems/Language/LanguageMenuUIController.cs b/Content.Client/_NF/UserInterface/Systems/Language/LanguageMenuUIController.cs new file mode 100644 index 00000000000..abed8720ff3 --- /dev/null +++ b/Content.Client/_NF/UserInterface/Systems/Language/LanguageMenuUIController.cs @@ -0,0 +1,128 @@ +using Content.Client._NF.Language; +using Content.Client.Gameplay; +using Content.Client.Language.Systems; +using Robust.Client.UserInterface.Controllers; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Console; +using static Content.Shared.Language.Systems.SharedLanguageSystem; +using Content.Client.UserInterface.Controls; +using Content.Shared.Input; +using Content.Shared.Language; +using JetBrains.Annotations; +using Robust.Shared.Input.Binding; +using Robust.Shared.Utility; +using static Robust.Client.UserInterface.Controls.BaseButton; + +namespace Content.Client.UserInterface.Systems.Language; + +[UsedImplicitly] +public sealed class LanguageMenuUIController : UIController, IOnStateEntered, IOnStateExited +{ + public LanguageMenuWindow? _languageWindow; + private MenuButton? LanguageButton => UIManager.GetActiveUIWidgetOrNull()?.LanguageButton; + + /// + /// A hook similar to LanguageSystem.LanguagesUpdatedHook, but safe to use from ui code. + /// This is a dirty workaround and I hate it. + /// + public Action<(string current, List spoken, List understood)>? LanguagesUpdatedHook; + + public override void Initialize() + { + LanguagesUpdatedHook += (args) => + { + if (_languageWindow != null) + { + _languageWindow.UpdateState(args.current, args.spoken); + } + }; + } + + public void OnStateEntered(GameplayState state) + { + DebugTools.Assert(_languageWindow == null); + + var clientLanguageSystem = IoCManager.Resolve().GetEntitySystem(); + clientLanguageSystem.LanguagesUpdatedHook += LanguagesUpdatedHook; + + _languageWindow = UIManager.CreateWindow(); + LayoutContainer.SetAnchorPreset(_languageWindow, LayoutContainer.LayoutPreset.CenterTop); + + CommandBinds.Builder.Bind(ContentKeyFunctions.OpenLanguageMenu, InputCmdHandler.FromDelegate(_ => ToggleWindow())).Register(); + } + + public void OnStateExited(GameplayState state) + { + if (_languageWindow != null) + { + _languageWindow.Dispose(); + _languageWindow = null; + } + + var clientLanguageSystem = IoCManager.Resolve().GetEntitySystem(); + clientLanguageSystem.LanguagesUpdatedHook -= LanguagesUpdatedHook; + + CommandBinds.Unregister(); + } + + public void UnloadButton() + { + if (LanguageButton == null) + { + return; + } + + LanguageButton.OnPressed -= LanguageButtonPressed; + } + + public void LoadButton() + { + if (LanguageButton == null) + { + return; + } + + LanguageButton.OnPressed += LanguageButtonPressed; + + if (_languageWindow == null) + { + return; + } + + _languageWindow.OnClose += DeactivateButton; + _languageWindow.OnOpen += ActivateButton; + } + + private void DeactivateButton() => LanguageButton!.Pressed = false; + private void ActivateButton() => LanguageButton!.Pressed = true; + + private void LanguageButtonPressed(ButtonEventArgs args) + { + ToggleWindow(); + } + + private void CloseWindow() + { + _languageWindow?.Close(); + } + + private void ToggleWindow() + { + if (_languageWindow == null) + return; + + if (LanguageButton != null) + { + LanguageButton.SetClickPressed(!_languageWindow.IsOpen); + } + + if (_languageWindow.IsOpen) + { + CloseWindow(); + } + else + { + _languageWindow.Open(); + } + } +} diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index aff5ebe8b32..c49030cdc90 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -5,6 +5,8 @@ using Content.Server.Administration.Managers; using Content.Server.Chat.Managers; using Content.Server.GameTicking; +using Content.Server.Language; +using Content.Server.Speech; using Content.Server.Speech.Components; using Content.Server.Speech.EntitySystems; using Content.Server.Station.Components; @@ -17,6 +19,7 @@ using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; +using Content.Shared.Language; using Content.Shared.Mobs.Systems; using Content.Shared.Players; using Content.Shared.Radio; @@ -42,6 +45,8 @@ namespace Content.Server.Chat.Systems; /// public sealed partial class ChatSystem : SharedChatSystem { + public const float DefaultObfuscationFactor = 0.2f; + [Dependency] private readonly IReplayRecordingManager _replay = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; @@ -57,6 +62,7 @@ public sealed partial class ChatSystem : SharedChatSystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly ReplacementAccentSystem _wordreplacement = default!; + [Dependency] private readonly LanguageSystem _language = default!; public const int VoiceRange = 10; // how far voice goes in world units public const int WhisperClearRange = 2; // how far whisper goes while still being understandable, in world units @@ -167,7 +173,8 @@ public void TrySendInGameICMessage( ICommonSession? player = null, string? nameOverride = null, bool checkRadioPrefix = true, - bool ignoreActionBlocker = false + bool ignoreActionBlocker = false, + LanguagePrototype? languageOverride = null // Frontier - languages mechanic ) { if (HasComp(source)) @@ -242,11 +249,12 @@ public void TrySendInGameICMessage( // Otherwise, send whatever type. switch (desiredType) { + // Frontier - languages mechanic case InGameICChatType.Speak: - SendEntitySpeak(source, message, range, nameOverride, hideLog, ignoreActionBlocker); + SendEntitySpeak(source, message, range, nameOverride, hideLog, ignoreActionBlocker, languageOverride: languageOverride); break; case InGameICChatType.Whisper: - SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker); + SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker, languageOverride: languageOverride); break; case InGameICChatType.Emote: SendEntityEmote(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker); @@ -372,7 +380,9 @@ private void SendEntitySpeak( ChatTransmitRange range, string? nameOverride, bool hideLog = false, - bool ignoreActionBlocker = false + bool ignoreActionBlocker = false, + LanguagePrototype? languageOverride = null // Frontier: languages mechanic + ) { if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker) @@ -401,23 +411,23 @@ private void SendEntitySpeak( speech = proto; } - name = FormattedMessage.EscapeText(name); + // Frontier - languages mechanic + var language = languageOverride ?? _language.GetLanguage(source); - var wrappedMessage = Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message", - ("entityName", name), - ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), - ("fontType", speech.FontId), - ("fontSize", speech.FontSize), - ("message", FormattedMessage.EscapeText(message))); + name = FormattedMessage.EscapeText(name); + var wrappedMessage = WrapPublicMessage(source, name, message); + var obfuscated = _language.ObfuscateSpeech(source, message, language); + var wrappedObfuscated = WrapPublicMessage(source, name, obfuscated); - SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range); + // Frontier: languages mechanic + SendInVoiceRange(ChatChannel.Local, name, message, wrappedMessage, obfuscated, wrappedObfuscated, source, range, languageOverride: language); - var ev = new EntitySpokeEvent(source, message, null, null); + var ev = new EntitySpokeEvent(source, message, null, false, language); RaiseLocalEvent(source, ev, true); // To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc. // Also doesn't log if hideLog is true. - if (!HasComp(source) || hideLog == true) + if (!HasComp(source) || hideLog) return; if (originalMessage == message) @@ -445,7 +455,9 @@ private void SendEntityWhisper( RadioChannelPrototype? channel, string? nameOverride, bool hideLog = false, - bool ignoreActionBlocker = false + bool ignoreActionBlocker = false, + LanguagePrototype? languageOverride = null // Frontier: languages mechanic + ) { if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker) @@ -455,8 +467,8 @@ private void SendEntityWhisper( if (message.Length == 0) return; - var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f); - + // Frontier - languages mechanic + // var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f); // get the entity's name by visual identity (if no override provided). string nameIdentity = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(source, EntityManager)); // get the entity's name by voice (if no override provided). @@ -473,41 +485,58 @@ private void SendEntityWhisper( } name = FormattedMessage.EscapeText(name); - var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message", - ("entityName", name), ("message", FormattedMessage.EscapeText(message))); - - var wrappedobfuscatedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message", - ("entityName", nameIdentity), ("message", FormattedMessage.EscapeText(obfuscatedMessage))); - - var wrappedUnknownMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message", - ("message", FormattedMessage.EscapeText(obfuscatedMessage))); - + // Frontier - languages mechanic (+ everything in the foreach loop) + var language = languageOverride ?? _language.GetLanguage(source); + var languageObfuscatedMessage = _language.ObfuscateSpeech(source, message, language); + // There are 6 possible message states. It's not worth to precompute everything here. foreach (var (session, data) in GetRecipients(source, WhisperMuffledRange)) { - EntityUid listener; - - if (session.AttachedEntity is not { Valid: true } playerEntity) + if (session.AttachedEntity is not { Valid: true } listener) continue; - listener = session.AttachedEntity.Value; - 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 canUnderstand = _language.CanUnderstand(listener, language); + var finalMessage = canUnderstand ? message : languageObfuscatedMessage; + if (data.Range <= WhisperClearRange) - _chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel); - //If listener is too far, they only hear fragments of the message - //Collisiongroup.Opaque is not ideal for this use. Preferably, there should be a check specifically with "Can Ent1 see Ent2" in mind - else if (_interactionSystem.InRangeUnobstructed(source, listener, WhisperMuffledRange, Shared.Physics.CollisionGroup.Opaque)) //Shared.Physics.CollisionGroup.Opaque - _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.Channel); - //If listener is too far and has no line of sight, they can't identify the whisperer's identity + { + var wrappedResult = Loc.GetString("chat-manager-entity-whisper-wrap-message", + ("entityName", name), + ("message", FormattedMessage.EscapeText(finalMessage))); + + // If the listener is in the clear range, do not perform further obfuscations + _chatManager.ChatMessageToOne(ChatChannel.Whisper, finalMessage, wrappedResult, source, false, session.Channel); + } + else if (_interactionSystem.InRangeUnobstructed(source, listener, WhisperMuffledRange, Shared.Physics.CollisionGroup.Opaque)) + { + + // If the listener is too far, they only hear fragments of the message + // Collisiongroup.Opaque is not ideal for this use. Preferably, there should be a check specifically with "Can Ent1 see Ent2" in mind + var result = ObfuscateMessageReadability(finalMessage); + var wrappedResult = Loc.GetString("chat-manager-entity-whisper-wrap-message", + ("entityName", nameIdentity), ("message", FormattedMessage.EscapeText(result))); + + _chatManager.ChatMessageToOne(ChatChannel.Whisper, result, wrappedResult, source, false, session.Channel); + } else - _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedUnknownMessage, source, false, session.Channel); + { + //If listener is too far and has no line of sight, they can't identify the whisperer's identity + var result = ObfuscateMessageReadability(finalMessage); + var wrappedResult = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message", + ("message", FormattedMessage.EscapeText(result))); + + _chatManager.ChatMessageToOne(ChatChannel.Whisper, result, wrappedResult, source, false, session.Channel); + } } - _replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); + var replayWrap = Loc.GetString("chat-manager-entity-whisper-wrap-message", + ("entityName", name), + ("message", FormattedMessage.EscapeText(message))); + _replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, replayWrap, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); - var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage); + var ev = new EntitySpokeEvent(source, message, channel, true, language); RaiseLocalEvent(source, ev, true); if (!hideLog) if (originalMessage == message) @@ -554,7 +583,8 @@ private void SendEntityEmote( if (checkEmote) TryEmoteChatInput(source, action); - SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author); + // Frontier - languages (emotes are skipped in obfuscation check) + SendInVoiceRange(ChatChannel.Emotes, name, action, wrappedMessage, obfuscated: "", obfuscatedWrappedMessage: "", source, range, author); if (!hideLog) if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user} as {name}: {action}"); @@ -581,7 +611,14 @@ private void SendLOOC(EntityUid source, ICommonSession player, string message, b ("entityName", name), ("message", FormattedMessage.EscapeText(message))); - SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, player.UserId); + // Frontier - languages mechanic + SendInVoiceRange(ChatChannel.LOOC, name, message, wrappedMessage, + obfuscated: string.Empty, + obfuscatedWrappedMessage: string.Empty, // will be skipped anyway + source, + hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, + player.UserId, + languageOverride: LanguageSystem.Universal); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}"); } @@ -662,15 +699,32 @@ private MessageRangeCheckResult MessageRangeCheck(ICommonSession session, ICChat /// /// Sends a chat message to the given players in range of the source entity. /// - private void SendInVoiceRange(ChatChannel channel, string message, string wrappedMessage, EntityUid source, ChatTransmitRange range, NetUserId? author = null) + private void SendInVoiceRange(ChatChannel channel, string name, string message, string wrappedMessage, string obfuscated, string obfuscatedWrappedMessage, EntityUid source, ChatTransmitRange range, NetUserId? author = null, LanguagePrototype? languageOverride = null) { + + var language = languageOverride ?? _language.GetLanguage(source); // frontier + foreach (var (session, data) in GetRecipients(source, VoiceRange)) { var entRange = MessageRangeCheck(session, data, range); if (entRange == MessageRangeCheckResult.Disallowed) continue; var entHideChat = entRange == MessageRangeCheckResult.HideChat; - _chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author); + + // Frontier - languages mechanic + if (session.AttachedEntity is not { Valid: true } playerEntity) + continue; + EntityUid listener = session.AttachedEntity.Value; + + + if (channel == ChatChannel.LOOC || channel == ChatChannel.Emotes || _language.CanUnderstand(listener, language)) + { + _chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author); + } + else + { + _chatManager.ChatMessageToOne(channel, obfuscated, obfuscatedWrappedMessage, source, entHideChat, session.Channel, author: author); + } } _replay.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); @@ -780,6 +834,19 @@ public string SanitizeMessageReplaceWords(string message) return msg; } + // Frontier - languages mechanic + public string WrapPublicMessage(EntityUid source, string name, string message) + { + var speech = GetSpeechVerb(source, message); + var verbName = Loc.GetString(_random.Pick(speech.SpeechVerbStrings)); + return Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message", + ("entityName", name), + ("verb", verbName), + ("fontType", speech.FontId), + ("fontSize", speech.FontSize), + ("message", FormattedMessage.EscapeText(message))); + } + /// /// Returns list of players and ranges for all players withing some range. Also returns observers with a range of -1. /// @@ -826,7 +893,7 @@ public readonly record struct ICChatRecipientData(float Range, bool Observer, bo { } - private string ObfuscateMessageReadability(string message, float chance) + public string ObfuscateMessageReadability(string message, float chance = DefaultObfuscationFactor) { var modifiedMessage = new StringBuilder(message); @@ -915,7 +982,9 @@ public sealed class EntitySpokeEvent : EntityEventArgs { public readonly EntityUid Source; public readonly string Message; - public readonly string? ObfuscatedMessage; // not null if this was a whisper + // Frontier - languages mechanic + public readonly bool IsWhisper; + public readonly LanguagePrototype Language; /// /// If the entity was trying to speak into a radio, this was the channel they were trying to access. If a radio @@ -923,12 +992,13 @@ public sealed class EntitySpokeEvent : EntityEventArgs /// public RadioChannelPrototype? Channel; - public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, string? obfuscatedMessage) + public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, bool isWhisper, LanguagePrototype language) { Source = source; Message = message; Channel = channel; - ObfuscatedMessage = obfuscatedMessage; + IsWhisper = isWhisper; + Language = language; } } diff --git a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs index bbbf906f47e..d970aa948d6 100644 --- a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs +++ b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs @@ -1,6 +1,9 @@ +using System.Linq; using Content.Server.Ghost.Roles.Components; 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.Shared.Humanoid; //Delta-V - Banning humanoids from becoming ghost roles. @@ -23,6 +26,22 @@ public override void Effect(ReagentEffectArgs args) entityManager.RemoveComponent(uid); entityManager.RemoveComponent(uid); + // Frontier - languages mechanic + // Try to make the entity speak GalacticCommon - the default language for sentient species + var speaker = entityManager.EnsureComponent(uid); + var gc = SharedLanguageSystem.GalacticCommon.ID; + + if (!speaker.UnderstoodLanguages.Contains(gc)) + speaker.UnderstoodLanguages.Add(gc); + + if (!speaker.SpokenLanguages.Contains(gc)) + { + speaker.CurrentLanguage = gc; + speaker.SpokenLanguages.Add(gc); + + args.EntityManager.EventBus.RaiseLocalEvent(uid, new SharedLanguageSystem.LanguagesUpdateEvent(), true); + } + // 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/Mind/Commands/MakeSentientCommand.cs b/Content.Server/Mind/Commands/MakeSentientCommand.cs index 5e19d135b6f..5efeb563bdc 100644 --- a/Content.Server/Mind/Commands/MakeSentientCommand.cs +++ b/Content.Server/Mind/Commands/MakeSentientCommand.cs @@ -1,7 +1,10 @@ using Content.Server.Administration; +using Content.Server.Language; using Content.Shared.Administration; using Content.Shared.Emoting; using Content.Shared.Examine; +using Content.Shared.Language; +using Content.Shared.Language.Systems; using Content.Shared.Mind.Components; using Content.Shared.Movement.Components; using Content.Shared.Speech; @@ -55,6 +58,14 @@ public static void MakeSentient(EntityUid uid, IEntityManager entityManager, boo { entityManager.EnsureComponent(uid); entityManager.EnsureComponent(uid); + + var language = IoCManager.Resolve().GetEntitySystem(); + 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); + } } entityManager.EnsureComponent(uid); diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index d18b044205c..71905fe6507 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -1,6 +1,9 @@ using Content.Server.Chat.Systems; using Content.Server.Emp; +using Content.Server.Language; using Content.Server.Radio.Components; +using Content.Server.Speech; +using Content.Shared.Chat; using Content.Shared.Inventory.Events; using Content.Shared.Radio; using Content.Shared.Radio.Components; @@ -14,6 +17,7 @@ public sealed class HeadsetSystem : SharedHeadsetSystem { [Dependency] private readonly INetManager _netMan = default!; [Dependency] private readonly RadioSystem _radio = default!; + [Dependency] private readonly LanguageSystem _language = default!; // Frontier - languages mechanic public override void Initialize() { @@ -99,8 +103,17 @@ public void SetEnabled(EntityUid uid, bool value, HeadsetComponent? component = private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref RadioReceiveEvent args) { - if (TryComp(Transform(uid).ParentUid, out ActorComponent? actor)) - _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel); + // Frontier - languages mechanic + var parent = Transform(uid).ParentUid; + if (TryComp(parent, out ActorComponent? actor)) + { + var canUnderstand = _language.CanUnderstand(parent, args.Language); + var msg = new MsgChatMessage + { + Message = canUnderstand ? args.UnderstoodChatMsg : args.NotUnderstoodChatMsg + }; + _netMan.ServerSendMessage(msg, actor.PlayerSession.Channel); + } } private void OnEmpPulse(EntityUid uid, HeadsetComponent component, ref EmpPulseEvent args) diff --git a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs index ace7d8ae31a..8be93a595ef 100644 --- a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.Interaction; +using Content.Server.Language; using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; @@ -29,6 +30,7 @@ public sealed class RadioDeviceSystem : EntitySystem [Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!; + [Dependency] private readonly LanguageSystem _language = default!; // Used to prevent a shitter from using a bunch of radios to spam chat. private HashSet<(string, EntityUid)> _recentlySent = new(); @@ -208,7 +210,9 @@ private void OnReceiveRadio(EntityUid uid, RadioSpeakerComponent component, ref ("originalName", nameEv.Name)); // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios - _chat.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Whisper, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false); + // Frontier - languages mechanic + var message = args.UnderstoodChatMsg.Message; // The chat system will handle the rest and re-obfuscate if needed. + _chat.TrySendInGameICMessage(uid, message, InGameICChatType.Whisper, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false, languageOverride: args.Language); } private void OnBeforeIntercomUiOpen(EntityUid uid, IntercomComponent component, BeforeActivatableUIOpenEvent args) diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 7b54d7daff7..96c8fa2467b 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -1,10 +1,13 @@ using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; +using Content.Server.Language; using Content.Server.Power.Components; using Content.Server.Radio.Components; +using Content.Server.Speech; using Content.Server.VoiceMask; using Content.Shared.Chat; using Content.Shared.Database; +using Content.Shared.Language; using Content.Shared.Radio; using Content.Shared.Radio.Components; using Robust.Server.GameObjects; @@ -31,6 +34,7 @@ public sealed class RadioSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly LanguageSystem _language = default!; // set used to prevent radio feedback loops. private readonly HashSet _messages = new(); @@ -46,7 +50,7 @@ private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent { if (args.Channel != null && component.Channels.Contains(args.Channel.ID)) { - SendRadioMessage(uid, args.Message, args.Channel, uid); + SendRadioMessage(uid, args.Message, args.Channel, uid, args.Language); args.Channel = null; // prevent duplicate messages from other listeners. } } @@ -54,15 +58,25 @@ private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent component, ref RadioReceiveEvent args) { if (TryComp(uid, out ActorComponent? actor)) - _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel); + { + // Frontier - languages mechanic + var listener = component.Owner; + var msg = args.UnderstoodChatMsg; + if (listener != null && !_language.CanUnderstand(listener, args.Language)) + { + msg = args.NotUnderstoodChatMsg; + } + + _netMan.ServerSendMessage(new MsgChatMessage { Message = msg}, actor.PlayerSession.Channel); + } } /// /// Send radio message to all active radio listeners /// - public void SendRadioMessage(EntityUid messageSource, string message, ProtoId channel, EntityUid radioSource, bool escapeMarkup = true) + public void SendRadioMessage(EntityUid messageSource, string message, ProtoId channel, EntityUid radioSource, LanguagePrototype? language = null, bool escapeMarkup = true) { - SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup); + SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup, language: language); } /// @@ -70,8 +84,15 @@ public void SendRadioMessage(EntityUid messageSource, string message, ProtoId /// Entity that spoke the message /// Entity that picked up the message and will send it, e.g. headset - public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, bool escapeMarkup = true) + /// The language to send the message in. + public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, LanguagePrototype? language = null, bool escapeMarkup = true) { + // Frontier - languages mechanic + if (language == null) + { + language = _language.GetLanguage(messageSource); + } + // TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this. if (!_messages.Add(message)) return; @@ -99,6 +120,7 @@ public void SendRadioMessage(EntityUid messageSource, string message, RadioChann name = FormattedMessage.EscapeText(name); + // most radios are relayed to chat, so lets parse the chat message beforehand SpeechVerbPrototype speech; if (mask != null && mask.Enabled @@ -114,24 +136,17 @@ public void SendRadioMessage(EntityUid messageSource, string message, RadioChann ? FormattedMessage.EscapeText(message) : message; - var wrappedMessage = Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap", - ("color", channel.Color), - ("fontType", speech.FontId), - ("fontSize", speech.FontSize), - ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), - ("channel", $"\\[{channel.LocalizedName}\\]"), - ("name", name), - ("message", content)); + // Frontier - languages mechanic + // A message that the listener could understand + var wrappedMessage = WrapRadioMessage(messageSource, channel, name, content); + var msg = new ChatMessage(ChatChannel.Radio, content, wrappedMessage, NetEntity.Invalid, null); - // most radios are relayed to chat, so lets parse the chat message beforehand - var chat = new ChatMessage( - ChatChannel.Radio, - message, - wrappedMessage, - NetEntity.Invalid, - null); - var chatMsg = new MsgChatMessage { Message = chat }; - var ev = new RadioReceiveEvent(message, messageSource, channel, chatMsg); + // ... you guess it + var obfuscated = _language.ObfuscateSpeech(null, content, language); + var obfuscatedWrapped = WrapRadioMessage(messageSource, channel, name, obfuscated); + var notUdsMsg = new ChatMessage(ChatChannel.Radio, obfuscated, obfuscatedWrapped, NetEntity.Invalid, null); + + var ev = new RadioReceiveEvent(messageSource, channel, msg, notUdsMsg, language); var sendAttemptEv = new RadioSendAttemptEvent(channel, radioSource); RaiseLocalEvent(ref sendAttemptEv); @@ -177,10 +192,24 @@ public void SendRadioMessage(EntityUid messageSource, string message, RadioChann else _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(messageSource):user} on {channel.LocalizedName}: {message}"); - _replay.RecordServerMessage(chat); + _replay.RecordServerMessage(msg); _messages.Remove(message); } + // Frontier - languages mechanic (extracted from above) + private string WrapRadioMessage(EntityUid source, RadioChannelPrototype channel, string name, string message) + { + var speech = _chat.GetSpeechVerb(source, message); + return Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap", + ("color", channel.Color), + ("fontType", speech.FontId), + ("fontSize", speech.FontSize), + ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), + ("channel", $"\\[{channel.LocalizedName}\\]"), + ("name", name), + ("message", FormattedMessage.EscapeText(message))); + } + /// private bool HasActiveServer(MapId mapId, string channelId) { diff --git a/Content.Server/Radio/RadioEvent.cs b/Content.Server/Radio/RadioEvent.cs index 69d764ffe67..5db1465e03f 100644 --- a/Content.Server/Radio/RadioEvent.cs +++ b/Content.Server/Radio/RadioEvent.cs @@ -1,10 +1,22 @@ using Content.Shared.Chat; +using Content.Shared.Language; using Content.Shared.Radio; namespace Content.Server.Radio; +/// +/// The message to display when the speaker can understand "language" +/// The message to display when the speaker cannot understand "language" +/// [ByRefEvent] -public readonly record struct RadioReceiveEvent(string Message, EntityUid MessageSource, RadioChannelPrototype Channel, MsgChatMessage ChatMsg); +public readonly record struct RadioReceiveEvent( + // Frontier - languages mechanic + EntityUid MessageSource, + RadioChannelPrototype Channel, + ChatMessage UnderstoodChatMsg, + ChatMessage NotUnderstoodChatMsg, + LanguagePrototype Language +); /// /// Use this event to cancel sending message per receiver diff --git a/Content.Server/Speech/EntitySystems/ListeningSystem.cs b/Content.Server/Speech/EntitySystems/ListeningSystem.cs index 3fb98f502c8..72ecc5015c8 100644 --- a/Content.Server/Speech/EntitySystems/ListeningSystem.cs +++ b/Content.Server/Speech/EntitySystems/ListeningSystem.cs @@ -8,6 +8,7 @@ namespace Content.Server.Speech.EntitySystems; /// public sealed class ListeningSystem : EntitySystem { + [Dependency] private readonly ChatSystem _chat = default!; // Frontier - languages mechanic [Dependency] private readonly SharedTransformSystem _xforms = default!; public override void Initialize() @@ -18,10 +19,10 @@ public override void Initialize() private void OnSpeak(EntitySpokeEvent ev) { - PingListeners(ev.Source, ev.Message, ev.ObfuscatedMessage); + PingListeners(ev.Source, ev.Message, ev.IsWhisper); } - public void PingListeners(EntityUid source, string message, string? obfuscatedMessage) + public void PingListeners(EntityUid source, string message, bool isWhisper) { // TODO whispering / audio volume? Microphone sensitivity? // for now, whispering just arbitrarily reduces the listener's max range. @@ -32,7 +33,8 @@ public void PingListeners(EntityUid source, string message, string? obfuscatedMe var attemptEv = new ListenAttemptEvent(source); var ev = new ListenEvent(message, source); - var obfuscatedEv = obfuscatedMessage == null ? null : new ListenEvent(obfuscatedMessage, source); + // Frontier - languages mechanic + var obfuscatedEv = !isWhisper ? null : new ListenEvent(_chat.ObfuscateMessageReadability(message), source); var query = EntityQueryEnumerator(); while (query.MoveNext(out var listenerUid, out var listener, out var xform)) diff --git a/Content.Server/_NF/Language/Commands/ListLanguagesCommand.cs b/Content.Server/_NF/Language/Commands/ListLanguagesCommand.cs new file mode 100644 index 00000000000..d64573daeba --- /dev/null +++ b/Content.Server/_NF/Language/Commands/ListLanguagesCommand.cs @@ -0,0 +1,39 @@ +using System.Linq; +using Content.Shared.Administration; +using Robust.Shared.Console; +using Robust.Shared.Enums; + +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 void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not { } player) + { + shell.WriteError("This command cannot be run from the server."); + return; + } + + if (player.Status != SessionStatus.InGame) + return; + + if (player.AttachedEntity is not {} playerEntity) + { + shell.WriteError("You don't have an entity!"); + return; + } + + var languages = IoCManager.Resolve().GetEntitySystem(); + + var (spokenLangs, knownLangs) = languages.GetAllLanguages(playerEntity); + + shell.WriteLine("Spoken: " + string.Join(", ", spokenLangs)); + shell.WriteLine("Understood: " + string.Join(", ", knownLangs)); + } +} diff --git a/Content.Server/_NF/Language/Commands/SayLanguageCommand.cs b/Content.Server/_NF/Language/Commands/SayLanguageCommand.cs new file mode 100644 index 00000000000..8acbe5c78c4 --- /dev/null +++ b/Content.Server/_NF/Language/Commands/SayLanguageCommand.cs @@ -0,0 +1,53 @@ +using Content.Server.Chat.Systems; +using Content.Shared.Administration; +using Robust.Shared.Console; +using Robust.Shared.Enums; + +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 void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not { } player) + { + shell.WriteError("This command cannot be run from the server."); + return; + } + + if (player.Status != SessionStatus.InGame) + return; + + if (player.AttachedEntity is not {} playerEntity) + { + shell.WriteError("You don't have an entity!"); + return; + } + + 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)) + return; + + var languages = IoCManager.Resolve().GetEntitySystem(); + var chats = IoCManager.Resolve().GetEntitySystem(); + + var language = languages.GetLanguage(languageId); + if (language == null || !languages.CanSpeak(playerEntity, language.ID)) + { + shell.WriteError($"Language {languageId} is invalid or you cannot speak it!"); + return; + } + + chats.TrySendInGameICMessage(playerEntity, message, InGameICChatType.Speak, ChatTransmitRange.Normal, false, shell, player, languageOverride: language); + } +} diff --git a/Content.Server/_NF/Language/Commands/SelectLanguageCommand.cs b/Content.Server/_NF/Language/Commands/SelectLanguageCommand.cs new file mode 100644 index 00000000000..e489267cd31 --- /dev/null +++ b/Content.Server/_NF/Language/Commands/SelectLanguageCommand.cs @@ -0,0 +1,48 @@ +using System.Linq; +using Content.Shared.Administration; +using Robust.Shared.Console; +using Robust.Shared.Enums; + +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 void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not { } player) + { + shell.WriteError("This command cannot be run from the server."); + return; + } + + if (player.Status != SessionStatus.InGame) + return; + + if (player.AttachedEntity is not { } playerEntity) + { + shell.WriteError("You don't have an entity!"); + return; + } + + if (args.Length < 1) + return; + + var languageId = args[0]; + + var languages = IoCManager.Resolve().GetEntitySystem(); + + var language = languages.GetLanguage(languageId); + if (language == null || !languages.CanSpeak(playerEntity, language.ID)) + { + shell.WriteError($"Language {languageId} is invalid or you cannot speak it!"); + return; + } + + languages.SetLanguage(playerEntity, language.ID); + } +} diff --git a/Content.Server/_NF/Language/LanguageSystem.Networking.cs b/Content.Server/_NF/Language/LanguageSystem.Networking.cs new file mode 100644 index 00000000000..f91a411b6b1 --- /dev/null +++ b/Content.Server/_NF/Language/LanguageSystem.Networking.cs @@ -0,0 +1,54 @@ +using Content.Server.Mind; +using Content.Shared.Language; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Robust.Shared.Player; + +namespace Content.Server.Language; + +public sealed partial class LanguageSystem +{ + [Dependency] private readonly MindSystem _mind = default!; + + public void InitializeNet() + { + // Refresh the client's state when its mind hops to a different entity + SubscribeLocalEvent((uid, _, _) => SendLanguageStateToClient(uid)); + SubscribeLocalEvent((_, _, args) => + { + if (args.Mind.Comp.Session != null) + SendLanguageStateToClient(args.Mind.Comp.Session); + }); + + SubscribeLocalEvent((uid, comp, _) => SendLanguageStateToClient(uid, comp)); + SubscribeNetworkEvent((_, session) => SendLanguageStateToClient(session.SenderSession)); + } + + private void SendLanguageStateToClient(EntityUid uid, LanguageSpeakerComponent? comp = null) + { + // Try to find a mind inside the entity and notify its session + if (!_mind.TryGetMind(uid, out var mind, out var mindComp) || mindComp.Session == null) + return; + + SendLanguageStateToClient(uid, mindComp.Session, comp); + } + + private void SendLanguageStateToClient(ICommonSession session, LanguageSpeakerComponent? comp = null) + { + // Try to find an entity associated with the session and resolve the languages from it + if (session.AttachedEntity is not { Valid: true } entity) + return; + + SendLanguageStateToClient(entity, session, comp); + } + + private void SendLanguageStateToClient(EntityUid uid, ICommonSession session, LanguageSpeakerComponent? component = null) + { + var langs = GetLanguages(uid, component); + if (langs == null) + return; + + var message = new LanguagesUpdatedMessage(langs.CurrentLanguage, langs.SpokenLanguages, langs.UnderstoodLanguages); + RaiseNetworkEvent(message, session); + } +} diff --git a/Content.Server/_NF/Language/LanguageSystem.cs b/Content.Server/_NF/Language/LanguageSystem.cs new file mode 100644 index 00000000000..efea6b513a9 --- /dev/null +++ b/Content.Server/_NF/Language/LanguageSystem.cs @@ -0,0 +1,306 @@ +using System.Linq; +using System.Text; +using Content.Shared.GameTicking; +using Content.Shared.Language; +using Content.Shared.Language.Systems; +using Robust.Shared.Random; +using Robust.Shared.Player; +using Robust.Server.GameObjects; +using UniversalLanguageSpeakerComponent = Content.Shared.Language.Components.UniversalLanguageSpeakerComponent; + +namespace Content.Server.Language; + +public sealed partial class LanguageSystem : SharedLanguageSystem +{ + /// + /// 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(); + + SubscribeLocalEvent(OnInitLanguageSpeaker); + SubscribeAllEvent(it => RandomRoundSeed = _random.Next()); + + InitializeNet(); + } + + private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) + { + if (string.IsNullOrEmpty(component.CurrentLanguage)) + { + component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); + } + } + + /// + /// Obfuscate speech of the given entity, or 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) + { + 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) + { + ObfuscateSyllables(builder, message, language); + } + else + { + 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) + { + if (language.ID == UniversalPrototype || HasComp(listener)) + return true; + + var listenerLanguages = GetLanguages(listener, listenerLanguageComp)?.UnderstoodLanguages; + + return listenerLanguages?.Contains(language.ID, StringComparer.Ordinal) ?? false; + } + + public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? speakerComp = null) + { + if (HasComp(speaker)) + return true; + + var langs = GetLanguages(speaker, speakerComp)?.UnderstoodLanguages; + return langs?.Contains(language, StringComparer.Ordinal) ?? false; + } + + // + // Returns the current language of the given entity. Assumes Universal if not specified. + // + public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? languageComp = null) + { + var id = GetLanguages(speaker, languageComp)?.CurrentLanguage; + if (id == null) + return Universal; // Fallback + + _prototype.TryIndex(id, out LanguagePrototype? proto); + + return proto ?? Universal; + } + + // + // Set the CurrentLanguage of the given entity. + // + public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? languageComp = null) + { + if (!CanSpeak(speaker, language) || HasComp(speaker)) + return; + + if (languageComp == null && !TryComp(speaker, out languageComp)) + return; + + if (languageComp.CurrentLanguage == language) + return; + + languageComp.CurrentLanguage = language; + + RaiseLocalEvent(speaker, new LanguagesUpdateEvent(), true); + } + + /// + /// Adds a new language to the lists of understood and/or spoken languages of the given component. + /// + public void AddLanguage(LanguageSpeakerComponent comp, string language, bool addSpoken = true, bool addUnderstood = true) + { + if (addSpoken && !comp.SpokenLanguages.Contains(language, StringComparer.Ordinal)) + comp.SpokenLanguages.Add(language); + + if (addUnderstood && !comp.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) + 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) + { + 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 + + /// + /// 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. + /// + /// 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. + /// + private DetermineEntityLanguagesEvent GetLanguages(EntityUid speaker, LanguageSpeakerComponent? comp = null) + { + // This is a shortcut for ghosts and entities that should not speak normally (admemes) + if (HasComp(speaker) || !TryComp(speaker, out comp)) + return _universalLanguagesEvent; + + var ev = _determineLanguagesEvent; + ev.SpokenLanguages.Clear(); + ev.UnderstoodLanguages.Clear(); + + ev.CurrentLanguage = comp.CurrentLanguage; + ev.SpokenLanguages.AddRange(comp.SpokenLanguages); + ev.UnderstoodLanguages.AddRange(comp.UnderstoodLanguages); + + RaiseLocalEvent(speaker, ev, true); + + 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; + } + + /// + /// 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; + } + + /// + /// 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; + } + } +} diff --git a/Content.Server/_NF/Language/TranslatorImplanterSystem.cs b/Content.Server/_NF/Language/TranslatorImplanterSystem.cs new file mode 100644 index 00000000000..0886dffdbcf --- /dev/null +++ b/Content.Server/_NF/Language/TranslatorImplanterSystem.cs @@ -0,0 +1,79 @@ +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.Systems; +using Content.Shared.Mobs.Components; + +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); + if (component.RequiredLanguages.Count > 0 && !component.RequiredLanguages.Any(lang => understood.Contains(lang))) + { + RefusesPopup(implanter, target); + 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; + SuccessPopup(implanter, target); + + _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 SharedLanguageSystem.LanguagesUpdateEvent(), true); + } + + private void RefusesPopup(EntityUid implanter, EntityUid target) + { + _popup.PopupEntity( + Loc.GetString("translator-implanter-refuse", ("implanter", implanter), ("target", target)), + implanter); + } + + private void SuccessPopup(EntityUid implanter, EntityUid target) + { + _popup.PopupEntity( + Loc.GetString("translator-implanter-success", ("implanter", implanter), ("target", target)), + implanter); + } +} diff --git a/Content.Server/_NF/Language/TranslatorSystem.cs b/Content.Server/_NF/Language/TranslatorSystem.cs new file mode 100644 index 00000000000..98588c0551d --- /dev/null +++ b/Content.Server/_NF/Language/TranslatorSystem.cs @@ -0,0 +1,243 @@ +using System.Linq; +using Content.Server.Popups; +using Content.Server.PowerCell; +using Content.Shared.Hands; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Shared.Language; +using Content.Shared.Language.Components; +using Content.Shared.Language.Systems; +using Content.Shared.PowerCell; +using static Content.Server.Language.LanguageSystem; +using HandheldTranslatorComponent = Content.Shared.Language.Components.HandheldTranslatorComponent; +using HoldsTranslatorComponent = Content.Shared.Language.Components.HoldsTranslatorComponent; +using IntrinsicTranslatorComponent = Content.Shared.Language.Components.IntrinsicTranslatorComponent; + +namespace Content.Server.Language; + +// this does not support holding multiple translators at once yet. +// that should not be an issue for now, but it better get fixed later. +public sealed class TranslatorSystem : SharedTranslatorSystem +{ + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly LanguageSystem _language = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + + private ISawmill _sawmill = default!; + + public override void Initialize() + { + base.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(OnTranslatorToggle); + SubscribeLocalEvent(OnPowerCellSlotEmpty); + + SubscribeLocalEvent( + (uid, component, args) => TranslatorEquipped(args.User, uid, component)); + SubscribeLocalEvent( + (uid, component, args) => TranslatorUnequipped(args.User, uid, component)); + } + + private void ApplyTranslation(EntityUid uid, IntrinsicTranslatorComponent component, + DetermineEntityLanguagesEvent ev) + { + if (!component.Enabled) + 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; + } + } + } + + if (addSpoken) + { + foreach (var language in component.SpokenLanguages) + { + AddIfNotExists(ev.SpokenLanguages, language); + } + + if (component.CurrentSpeechLanguage != null && ev.CurrentLanguage.Length == 0) + { + ev.CurrentLanguage = component.CurrentSpeechLanguage; + } + } + + if (addUnderstood) + { + foreach (var language in component.UnderstoodLanguages) + { + AddIfNotExists(ev.UnderstoodLanguages, language); + } + } + } + + private void TranslatorEquipped(EntityUid holder, EntityUid translator, HandheldTranslatorComponent component) + { + if (!EntityManager.HasComponent(holder)) + return; + + var intrinsic = EntityManager.EnsureComponent(holder); + UpdateBoundIntrinsicComp(component, intrinsic, component.Enabled); + + UpdatedLanguages(holder); + } + + private void TranslatorUnequipped(EntityUid holder, EntityUid translator, HandheldTranslatorComponent component) + { + if (!EntityManager.TryGetComponent(holder, out var intrinsic)) + return; + + if (intrinsic.Issuer == component) + { + + intrinsic.Enabled = false; + EntityManager.RemoveComponent(holder, intrinsic); + } + + _language.EnsureValidLanguage(holder); + + UpdatedLanguages(holder); + } + + private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponent component, ActivateInWorldEvent args) + { + if (!component.ToggleOnInteract) + return; + + var hasPower = _powerCell.HasDrawCharge(translator); + + 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 isEnabled = !component.Enabled; + if (intrinsic.Issuer != component) + { + // 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; + isEnabled = true; + } + + isEnabled &= hasPower; + UpdateBoundIntrinsicComp(component, intrinsic, isEnabled); + component.Enabled = isEnabled; + _powerCell.SetPowerCellDrawEnabled(translator, isEnabled); + + _language.EnsureValidLanguage(holder); + UpdatedLanguages(holder); + } + else + { + // This is a standalone translator (e.g. lying on the ground). Simply toggle its state. + component.Enabled = !component.Enabled && hasPower; + _powerCell.SetPowerCellDrawEnabled(translator, !component.Enabled && hasPower); + } + + OnAppearanceChange(translator, component); + + // 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 ? "translator-component-turnon" : "translator-component-shutoff", ("translator", component.Owner)); + _popup.PopupEntity(message, component.Owner, args.User); + } + } + + private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorComponent component, PowerCellSlotEmptyEvent args) + { + component.Enabled = false; + _powerCell.SetPowerCellDrawEnabled(translator, false); + OnAppearanceChange(translator, component); + + if (Transform(translator).ParentUid is { Valid: true } holder && EntityManager.HasComponent(holder)) + { + if (!EntityManager.TryGetComponent(holder, out var intrinsic)) + return; + + if (intrinsic.Issuer == component) + { + intrinsic.Enabled = false; + EntityManager.RemoveComponent(holder, intrinsic); + } + + _language.EnsureValidLanguage(holder); + UpdatedLanguages(holder); + } + } + + /// + /// Copies the state from the handheld [comp] to the [intrinsic] comp, using [isEnabled] as the enabled state. + /// + private void UpdateBoundIntrinsicComp(HandheldTranslatorComponent comp, HoldsTranslatorComponent intrinsic, bool isEnabled) + { + if (isEnabled) + { + intrinsic.SpokenLanguages = new List(comp.SpokenLanguages); + intrinsic.UnderstoodLanguages = new List(comp.UnderstoodLanguages); + intrinsic.CurrentSpeechLanguage = comp.CurrentSpeechLanguage; + } + else + { + intrinsic.SpokenLanguages.Clear(); + intrinsic.UnderstoodLanguages.Clear(); + intrinsic.CurrentSpeechLanguage = null; + } + + intrinsic.Enabled = isEnabled; + intrinsic.Issuer = comp; + } + + private static void AddIfNotExists(List list, string item) + { + if (list.Contains(item)) + return; + list.Add(item); + } + + private void UpdatedLanguages(EntityUid uid) + { + RaiseLocalEvent(uid, new SharedLanguageSystem.LanguagesUpdateEvent(), true); + } +} diff --git a/Content.Server/_NF/VulpLanguage/VulpLangaugeListenerComponent.cs b/Content.Server/_NF/VulpLanguage/VulpLangaugeListenerComponent.cs deleted file mode 100644 index 5f4ca7f3ca3..00000000000 --- a/Content.Server/_NF/VulpLanguage/VulpLangaugeListenerComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.VulpLangauge -{ - [RegisterComponent] - public partial class VulpLangaugeListenerComponent : Component - { - } -} diff --git a/Content.Server/_NF/VulpLanguage/VulpLanguageSpeakerComponent.cs b/Content.Server/_NF/VulpLanguage/VulpLanguageSpeakerComponent.cs deleted file mode 100644 index ac1b6d3aa36..00000000000 --- a/Content.Server/_NF/VulpLanguage/VulpLanguageSpeakerComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.VulpLangauge -{ - [RegisterComponent] - public partial class VulpLanguageSpeakerComponent : Component - { - } -} diff --git a/Content.Server/_NF/VulpLanguage/VulpTranslatorComponent.cs b/Content.Server/_NF/VulpLanguage/VulpTranslatorComponent.cs deleted file mode 100644 index 0ce8b145f84..00000000000 --- a/Content.Server/_NF/VulpLanguage/VulpTranslatorComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.VulpLangauge -{ - [RegisterComponent] - public partial class VulpTranslatorComponent : Component - { - } -} diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index ee4a4e9023b..f48d7ef423e 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -25,6 +25,7 @@ public static class ContentKeyFunctions public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward"; public static readonly BoundKeyFunction EscapeContext = "EscapeContext"; public static readonly BoundKeyFunction OpenCharacterMenu = "OpenCharacterMenu"; + public static readonly BoundKeyFunction OpenLanguageMenu = "OpenLanguageMenu"; // Frontier - Languages public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu"; public static readonly BoundKeyFunction OpenGuidebook = "OpenGuidebook"; public static readonly BoundKeyFunction OpenInventoryMenu = "OpenInventoryMenu"; diff --git a/Content.Shared/_NF/Language/Components/LanguageSpeakerComponent.cs b/Content.Shared/_NF/Language/Components/LanguageSpeakerComponent.cs new file mode 100644 index 00000000000..c6610c69d99 --- /dev/null +++ b/Content.Shared/_NF/Language/Components/LanguageSpeakerComponent.cs @@ -0,0 +1,29 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.Language; + +[RegisterComponent, AutoGenerateComponentState] +public sealed partial class LanguageSpeakerComponent : Component +{ + /// + /// The current language the entity may use to speak. + /// Other listeners will hear the entity speak in this language. + /// + [ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public string CurrentLanguage = default!; + + /// + /// List of languages this entity can speak. + /// + [ViewVariables] + [DataField("speaks", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] + public List SpokenLanguages = new(); + + /// + /// List of languages this entity can understand. + /// + [ViewVariables] + [DataField("understands", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] + public List UnderstoodLanguages = new(); +} diff --git a/Content.Shared/_NF/Language/Components/TranslatorComponent.cs b/Content.Shared/_NF/Language/Components/TranslatorComponent.cs new file mode 100644 index 00000000000..196363a8c9e --- /dev/null +++ b/Content.Shared/_NF/Language/Components/TranslatorComponent.cs @@ -0,0 +1,93 @@ +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; + +public abstract partial class BaseTranslatorComponent : Component +{ + /// + /// 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("default-language")] + [ViewVariables(VVAccess.ReadWrite)] + public string? CurrentSpeechLanguage = null; + + /// + /// 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(); + + /// + /// The languages the wielding MUST know in order for this translator to have effect. + /// 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(); + + /// + /// If true, the wielder must understand all languages in [RequiredLanguages] to speak [SpokenLanguages], + /// and understand all languages in [RequiredLanguages] to understand [UnderstoodLanguages]. + /// + /// Otherwise, at least one language must be known (or the list must be empty). + /// + [DataField("requires-all")] + [ViewVariables(VVAccess.ReadWrite)] + public bool RequiresAllLanguages = false; + + [DataField("enabled")] + public bool Enabled = true; +} + +/// +/// A translator that must be held in a hand or a pocket of an entity in order ot have effect. +/// +[RegisterComponent] +public sealed partial class HandheldTranslatorComponent : BaseTranslatorComponent +{ + /// + /// Whether or not interacting with this translator + /// toggles it on or off. + /// + [DataField("toggleOnInteract")] + public bool ToggleOnInteract = true; +} + +/// +/// A translator attached to an entity that translates its speech. +/// An example is a translator implant that allows the speaker to speak another language. +/// +[RegisterComponent, Virtual] +public partial class IntrinsicTranslatorComponent : BaseTranslatorComponent +{ +} + +/// +/// Applied internally to the holder of [HandheldTranslatorComponent]. +/// Do not use directly. Use [HandheldTranslatorComponent] instead. +/// +[RegisterComponent] +public sealed partial class HoldsTranslatorComponent : IntrinsicTranslatorComponent +{ + public Component? Issuer = null; +} + +/// +/// Applied to entities who were injected with a translator implant. +/// +[RegisterComponent] +public sealed partial class ImplantedTranslatorComponent : IntrinsicTranslatorComponent +{ +} diff --git a/Content.Shared/_NF/Language/Components/TranslatorImplanterComponent.cs b/Content.Shared/_NF/Language/Components/TranslatorImplanterComponent.cs new file mode 100644 index 00000000000..d198d1e5812 --- /dev/null +++ b/Content.Shared/_NF/Language/Components/TranslatorImplanterComponent.cs @@ -0,0 +1,34 @@ +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)), ViewVariables] + public List SpokenLanguages = new(); + + [DataField("understood", customTypeSerializer: typeof(PrototypeIdListSerializer)), ViewVariables] + 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)), ViewVariables] + public List RequiredLanguages = new(); + + /// + /// If true, only allows to use this implanter on mobs. + /// + [DataField("mobs-only")] + public bool MobsOnly = true; + + /// + /// Whether this implant has been used already. + /// + public bool Used = false; +} diff --git a/Content.Shared/_NF/Language/Components/UniversalLanguageSpeakerComponent.cs b/Content.Shared/_NF/Language/Components/UniversalLanguageSpeakerComponent.cs new file mode 100644 index 00000000000..68025546935 --- /dev/null +++ b/Content.Shared/_NF/Language/Components/UniversalLanguageSpeakerComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.Language.Components; + +// +// 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/_NF/Language/LanguagePrototype.cs b/Content.Shared/_NF/Language/LanguagePrototype.cs new file mode 100644 index 00000000000..25b9391ecec --- /dev/null +++ b/Content.Shared/_NF/Language/LanguagePrototype.cs @@ -0,0 +1,41 @@ +using System.Runtime.CompilerServices; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Language; + +[Prototype("language")] +public sealed class LanguagePrototype : IPrototype +{ + [IdDataField] + 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. + // + [DataField("obfuscateSyllables", required: true)] + public bool ObfuscateSyllables { get; private set; } = false; + + // + // 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(); + + #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"); + + #endregion utility +} diff --git a/Content.Shared/_NF/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/_NF/Language/Systems/SharedLanguageSystem.cs new file mode 100644 index 00000000000..3794f3bf14d --- /dev/null +++ b/Content.Shared/_NF/Language/Systems/SharedLanguageSystem.cs @@ -0,0 +1,57 @@ +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 +{ + [ValidatePrototypeId] + public static readonly string GalacticCommonPrototype = "GalacticCommon"; + [ValidatePrototypeId] + public static readonly string UniversalPrototype = "Universal"; + public static LanguagePrototype GalacticCommon { get; private set; } = default!; + 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) + { + _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/Content.Shared/_NF/Language/Systems/SharedTranslatorImplanterSystem.cs b/Content.Shared/_NF/Language/Systems/SharedTranslatorImplanterSystem.cs new file mode 100644 index 00000000000..a13225378cd --- /dev/null +++ b/Content.Shared/_NF/Language/Systems/SharedTranslatorImplanterSystem.cs @@ -0,0 +1,36 @@ +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/Content.Shared/_NF/Language/Systems/SharedTranslatorSystem.cs b/Content.Shared/_NF/Language/Systems/SharedTranslatorSystem.cs new file mode 100644 index 00000000000..b64276eba8c --- /dev/null +++ b/Content.Shared/_NF/Language/Systems/SharedTranslatorSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Examine; +using Content.Shared.Language.Components; +using Content.Shared.Toggleable; + +namespace Content.Shared.Language.Systems; + +public abstract class SharedTranslatorSystem : EntitySystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamined); + } + + private void OnExamined(EntityUid uid, HandheldTranslatorComponent component, ExaminedEvent args) + { + var state = Loc.GetString(component.Enabled + ? "translator-enabled" + : "translator-disabled"); + + args.PushMarkup(state); + } + + protected void OnAppearanceChange(EntityUid translator, HandheldTranslatorComponent? comp = null) + { + if (comp == null && !TryComp(translator, out comp)) + return; + + _appearance.SetData(translator, ToggleVisuals.Toggled, comp.Enabled); + } +} diff --git a/Resources/Locale/en-US/_NF/language/language-menu.ftl b/Resources/Locale/en-US/_NF/language/language-menu.ftl new file mode 100644 index 00000000000..83687d0f1a6 --- /dev/null +++ b/Resources/Locale/en-US/_NF/language/language-menu.ftl @@ -0,0 +1,4 @@ +language-menu-window-title = Language Menu +language-menu-current-language = Current Language: {$language} +language-menu-description-header = Description +ui-options-function-open-language-menu = Open language Menu diff --git a/Resources/Locale/en-US/_NF/language/languages.ftl b/Resources/Locale/en-US/_NF/language/languages.ftl new file mode 100644 index 00000000000..0cb0ab120ec --- /dev/null +++ b/Resources/Locale/en-US/_NF/language/languages.ftl @@ -0,0 +1,72 @@ +language-Universal-name = Universal +language-Universal-description = What are you? + +language-GalacticCommon-name = Galactic common +language-GalacticCommon-description = Commonly used for inter-species communications and official purposes. + +language-Bubblish-name = Bubblish +language-Bubblish-description = The language of slimes. It's a mixture of bubbling noises and pops. Very difficult to speak without mechanical aid for humans. + +language-RootSpeak-name = Rootspeak +language-RootSpeak-description = Strange whistling language spoken by the diona. + +language-CodeSpeak-name = Codespeak +language-CodeSpeak-description = Syndicate operatives can use a series of codewords to convey complex information, while sounding like random concepts and drinks to anyone listening in. + +language-Nekomimetic-name = Nekomimetic +language-Nekomimetic-description = To the casual observer, this langauge is an incomprehensible mess of broken Japanese. To the felinids, it's somehow comprehensible. + +language-Draconic-name = Draconic +language-Draconic-description = The common language of lizard-people, composed of sibilant hisses and rattles. + +language-Canilunzt-name = Canilunzt +language-Canilunzt-description = The guttural language spoken and utilized by the inhabitants of Vazzend system, composed of growls, barks, yaps, and heavy utilization of ears and tail movements, Vulpkanin speak this language with ease. + +language-SolCommon-name = Sol common +language-SolCommon-description = The language species from the Sol System use between each other. + +language-Cat-name = Animal Cat +language-Cat-description = Primitive sounds made by cats. Somehow they convey meanings! + +language-Dog-name = Animal Dog +language-Dog-description = Barking and growling sounds, used to convey primitive meanings. + +language-Mothroach-name = Animal Mothroach +language-Mothroach-description = Cute squeaking noises that sometimes make meaningful phrases. + +language-Xeno-name = Xeno +language-Xeno-description = A forgotten language spoken by the xenomorphs. + +language-RobotTalk-name = Binary encoded +language-RobotTalk-description = Not a language by itself, used by robots and machines to exchange data. + +language-Monkey-name = Primate +language-Monkey-description = A collection of sounds and gestures made by the primates for the purpose of communication. + +language-Bee-name = Animal bee +language-Bee-description = A strange language based on movements that bees use to communicate. + +language-Mouse-name = Animal mouse +language-Mouse-description = Cute squeaking sounds mice use to beg for food. + +# These ones are half-assed because these creatures are almost never played as. +language-Chicken-name = Animal chicken +language-Chicken-description = A collection of sounds made by chickens. + +language-Duck-name = Animal duck +language-Duck-description = A collection of sounds made by ducks. + +language-Cow-name = Animal cow +language-Cow-description = A collection of sounds made by cows. + +language-Sheep-name = Animal sheep +language-Sheep-description = A collection of sounds made by sheep. + +language-Kangaroo-name = Animal kangaroo +language-Kangaroo-description = A collection of sounds made by kangaroos. + +language-Pig-name = Animal pig +language-Pig-description = A collection of sounds made by pigs. + +language-Moffic-name = Moffic +language-Moffic-description = The language of the mothpeople borders on complete unintelligibility. diff --git a/Resources/Locale/en-US/_NF/language/translator.ftl b/Resources/Locale/en-US/_NF/language/translator.ftl new file mode 100644 index 00000000000..85e88023977 --- /dev/null +++ b/Resources/Locale/en-US/_NF/language/translator.ftl @@ -0,0 +1,9 @@ +translator-component-shutoff = The {$translator} shuts off. +translator-component-turnon = The {$translator} turns on. +translator-enabled = It appears to be active. +translator-disabled = It appears to be disabled. + +translator-implanter-refuse = The {$implanter} has no effect on {$target}. +translator-implanter-success = The {$implanter} successfully injected {$target}. +translator-implanter-ready = This implanter appears to be ready to use. +translator-implanter-used = This implanter seems empty. diff --git a/Resources/Locale/en-US/research/technologies.ftl b/Resources/Locale/en-US/research/technologies.ftl index 37f0bb58824..a7bd1a998f8 100644 --- a/Resources/Locale/en-US/research/technologies.ftl +++ b/Resources/Locale/en-US/research/technologies.ftl @@ -68,4 +68,6 @@ research-technology-honk-mech = H.O.N.K. Mech research-technology-advanced-spray = Advanced Spray research-technology-bluespace-cargo-transport = Bluespace Cargo Transport research-technology-quantum-fiber-weaving = Quantum Fiber Weaving +research-technology-basic-translation = Basic Translation +research-technology-advanced-translation = Advanced Translation research-technology-bluespace-chemistry = Bluespace Chemistry diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml index 1fc4407b02b..f1c7f700eb7 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml @@ -20,8 +20,6 @@ - type: Body prototype: Vulpkanin requiredLegs: 2 - - type: VulpLanguageSpeaker - - type: VulpLangaugeListener - type: VulpGiveTranslator - type: Speech speechSounds: Vulpkanin @@ -115,6 +113,12 @@ shortDensity: 140 shortPseudoItem: true shortCosmeticOnly: false + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Canilunzt + understands: + - Canilunzt - type: entity save: false diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/vulptranslator.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/vulptranslator.yml deleted file mode 100644 index cc6e3a49424..00000000000 --- a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/vulptranslator.yml +++ /dev/null @@ -1,14 +0,0 @@ -- type: entity - id: VulpTranslator - parent: [ BaseItem, PowerCellSlotMediumItem ] - name: Canilunzt translator - description: "Used by Vulpkanins to translate their speech." - components: - - type: Sprite - sprite: /Textures/Objects/Devices/vulp_translator.rsi - layers: - - state: icon - - type: PowerCellDraw - drawRate: 0 - useRate: 1 - - type: VulpTranslator diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index dc6e718290b..3b5bfe1cb8b 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -209,6 +209,14 @@ - type: GuideHelp guides: - Cyborgs + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk - type: entity id: BaseBorgChassisNT diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 4d60908ce35..794dc305233 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -48,8 +48,14 @@ flavorKind: station-event-random-sentience-flavor-organic - type: Bloodstream bloodMaxVolume: 50 - - type: ReplacementAccent - accent: mouse + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: mouse + - type: LanguageSpeaker + speaks: + - Mouse + understands: + - Mouse - type: MeleeWeapon soundHit: path: /Audio/Effects/bite.ogg @@ -121,6 +127,12 @@ rootTask: task: SimpleHostileCompound - type: ZombieImmune + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Bee + understands: + - Bee - type: entity name: bee @@ -220,8 +232,14 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: ReplacementAccent - accent: chicken + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: chicken + - type: LanguageSpeaker + speaks: + - Chicken + understands: + - Chicken - type: SentienceTarget flavorKind: station-event-random-sentience-flavor-organic - type: NpcFactionMember @@ -598,8 +616,14 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: ReplacementAccent - accent: duck + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: duck + - type: LanguageSpeaker + speaks: + - Duck + understands: + - Duck - type: SentienceTarget flavorKind: station-event-random-sentience-flavor-organic - type: NpcFactionMember @@ -786,6 +810,12 @@ - type: GuideHelp guides: - Chef + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Cow + understands: + - Cow - type: entity name: crab @@ -944,6 +974,12 @@ - type: HTN rootTask: task: RuminantHostileCompound + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Sheep + understands: + - Sheep # Note that we gotta make this bitch vomit someday when you feed it anthrax or sumthin. Needs to be a small item thief too and aggressive if attacked. - type: entity @@ -991,6 +1027,12 @@ - type: NpcFactionMember factions: - Passive + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Duck + understands: + - Duck - type: entity name: gorilla @@ -1044,6 +1086,12 @@ - type: HTN rootTask: task: SimpleHostileCompound + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Monkey + understands: + - Monkey - type: entity name: kangaroo @@ -1075,8 +1123,14 @@ - type: Inventory speciesId: kangaroo templateId: kangaroo - - type: ReplacementAccent - accent: kangaroo + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: kangaroo + - type: LanguageSpeaker + speaks: + - Kangaroo + understands: + - Kangaroo - type: InventorySlots - type: Strippable - type: Butcherable @@ -1227,6 +1281,13 @@ spawned: - id: FoodMeat amount: 3 + # Frontier - languages mechanic +# - type: MonkeyAccent + - type: LanguageSpeaker + speaks: + - Monkey + understands: + - Monkey - type: Puller needsHands: false - type: CanHostGuardian @@ -1564,8 +1625,14 @@ spawned: - id: FoodMeatRat amount: 1 - - type: ReplacementAccent - accent: mouse + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: mouse + - type: LanguageSpeaker + speaks: + - Mouse + understands: + - Mouse - type: Tag tags: - Trash @@ -2129,8 +2196,14 @@ - type: MeleeChemicalInjector transferAmount: 0.75 solution: melee - - type: ReplacementAccent - accent: xeno + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: xeno + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: InteractionPopup successChance: 0.5 interactSuccessString: petting-success-tarantula @@ -2444,6 +2517,12 @@ - type: Tag tags: - VimPilot + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Dog + understands: + - Dog - type: entity name: corgi @@ -2490,8 +2569,14 @@ spawned: - id: FoodMeat amount: 2 - - type: ReplacementAccent - accent: dog + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: dog + - type: LanguageSpeaker + speaks: + - Dog + understands: + - Dog - type: InteractionPopup interactSuccessString: petting-success-dog interactFailureString: petting-failure-generic @@ -2645,8 +2730,14 @@ spawned: - id: FoodMeat amount: 3 - - type: ReplacementAccent - accent: cat + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: cat + - type: LanguageSpeaker + speaks: + - Cat + understands: + - Cat - type: InteractionPopup successChance: 0.7 interactSuccessString: petting-success-cat @@ -2771,8 +2862,14 @@ spawned: - id: FoodMeat amount: 3 - - type: ReplacementAccent - accent: cat + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: cat + - type: LanguageSpeaker + speaks: + - Cat + understands: + - Cat - type: Grammar attributes: gender: epicene @@ -2950,6 +3047,12 @@ - type: Tag tags: - VimPilot + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Dog + understands: + - Dog - type: entity name: hamster @@ -3046,8 +3149,14 @@ spawned: - id: FoodMeat amount: 1 - - type: ReplacementAccent - accent: mouse + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: mouse + - type: LanguageSpeaker + speaks: + - Mouse + understands: + - Mouse - type: Tag tags: - VimPilot @@ -3153,8 +3262,14 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Animals/pig_oink.ogg - - type: ReplacementAccent - accent: pig + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: pig + - type: LanguageSpeaker + speaks: + - Pig + understands: + - Pig - type: SentienceTarget flavorKind: station-event-random-sentience-flavor-organic - type: NpcFactionMember diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml index 472daed59b7..e3a9551a249 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml @@ -15,8 +15,14 @@ - type: Sprite sprite: Mobs/Aliens/Argocyte/argocyte_common.rsi - type: SolutionContainerManager - - type: ReplacementAccent - accent: xeno + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: xeno + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: Bloodstream bloodReagent: FerrochromicAcid bloodMaxVolume: 75 #we don't want the map to become pools of blood diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml index bc63deeac36..fd27a6fde3d 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml @@ -5,6 +5,12 @@ abstract: true description: A floating demon aspect of the honkmother. components: + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + understands: + - GalacticCommon - type: GhostRole allowMovement: true makeSentient: true diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index 6f4d3512b6e..f483e2b7d58 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -70,8 +70,14 @@ tags: - Carp - DoorBumpOpener - - type: ReplacementAccent - accent: genericAggressive + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: genericAggressive + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: Speech speechVerb: LargeMob - type: InteractionPopup @@ -167,6 +173,14 @@ - type: HTN rootTask: task: DragonCarpCompound + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Xeno + understands: + - GalacticCommon + - Xeno - type: entity id: MobCarpDungeon diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml index 01fce382e37..26388127982 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml @@ -292,6 +292,12 @@ solution: bloodstream - type: DrainableSolution solution: bloodstream + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Bubblish + understands: + - Bubblish - type: entity name: Reagent Slime Spawner diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml b/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml index 06ab02dedc9..231c00eeb2b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml @@ -52,8 +52,14 @@ damage: types: Slash: 6 - - type: ReplacementAccent - accent: genericAggressive + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: genericAggressive + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: entity parent: BaseMobFlesh diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml b/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml index edbfb5bf12d..1a9709a0c80 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml @@ -69,6 +69,12 @@ interactFailureString: petting-failure-generic interactSuccessSound: path: /Audio/Animals/lizard_happy.ogg + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: entity id: MobWatcherLavaland diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml index 657ac466f84..635f31a171f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml @@ -41,3 +41,11 @@ - type: MovementSpeedModifier baseWalkSpeed : 1 baseSprintSpeed : 1 + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Xeno + understands: + - GalacticCommon + - Xeno diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml b/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml index 633a4ff3cac..74ac666769e 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml @@ -66,3 +66,9 @@ interactFailureString: petting-failure-generic interactSuccessSound: path: /Audio/Animals/lizard_happy.ogg + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index b496c5f5c7c..7548f27d9f2 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -296,8 +296,14 @@ spawned: - id: FoodMeat amount: 2 - - type: ReplacementAccent - accent: dog + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: dog + - type: LanguageSpeaker + speaks: + - Dog + understands: + - Dog - type: InteractionPopup successChance: 0.5 interactSuccessString: petting-success-dog @@ -395,8 +401,14 @@ spawned: - id: FoodMeat amount: 3 - - type: ReplacementAccent - accent: dog + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: dog + - type: LanguageSpeaker + speaks: + - Dog + understands: + - Dog - type: InteractionPopup successChance: 0.7 interactSuccessString: petting-success-dog @@ -668,6 +680,14 @@ - FootstepSound - type: StealTarget stealGroup: AnimalShiva + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno + - type: NoSlip + - type: Spider + - type: IgnoreSpiderWeb - type: entity name: Willow diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index a2875582127..cc7a239010c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -114,6 +114,14 @@ - type: Grammar attributes: gender: male + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Mouse + understands: + - GalacticCommon + - Mouse - type: entity id: MobRatKingBuff @@ -272,6 +280,13 @@ - type: GuideHelp guides: - MinorAntagonists + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Mouse + understands: + - GalacticCommon + - Mouse - type: weightedRandomEntity id: RatKingLoot diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml index 68ebf52dc06..531df7c1574 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml @@ -93,3 +93,11 @@ - RevenantTheme - type: Speech speechVerb: Ghost + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Xeno + understands: + - GalacticCommon + - Xeno diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index cd7882d3d1d..a9b1bce856f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -107,6 +107,14 @@ - type: TypingIndicator proto: robot - type: ZombieImmune + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk - type: entity parent: [ MobSiliconBase, BaseVehicle] diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index 93f79eb8d98..efca626dc2b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -111,8 +111,14 @@ successChance: 0.5 interactSuccessString: petting-success-slimes interactFailureString: petting-failure-generic - - type: ReplacementAccent - accent: slimes + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: slimes + - type: LanguageSpeaker + speaks: + - Bubblish + understands: + - Bubblish #Frontier Disabled # - type: GhostTakeoverAvailable # - type: GhostRole diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index 618ee16c895..6ef1c3c5ff1 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -165,8 +165,14 @@ - type: FootstepModifier footstepSoundCollection: collection: FootstepBounce - - type: ReplacementAccent - accent: kangaroo + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: kangaroo + - type: LanguageSpeaker + speaks: + - Kangaroo + understands: + - Kangaroo - type: InventorySlots - type: Strippable - type: UserInterface @@ -248,8 +254,14 @@ - type: MeleeChemicalInjector solution: melee transferAmount: 3 - - type: ReplacementAccent - accent: xeno + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: xeno + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: InteractionPopup successChance: 0.20 interactSuccessString: petting-success-tarantula @@ -351,8 +363,14 @@ - type: MeleeChemicalInjector solution: melee transferAmount: 6 - - type: ReplacementAccent - accent: xeno +# Frontier - languages mechanic +# - type: ReplacementAccent +# accent: xeno + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: InteractionPopup successChance: 0.2 interactSuccessString: petting-success-snake diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml index 0a2b4f80bbc..26295f52f75 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml @@ -74,8 +74,14 @@ Quantity: 5 - type: MeleeChemicalInjector solution: melee - - type: ReplacementAccent - accent: genericAggressive + # Frontier - languages mechanic + # - type: ReplacementAccent + # accent: genericAggressive + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: Speech speechVerb: SmallMob - type: NonSpreaderZombie diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 7a263ced001..a1e9472b93d 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -123,6 +123,12 @@ molsPerSecondPerUnitMass: 0.0005 - type: Speech speechVerb: LargeMob + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: entity name: Praetorian @@ -240,6 +246,14 @@ - type: Tag tags: - CannotSuicide + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Xeno + understands: + - GalacticCommon + - Xeno - type: entity name: Ravager diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml index 95516635cef..9d91fcc111d 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml @@ -139,6 +139,14 @@ - type: GuideHelp guides: - MinorAntagonists + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Xeno + understands: + - GalacticCommon + - Xeno - type: entity parent: BaseMobDragon diff --git a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml index 63e27b8c530..3fccf0140bf 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml @@ -30,6 +30,13 @@ - PetsNT - type: Alerts - type: Familiar + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Mouse + understands: + - GalacticCommon + - Mouse - type: entity name: Cerberus @@ -88,3 +95,10 @@ Male: Cerberus Female: Cerberus Unsexed: Cerberus + - type: LanguageSpeaker + speaks: + - Dog + understands: + - GalacticCommon + - CodeSpeak + - Dog diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml index b05b73caa42..c2fe76090b3 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml @@ -135,6 +135,14 @@ # - type: HTN # Frontier # rootTask: # task: SimpleHumanoidHostileCompound + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - CodeSpeak + understands: + - GalacticCommon + - CodeSpeak # From Wizard deck of cards - type: entity @@ -163,6 +171,12 @@ map: [ "enum.DamageStateVisualLayers.BaseUnshaded" ] color: "#40a7d7" shader: unshaded + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + understands: + - GalacticCommon - type: entity name: HoloClown @@ -244,6 +258,12 @@ # - type: HTN # Frontier # rootTask: # task: SimpleHumanoidHostileCompound + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + understands: + - GalacticCommon - type: entity id: ActionToggleGuardian diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index 114c3fa7479..b62de96a35f 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -48,6 +48,8 @@ - type: Tag tags: - BypassInteractionRangeChecks + # Frontier - languages mechanic + - type: UniversalLanguageSpeaker # Ghosts should understand any language. - type: entity id: ActionGhostBoo diff --git a/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml b/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml index ad9b37f63e1..fbdc50d33d2 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml @@ -7,3 +7,5 @@ - type: MovementSpeedModifier baseSprintSpeed: 24 baseWalkSpeed: 16 + # Frontier - languages mechanic + - type: UniversalLanguageSpeaker diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 0f8998bdec8..7335f8b5217 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -16,6 +16,14 @@ startingItem: PowerCellMedium - type: RandomMetadata nameSegments: [names_borg] + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk - type: entity id: PlayerBorgBattery @@ -51,3 +59,11 @@ cell_slot: name: power-cell-slot-component-slot-name-default startingItem: PowerCellHyper + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index a13f854033c..e5b947246ef 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -216,7 +216,7 @@ - type: MobPrice 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: CanEscapeInventory # Carrying system from nyanotrasen. - type: Tag tags: - CanPilot @@ -302,6 +302,12 @@ Asphyxiation: -1.0 - type: FireVisuals alternateState: Standing + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + understands: + - GalacticCommon - type: entity save: false diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index 837280b065e..7a419d81a63 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -96,6 +96,13 @@ soundsId: DionaBodyEmotes - type: IgnoreKudzu - type: ShoesRequiredStepTriggerImmune # Frontier + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RootSpeak + understands: + - GalacticCommon + - RootSpeak - type: IgniteOnHeatDamage fireStacks: 1 threshold: 12 diff --git a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml index fe36754b9b5..ad75435aa53 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml @@ -52,6 +52,14 @@ accent: dwarf - type: Speech speechSounds: Bass + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - SolCommon + understands: + - GalacticCommon + - SolCommon - type: entity parent: BaseSpeciesDummy diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 2bf6e788ef6..3f97458958e 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -16,6 +16,14 @@ - id: FoodMeatHuman amount: 5 - type: Carriable # Carrying system from nyanotrasen. + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - SolCommon + understands: + - GalacticCommon + - SolCommon - type: entity parent: BaseSpeciesDummy diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml index 292d547e516..fc60b5a9783 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml @@ -10,6 +10,13 @@ - type: Hunger - type: Thirst - type: Carriable # Carrying system from nyanotrasen. + - type: LanguageSpeaker # Frontier + speaks: + - GalacticCommon + - Moffic + understands: + - GalacticCommon + - Moffic - type: Icon sprite: Mobs/Species/Moth/parts.rsi state: full diff --git a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml index e70430b4e9e..8612f6a4088 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml @@ -62,6 +62,14 @@ - type: SizeAttributeWhitelist # Frontier tall: true tallscale: 1.2 + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Draconic + understands: + - GalacticCommon + - Draconic - type: entity parent: BaseSpeciesDummy diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index 481afd06a3c..0aecf93a589 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -74,6 +74,14 @@ types: Asphyxiation: -1.0 maxSaturation: 15 + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Bubblish + understands: + - GalacticCommon + - Bubblish - type: entity parent: MobHumanDummy diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 57432e76112..63974bbf622 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -336,6 +336,25 @@ - DeviceQuantumSpinInverter - UtilityBeltChiefEngineer - JetpackVoid + # Frontier - languages mechanic + - CanilunztTranslator + - BubblishTranslator + - NekomimeticTranslator + - DraconicTranslator + - SolCommonTranslator + - RootSpeakTranslator + - XenoTranslator + - BasicGalaticCommonTranslatorImplanter + - AdvancedGalaticCommonTranslatorImplanter + - BubblishTranslatorImplanter + - NekomimeticTranslatorImplanter + - DraconicTranslatorImplanter + - CanilunztTranslatorImplanter + - SolCommonTranslatorImplanter + - RootSpeakTranslatorImplanter + - AnimalTranslator + - MofficTranslatorImplanter + - MofficTranslator - type: EmagLatheRecipes emagDynamicRecipes: - ExplosivePayload diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 8fbc32f4dbc..e9c51721afb 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -103,6 +103,14 @@ - type: WiresVisuals - type: MarketModifier mod: 10 + # Frontier - languages mechanic (for ghost takeover, mainly) + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk - type: entity parent: VendingMachine diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/NPCs/dogs.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/NPCs/dogs.yml index 4305cfc93ad..d0ca43b6fbc 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/NPCs/dogs.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/NPCs/dogs.yml @@ -46,8 +46,14 @@ spawned: - id: FoodMeat amount: 3 - - type: ReplacementAccent - accent: dog + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: dog + - type: LanguageSpeaker + speaks: + - Dog + understands: + - Dog - type: InteractionPopup successChance: 0.7 interactSuccessString: petting-success-dog diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml index d95014d1675..798fc03df0c 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml @@ -54,6 +54,16 @@ - type: SizeAttributeWhitelist # Frontier tall: true tallscale: 1 + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - SolCommon + - Nekomimetic + understands: + - GalacticCommon + - SolCommon + - Nekomimetic - type: entity save: false diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/oni.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/oni.yml index c84c737ca65..c787df21da1 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/oni.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/oni.yml @@ -37,6 +37,14 @@ - type: NpcFactionMember factions: - NanoTrasen + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Nekomimetic + understands: + - GalacticCommon + - Nekomimetic - type: entity save: false diff --git a/Resources/Prototypes/Recipes/Lathes/devices.yml b/Resources/Prototypes/Recipes/Lathes/devices.yml index 98d9120d6e0..3f2bed85b24 100644 --- a/Resources/Prototypes/Recipes/Lathes/devices.yml +++ b/Resources/Prototypes/Recipes/Lathes/devices.yml @@ -234,3 +234,4 @@ Silver: 500 Plasma: 500 Uranium: 250 + diff --git a/Resources/Prototypes/Research/civilianservices.yml b/Resources/Prototypes/Research/civilianservices.yml index f2306a63ed4..95faf102171 100644 --- a/Resources/Prototypes/Research/civilianservices.yml +++ b/Resources/Prototypes/Research/civilianservices.yml @@ -203,6 +203,7 @@ - WeaponSprayNozzle - ClothingBackpackWaterTank + # Tier 3 # - type: technology # Frontier diff --git a/Resources/Prototypes/_NF/Catalog/VendingMachines/Inventories/contravend.yml b/Resources/Prototypes/_NF/Catalog/VendingMachines/Inventories/contravend.yml index c9c74e6c0a8..0c92fe0f363 100644 --- a/Resources/Prototypes/_NF/Catalog/VendingMachines/Inventories/contravend.yml +++ b/Resources/Prototypes/_NF/Catalog/VendingMachines/Inventories/contravend.yml @@ -20,4 +20,5 @@ ClothingShoesChameleonNoSlips: 8 ClothingBackpackDuffelSyndicateEVABundle: 12 BoxHoloparasite: 2 + CodeSpeakImplanter: 5 VestineChemistryVial: 2 diff --git a/Resources/Prototypes/_NF/Entities/Mobs/NPCs/emotionalsupportanimals.yml b/Resources/Prototypes/_NF/Entities/Mobs/NPCs/emotionalsupportanimals.yml index c864a1433f0..c0f0c3b29d4 100644 --- a/Resources/Prototypes/_NF/Entities/Mobs/NPCs/emotionalsupportanimals.yml +++ b/Resources/Prototypes/_NF/Entities/Mobs/NPCs/emotionalsupportanimals.yml @@ -30,13 +30,26 @@ - VimPilot - type: Puller needsHands: false + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Cat + understands: + - Cat - type: entity id: BaseEmotionalGhostCat abstract: true components: - - type: ReplacementAccent - accent: cat +# Frontier - languages mechanic (emotional pets understand GC) +# - type: ReplacementAccent +# accent: cat + - type: LanguageSpeaker + speaks: + - Cat + understands: + - Cat + - GalacticCommon - type: Vocal sounds: Male: Cat @@ -49,8 +62,15 @@ id: BaseEmotionalGhostDog abstract: true components: - - type: ReplacementAccent - accent: dog + # Frontier - languages mechanic (emotional pets understand GC) + # - type: ReplacementAccent + # accent: dog + - type: LanguageSpeaker + speaks: + - Dog + understands: + - Dog + - GalacticCommon - type: Vocal sounds: Male: Dog @@ -132,8 +152,15 @@ parent: [BaseEmotionalGhost, MobChicken] id: MobChickenGhost components: - - type: ReplacementAccent - accent: chicken +# Frontier - languages mechanic +# - type: ReplacementAccent +# accent: chicken + - type: LanguageSpeaker + speaks: + - Chicken + understands: + - GalacticCommon + - Chicken - type: Vocal sounds: Male: Chicken diff --git a/Resources/Prototypes/_NF/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/_NF/Entities/Mobs/NPCs/pets.yml index ce0373fd261..574bd4e73e1 100644 --- a/Resources/Prototypes/_NF/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/_NF/Entities/Mobs/NPCs/pets.yml @@ -11,6 +11,15 @@ - !type:WhitelistRequirement - type: Loadout prototypes: [ MobClippyGear ] + # Frontier - languages mechanic + # - type: ReplacementAccent + # accent: archaic + - type: LanguageSpeaker + speaks: + - Cat + understands: + - GalacticCommon + - Cat - type: RandomMetadata nameSegments: [names_cat_clippy] # Its needed to fix the names since it was using the MobCatGhost list. - type: AutoImplant @@ -44,8 +53,12 @@ - !type:WhitelistRequirement - type: Loadout prototypes: [ MobClarpyGear ] - - type: ReplacementAccent - accent: pirateCat + - type: LanguageSpeaker + speaks: + - Cat + understands: + - GalacticCommon + - Cat - type: RandomMetadata nameSegments: [names_cat_clarpy] # Its needed to fix the names since it was using the MobCatGhost list. - type: AutoImplant @@ -75,8 +88,15 @@ - type: GhostRole name: ghost-role-information-mistake-name description: ghost-role-information-mistake-description - - type: ReplacementAccent - accent: mistakeCat +# Frontier - languages mechanic +# - type: ReplacementAccent +# accent: mistakeCat + - type: LanguageSpeaker + speaks: + - Cat + understands: + - GalacticCommon + - Cat - type: RandomMetadata nameSegments: [names_cat_mistake] # Its needed to fix the names since it was using the MobCatGhost list. - type: Vocal diff --git a/Resources/Prototypes/_NF/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/_NF/Entities/Mobs/NPCs/xeno.yml index 8d9ecad383c..6cc1cb6fca3 100644 --- a/Resources/Prototypes/_NF/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/_NF/Entities/Mobs/NPCs/xeno.yml @@ -8,8 +8,14 @@ tags: - CannotSuicide - type: SalvageMobRestrictions - - type: ReplacementAccent - accent: xeno + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: xeno + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: GhostRole allowMovement: true allowSpeech: true diff --git a/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translator_implants.yml b/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translator_implants.yml new file mode 100644 index 00000000000..abdaf86ce39 --- /dev/null +++ b/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translator_implants.yml @@ -0,0 +1,145 @@ +- type: entity + id: BaseTranslatorImplanter + parent: [ BaseItem ] + name: Basic translator implant + description: "Translates speech." + 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 Galatic Common translator implant + description: "An implant giving the ability to understand Galatic Common." + components: + - type: TranslatorImplanter + understood: + - GalacticCommon + +- type: entity + id: AdvancedGalaticCommonTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Advanced Galatic Common translator implant + description: "An implant giving the ability to understand and speak Galatic Common." + components: + - type: TranslatorImplanter + spoken: + - GalacticCommon + understood: + - GalacticCommon + +- type: entity + id: BubblishTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Bubblish translator implant + description: "An implant giving the ability to understand and speak Bubblish." + components: + - type: TranslatorImplanter + spoken: + - Bubblish + understood: + - Bubblish + +- type: entity + id: NekomimeticTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Nekomimetic translator implant + description: "An implant giving the ability to understand and speak Nekomimetic, Nya~!" + components: + - type: TranslatorImplanter + spoken: + - Nekomimetic + understood: + - Nekomimetic + +- type: entity + id: DraconicTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Draconic translator implant + description: "An implant giving the ability to understand and speak Draconic." + components: + - type: TranslatorImplanter + spoken: + - Draconic + understood: + - Draconic + +- type: entity + id: CanilunztTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Canilunzt translator implant + description: "An implant giving the ability to understand and speak Canilunzt, Yeeps!" + components: + - type: TranslatorImplanter + spoken: + - Canilunzt + understood: + - Canilunzt + +- type: entity + id: SolCommonTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: SolCommon translator implant + description: "An implant giving the ability to understand and speak SolCommon, raaagh!" + components: + - type: TranslatorImplanter + spoken: + - SolCommon + understood: + - SolCommon + +- type: entity + id: RootSpeakTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: RootSpeak translator implant + description: "An implant giving the ability to understand and speak RootSpeak." + components: + - type: TranslatorImplanter + spoken: + - RootSpeak + understood: + - RootSpeak + +- type: entity + id: MofficTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Moffic translator implant + description: "An implant giving the ability to understand and speak Moffic." + components: + - type: TranslatorImplanter + spoken: + - Moffic + understood: + - Moffic + +- type: entity + id: CodeSpeakImplanter + parent: [ BaseTranslatorImplanter ] + name: CodeSpeak Implanter + description: "\"CodeSpeak(tm) - Secure your communication with metaphors so elaborate, they seem randomly generated!\"" + components: + - type: TranslatorImplanter + spoken: + - CodeSpeak + understood: + - CodeSpeak + - type: StaticPrice + price: 150 diff --git a/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translators.yml b/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translators.yml new file mode 100644 index 00000000000..f875b40102b --- /dev/null +++ b/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translators.yml @@ -0,0 +1,236 @@ +- type: entity + id: TranslatorUnpowered + parent: [ BaseItem ] + name: Translator + description: "Translates speech." + components: + - type: Sprite + sprite: _NF/Objects/Devices/translator.rsi + state: icon + layers: + - state: icon + - state: translator + shader: unshaded + visible: false + map: [ "enum.ToggleVisuals.Layer", "enum.PowerDeviceVisualLayers.Powered" ] + - type: Appearance + - type: GenericVisualizer + visuals: + enum.ToggleVisuals.Toggled: + enum.ToggleVisuals.Layer: + True: { visible: true } + False: { visible: false } + - type: HandheldTranslator + enabled: false + +- type: entity + id: Translator + parent: [ TranslatorUnpowered, PowerCellSlotMediumItem ] + suffix: Powered + components: + - type: PowerCellDraw + drawRate: 1 + +- type: entity + id: TranslatorEmtpy + parent: [ Translator ] + suffix: Empty + components: + - type: ItemSlots + slots: + cell_slot: + name: power-cell-slot-component-slot-name-default + +- type: entity + id: VulpTranslator + parent: [ Translator ] + name: Vulpkanin translator + description: "Used only by Vulpkanin to understand and speak with Galatic Common speakers." + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + understood: + - GalacticCommon + requires: + - Canilunzt + requires-all: false + - type: PowerCellDraw + drawRate: 0.1 + - type: StaticPrice + price: 35 + +- type: entity + id: CanilunztTranslator + parent: [ TranslatorEmtpy ] + name: Canilunzt translator + description: "Translates speech between Canilunzt and Galactic Common. Commonly used by Vulpkanin to communicate with galactic common speakers" + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Canilunzt + understood: + - GalacticCommon + - Canilunzt + requires: + - GalacticCommon + - Canilunzt + requires-all: false + +- type: entity + id: BubblishTranslator + parent: [ TranslatorEmtpy ] + name: Bubblish translator + description: "Translates speech between Bubblish and Galactic Common." + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Bubblish + understood: + - GalacticCommon + - Bubblish + requires: + - GalacticCommon + - Bubblish + requires-all: false + +- type: entity + id: NekomimeticTranslator + parent: [ TranslatorEmtpy ] + name: Nekomimetic translator + description: "Translates speech between Nekomimetic and Galactic Common. Why would you want that?" + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Nekomimetic + understood: + - GalacticCommon + - Nekomimetic + requires: + - GalacticCommon + - Nekomimetic + requires-all: false + +- type: entity + id: DraconicTranslator + parent: [ TranslatorEmtpy ] + name: Draconic translator + description: "Translates speech between Draconic and Galactic Common." + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Draconic + understood: + - GalacticCommon + - Draconic + requires: + - GalacticCommon + - Draconic + requires-all: false + +- type: entity + id: SolCommonTranslator + parent: [ TranslatorEmtpy ] + name: Sol Common translator + description: "Translates speech between Sol Common and Galactic Common. Like a true Earthman!" + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - SolCommon + understood: + - GalacticCommon + - SolCommon + requires: + - GalacticCommon + - SolCommon + requires-all: false + +- type: entity + id: RootSpeakTranslator + parent: [ TranslatorEmtpy ] + name: RootSpeak translator + description: "Translates speech between RootSpeak and Galactic Common. Like a true plant?" + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - RootSpeak + understood: + - GalacticCommon + - RootSpeak + requires: + - GalacticCommon + - RootSpeak + requires-all: false + +- type: entity + id: MofficTranslator + parent: [ TranslatorEmtpy ] + name: Moffic translator + description: "Translates speech between Moffic and Galactic Common. Like a true moth... or bug?" + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Moffic + understood: + - GalacticCommon + - Moffic + requires: + - GalacticCommon + - Moffic + requires-all: false + +- type: entity + id: XenoTranslator + parent: [ TranslatorEmtpy ] + name: Xeno translator + description: "Translates speech between Xeno and Galactic Common. Not sure if that will help." + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Xeno + understood: + - GalacticCommon + - Xeno + requires: + - GalacticCommon + +- type: entity + id: AnimalTranslator + parent: [ TranslatorEmtpy ] + name: Animal translator + description: "Translates all the cutes nosies that animals make into a more understandable form!" + components: + - type: HandheldTranslator + understood: + - Cat + - Dog + - Mothroach + - Monkey + - Bee + - Mouse + - Chicken + - Duck + - Cow + - Sheep + - Kangaroo + - Pig + requires: + - GalacticCommon + requires-all: false diff --git a/Resources/Prototypes/_NF/Language/languages.yml b/Resources/Prototypes/_NF/Language/languages.yml new file mode 100644 index 00000000000..9c352a1f564 --- /dev/null +++ b/Resources/Prototypes/_NF/Language/languages.yml @@ -0,0 +1,525 @@ +# The universal language, assumed if the entity has a UniversalLanguageSpeakerComponent. +# Do not use otherwise. Try to use the respective component instead of this language. +- type: language + id: Universal + obfuscateSyllables: false + replacement: + - "*incomprehensible*" + +# 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 + +# Spoken by slimes. +- type: language + id: Bubblish + obfuscateSyllables: true + 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 + + # Spoken by dionas. +- type: language + id: RootSpeak + obfuscateSyllables: true + replacement: + - hs + - zt + - kr + - st + - sh + +# Syndicate operatives can use a series of codewords to convey complex information. +- type: language + id: CodeSpeak + obfuscateSyllables: true + replacement: + - WhiteRussian + - Station Representative + - Sherrif + - Deputy + - Vault + - Frontier + - Station + - Security + - Coffee + - Cola + - Water + - Engineering + - Captain + - Calling a friend + - Atmos + - Robotic + - Medical + - AI + - Human + - Vulpkanin + - Lizard + - Moth + - Tea + - Chair + - Sofa + - Ship + - Shuttle + - Weapon + - Weapons + - Laser + - Carp + - Space Carp + - Xeno + - Xenomorph + - Biohazard + - Money + - Space + - Danger + - Monkey + - Pun pun + - In + - Running + - Killing + - Kill + - Save + - Life + - Dragon + - Ninja + - Secret + +# 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 + +# 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 + +# 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 + +# 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 + +# Languages spoken by various critters. +- type: language + id: Cat + obfuscateSyllables: true + replacement: + - murr + - meow + - purr + - mrow + +- type: language + id: Dog + obfuscateSyllables: true + replacement: + - woof + - bark + - ruff + - bork + - raff + - garr + +- type: language + id: Mothroach + obfuscateSyllables: false + replacement: + - Chitter + - Buzz + - Chirp + - Squeak + - Peep + - Eeee + - Eep + +- type: language + id: Xeno + obfuscateSyllables: true + replacement: + - sss + - sSs + - SSS + +- type: language + id: RobotTalk + obfuscateSyllables: true + replacement: + - beep + - boop + +- type: language + id: Monkey + obfuscateSyllables: true + replacement: + - ok + - ook + - oook + - ooook + - oooook + +- type: language + id: Bee + obfuscateSyllables: false + replacement: + - Buz + - Buuz + - Buzz + - Buzzz + - Buuzz + +- type: language + id: Mouse + obfuscateSyllables: false + replacement: + - Squeak + - Piep + - Chuu + - Eeee + - Pip + - Fwiep + - Heep + +- type: language + id: Chicken + obfuscateSyllables: false + replacement: + - Coo + - Coot + - Cooot + +- type: language + id: Duck + obfuscateSyllables: false + replacement: + - Quack + +- type: language + id: Cow + obfuscateSyllables: false + replacement: + - Moo + - Mooo + +- type: language + id: Sheep + obfuscateSyllables: false + replacement: + - Ba + - Baa + - Baaa + +- type: language + id: Kangaroo + obfuscateSyllables: false + replacement: + - Shreak + - Chuu + +- type: language + id: Pig + obfuscateSyllables: false + replacement: + - Oink diff --git a/Resources/Prototypes/_NF/Recipes/Lathes/devices.yml b/Resources/Prototypes/_NF/Recipes/Lathes/devices.yml index 68f30ba047f..ccb334557ea 100644 --- a/Resources/Prototypes/_NF/Recipes/Lathes/devices.yml +++ b/Resources/Prototypes/_NF/Recipes/Lathes/devices.yml @@ -8,6 +8,196 @@ Plasma: 1500 Uranium: 150 +# Translators +- type: latheRecipe + id: CanilunztTranslator + result: CanilunztTranslator + completetime: 2 + materials: + Steel: 500 + Glass: 100 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: BubblishTranslator + result: BubblishTranslator + completetime: 2 + materials: + Steel: 500 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: NekomimeticTranslator + result: NekomimeticTranslator + completetime: 2 + materials: + Steel: 500 + Glass: 100 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: DraconicTranslator + result: DraconicTranslator + completetime: 2 + materials: + Steel: 500 + Glass: 100 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: SolCommonTranslator + result: SolCommonTranslator + completetime: 2 + materials: + Steel: 500 + Glass: 100 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: RootSpeakTranslator + result: RootSpeakTranslator + completetime: 2 + materials: + Steel: 500 + Glass: 100 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: MofficTranslator + result: MofficTranslator + completetime: 2 + materials: + Steel: 500 + Glass: 100 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: BasicGalaticCommonTranslatorImplanter + result: BasicGalaticCommonTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: XenoTranslator + result: XenoTranslator + completetime: 2 + materials: + Steel: 200 + Plastic: 50 + Gold: 50 + Plasma: 50 + Silver: 50 + +- type: latheRecipe + id: AdvancedGalaticCommonTranslatorImplanter + result: AdvancedGalaticCommonTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: BubblishTranslatorImplanter + result: BubblishTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: NekomimeticTranslatorImplanter + result: NekomimeticTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: DraconicTranslatorImplanter + result: DraconicTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: CanilunztTranslatorImplanter + result: CanilunztTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: SolCommonTranslatorImplanter + result: SolCommonTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: RootSpeakTranslatorImplanter + result: RootSpeakTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: MofficTranslatorImplanter + result: MofficTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: AnimalTranslator + result: AnimalTranslator + completetime: 2 + materials: + Steel: 200 + Plastic: 50 + Gold: 50 + Plasma: 50 + Silver: 5 - type: latheRecipe id: WeaponCrusher result: WeaponCrusher diff --git a/Resources/Prototypes/_NF/Research/civillianservices.yml b/Resources/Prototypes/_NF/Research/civillianservices.yml new file mode 100644 index 00000000000..8fc63143ea1 --- /dev/null +++ b/Resources/Prototypes/_NF/Research/civillianservices.yml @@ -0,0 +1,42 @@ +# Tier 2 - entrypoint to translators +- type: technology + id: BasicTranslation + name: research-technology-basic-translation + icon: + sprite: _NF/Objects/Devices/translator.rsi + state: icon + discipline: CivilianServices + tier: 2 + cost: 10000 + recipeUnlocks: + - CanilunztTranslator + - BubblishTranslator + - NekomimeticTranslator + - DraconicTranslator + - SolCommonTranslator + - RootSpeakTranslator + - BasicGalaticCommonTranslatorImplanter + - MofficTranslator + + +# Frontier - languages mechanic +- type: technology + id: AdvancedTranslation + name: research-technology-advanced-translation + icon: + sprite: _NF/Objects/Devices/translator.rsi + state: icon + discipline: CivilianServices + tier: 3 + cost: 15000 + recipeUnlocks: + - XenoTranslator + - AdvancedGalaticCommonTranslatorImplanter + - BubblishTranslatorImplanter + - NekomimeticTranslatorImplanter + - DraconicTranslatorImplanter + - CanilunztTranslatorImplanter + - SolCommonTranslatorImplanter + - RootSpeakTranslatorImplanter + - AnimalTranslator + - MofficTranslatorImplanter diff --git a/Resources/Prototypes/_Nyano/Entities/Mobs/NPCs/mothroach.yml b/Resources/Prototypes/_Nyano/Entities/Mobs/NPCs/mothroach.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Resources/Textures/Objects/Devices/vulp_translator.rsi/icon.png b/Resources/Textures/Objects/Devices/vulp_translator.rsi/icon.png deleted file mode 100644 index e60127b2df8..00000000000 Binary files a/Resources/Textures/Objects/Devices/vulp_translator.rsi/icon.png and /dev/null differ diff --git a/Resources/Textures/_NF/Interface/Actions/language.png b/Resources/Textures/_NF/Interface/Actions/language.png new file mode 100644 index 00000000000..2b39424d12d Binary files /dev/null and b/Resources/Textures/_NF/Interface/Actions/language.png differ diff --git a/Resources/Textures/_NF/Objects/Devices/translator.rsi/icon.png b/Resources/Textures/_NF/Objects/Devices/translator.rsi/icon.png new file mode 100644 index 00000000000..6871c808ccd Binary files /dev/null and b/Resources/Textures/_NF/Objects/Devices/translator.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Devices/vulp_translator.rsi/meta.json b/Resources/Textures/_NF/Objects/Devices/translator.rsi/meta.json similarity index 72% rename from Resources/Textures/Objects/Devices/vulp_translator.rsi/meta.json rename to Resources/Textures/_NF/Objects/Devices/translator.rsi/meta.json index 42e459937e1..0202c0c39c7 100644 --- a/Resources/Textures/Objects/Devices/vulp_translator.rsi/meta.json +++ b/Resources/Textures/_NF/Objects/Devices/translator.rsi/meta.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "license": "CC-BY-SA-3.0", "copyright": "baystation12", "size": { @@ -9,6 +9,9 @@ "states": [ { "name": "icon" + }, + { + "name": "translator" } ] } diff --git a/Resources/Textures/_NF/Objects/Devices/translator.rsi/translator.png b/Resources/Textures/_NF/Objects/Devices/translator.rsi/translator.png new file mode 100644 index 00000000000..6c54a0b8636 Binary files /dev/null and b/Resources/Textures/_NF/Objects/Devices/translator.rsi/translator.png differ diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 897506623c6..546f89add56 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -183,6 +183,9 @@ binds: - function: OpenCharacterMenu type: State key: C +- function: OpenLanguageMenu # Frontier / Languages + type: State + key: M - function: TextCursorSelect # TextCursorSelect HAS to be above ExamineEntity # So that LineEdit receives it correctly.