diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 639f326f7f4..9aa3f3846bf 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -68,6 +68,7 @@ public static void SetupContexts(IInputContextContainer contexts) human.AddFunction(ContentKeyFunctions.MovePulledObject); human.AddFunction(ContentKeyFunctions.ReleasePulledObject); human.AddFunction(ContentKeyFunctions.OpenCraftingMenu); + human.AddFunction(ContentKeyFunctions.OpenLanguageMenu); // Corvax-Languages human.AddFunction(ContentKeyFunctions.OpenInventoryMenu); human.AddFunction(ContentKeyFunctions.SmartEquipBackpack); human.AddFunction(ContentKeyFunctions.SmartEquipBelt); diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 24be904e061..12f13fb2ca2 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -214,6 +214,7 @@ void AddCheckBox(string checkBoxName, bool currentState, Action UIManager.GetActiveUIWidgetOrNull(); @@ -47,6 +49,7 @@ public void UnloadButtons() _action.UnloadButton(); _sandbox.UnloadButton(); _emotes.UnloadButton(); + _language.UnloadButton(); // Corvax-Languages } public void LoadButtons() @@ -60,5 +63,6 @@ public void LoadButtons() _action.LoadButton(); _sandbox.LoadButton(); _emotes.LoadButton(); + _language.LoadButton(); // Corvax-Languages } } diff --git a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml index dc8972970ac..e446e41a0de 100644 --- a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml +++ b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml @@ -73,6 +73,18 @@ HorizontalExpand="True" AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}" /> + + + + + + + + + + + + + + + + + diff --git a/Content.Client/_EinsteinEngine/Language/LanguageMenuWindow.xaml.cs b/Content.Client/_EinsteinEngine/Language/LanguageMenuWindow.xaml.cs new file mode 100644 index 00000000000..365df347371 --- /dev/null +++ b/Content.Client/_EinsteinEngine/Language/LanguageMenuWindow.xaml.cs @@ -0,0 +1,143 @@ +using Content.Client._EinsteinEngine.Language.Systems; +using Content.Shared._EinsteinEngine.Language; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; + +namespace Content.Client._EinsteinEngine.Language; + +[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(); + _clientLanguageSystem.OnLanguagesChanged += UpdateState; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + _clientLanguageSystem.OnLanguagesChanged -= UpdateState; + } + + protected override void Opened() + { + UpdateState(); + } + + private void UpdateState() + { + var languageSpeaker = _clientLanguageSystem.GetLocalSpeaker(); + if (languageSpeaker == null) + return; + + UpdateState(languageSpeaker.CurrentLanguage, languageSpeaker.SpokenLanguages); + } + + public void UpdateState(ProtoId currentLanguage, List> spokenLanguages) + { + var langName = Loc.GetString($"language-{currentLanguage}-name"); + 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(ProtoId language) + { + var proto = _clientLanguageSystem.GetLanguagePrototype(language); + var state = new EntryState { Language = language }; + + var container = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Vertical }; + + #region Header + var header = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Horizontal, + HorizontalExpand = true, + SeparationOverride = 2 + }; + + var name = new Label + { + Text = proto?.Name ?? Loc.GetString("generic-error"), + MinWidth = 50, + HorizontalExpand = true + }; + + var button = new Button { Text = "Choose" }; + button.OnPressed += _ => OnLanguageChosen(language); + state.Button = button; + + header.AddChild(name); + header.AddChild(button); + + container.AddChild(header); + #endregion + + #region Collapsible description + var body = new CollapsibleBody + { + HorizontalExpand = true, + Margin = new Thickness(4f, 4f) + }; + + var description = new RichTextLabel { HorizontalExpand = true }; + description.SetMessage(proto?.Description ?? Loc.GetString("generic-error")); + body.AddChild(description); + + var collapser = new Collapsible(Loc.GetString("language-menu-description-header"), body) + { + Orientation = BoxContainer.LayoutOrientation.Vertical, + HorizontalExpand = true + }; + + container.AddChild(collapser); + #endregion + + // 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(ProtoId id) + { + _clientLanguageSystem.RequestSetLanguage(id); + + // Predict the change + if (_clientLanguageSystem.GetLocalSpeaker()?.SpokenLanguages is {} languages) + UpdateState(id, languages); + } + + private struct EntryState + { + public ProtoId Language; + public Button? Button; + } +} diff --git a/Content.Client/_EinsteinEngine/Language/Systems/LanguageSystem.cs b/Content.Client/_EinsteinEngine/Language/Systems/LanguageSystem.cs new file mode 100644 index 00000000000..974de159b71 --- /dev/null +++ b/Content.Client/_EinsteinEngine/Language/Systems/LanguageSystem.cs @@ -0,0 +1,61 @@ +using Content.Shared._EinsteinEngine.Language; +using Content.Shared._EinsteinEngine.Language.Components; +using Content.Shared._EinsteinEngine.Language.Events; +using Content.Shared._EinsteinEngine.Language.Systems; +using Robust.Client.Player; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Client._EinsteinEngine.Language.Systems; + +public sealed class LanguageSystem : SharedLanguageSystem +{ + [Dependency] private readonly IPlayerManager _playerManager = default!; + + /// + /// Invoked when the languages of the local player entity change, for use in UI. + /// + public event Action? OnLanguagesChanged; + + public override void Initialize() + { + _playerManager.LocalPlayerAttached += NotifyUpdate; + SubscribeLocalEvent(OnHandleState); + } + + private void OnHandleState(Entity ent, ref ComponentHandleState args) + { + if (args.Current is not LanguageSpeakerComponent.State state) + return; + + ent.Comp.CurrentLanguage = state.CurrentLanguage; + ent.Comp.SpokenLanguages = state.SpokenLanguages; + ent.Comp.UnderstoodLanguages = state.UnderstoodLanguages; + + if (ent.Owner == _playerManager.LocalEntity) + NotifyUpdate(ent); + } + + /// + /// Returns the LanguageSpeakerComponent of the local player entity. + /// Will return null if the player does not have an entity, or if the client has not yet received the component state. + /// + public LanguageSpeakerComponent? GetLocalSpeaker() + { + return CompOrNull(_playerManager.LocalEntity); + } + + public void RequestSetLanguage(ProtoId language) + { + if (GetLocalSpeaker()?.CurrentLanguage?.Equals(language) == true) + return; + + RaiseNetworkEvent(new LanguagesSetMessage(language)); + } + + private void NotifyUpdate(EntityUid localPlayer) + { + RaiseLocalEvent(localPlayer, new LanguagesUpdateEvent(), broadcast: true); + OnLanguagesChanged?.Invoke(); + } +} diff --git a/Content.Client/_EinsteinEngine/UserInterface/Systems/Language/LanguageMenuUIController.cs b/Content.Client/_EinsteinEngine/UserInterface/Systems/Language/LanguageMenuUIController.cs new file mode 100644 index 00000000000..c5354930b9b --- /dev/null +++ b/Content.Client/_EinsteinEngine/UserInterface/Systems/Language/LanguageMenuUIController.cs @@ -0,0 +1,87 @@ +using Content.Client._EinsteinEngine.Language; +using Content.Client.Gameplay; +using Content.Client.UserInterface.Controls; +using Content.Shared.Input; +using JetBrains.Annotations; +using Robust.Client.UserInterface.Controllers; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Input.Binding; +using Robust.Shared.Utility; +using static Robust.Client.UserInterface.Controls.BaseButton; + +namespace Content.Client._EinsteinEngine.UserInterface.Systems.Language; + +[UsedImplicitly] +public sealed class LanguageMenuUIController : UIController, IOnStateEntered, IOnStateExited +{ + public LanguageMenuWindow? LanguageWindow; + private MenuButton? LanguageButton => UIManager.GetActiveUIWidgetOrNull()?.LanguageButton; + + public void OnStateEntered(GameplayState state) + { + DebugTools.Assert(LanguageWindow == null); + + LanguageWindow = UIManager.CreateWindow(); + LayoutContainer.SetAnchorPreset(LanguageWindow, LayoutContainer.LayoutPreset.CenterTop); + + LanguageWindow.OnClose += () => + { + if (LanguageButton != null) + LanguageButton.Pressed = false; + }; + LanguageWindow.OnOpen += () => + { + if (LanguageButton != null) + LanguageButton.Pressed = true; + }; + + CommandBinds.Builder.Bind(ContentKeyFunctions.OpenLanguageMenu, + InputCmdHandler.FromDelegate(_ => ToggleWindow())).Register(); + } + + public void OnStateExited(GameplayState state) + { + if (LanguageWindow != null) + { + LanguageWindow.Dispose(); + LanguageWindow = null; + } + + CommandBinds.Unregister(); + } + + public void UnloadButton() + { + if (LanguageButton == null) + return; + + LanguageButton.OnPressed -= LanguageButtonPressed; + } + + public void LoadButton() + { + if (LanguageButton == null) + return; + + LanguageButton.OnPressed += LanguageButtonPressed; + } + + private void LanguageButtonPressed(ButtonEventArgs args) + { + ToggleWindow(); + } + + private void ToggleWindow() + { + if (LanguageWindow == null) + return; + + if (LanguageButton != null) + LanguageButton.SetClickPressed(!LanguageWindow.IsOpen); + + if (LanguageWindow.IsOpen) + LanguageWindow.Close(); + else + LanguageWindow.Open(); + } +} diff --git a/Content.Server/Administration/Commands/DSay.cs b/Content.Server/Administration/Commands/DSay.cs index 8e7f0f4bf05..d96654bc015 100644 --- a/Content.Server/Administration/Commands/DSay.cs +++ b/Content.Server/Administration/Commands/DSay.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; // Corvax-Languages using Robust.Shared.Console; namespace Content.Server.Administration.Commands @@ -34,7 +35,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) return; var chat = _e.System(); - chat.TrySendInGameOOCMessage(entity, message, InGameOOCChatType.Dead, false, shell, player); + chat.TrySendInGameOOCMessage(entity, message, SharedChatSystem.InGameOOCChatType.Dead, false, shell, player); // Corvax-Languages } } } diff --git a/Content.Server/Administration/Commands/OSay.cs b/Content.Server/Administration/Commands/OSay.cs index 2f17bd9d70a..9ced309ab71 100644 --- a/Content.Server/Administration/Commands/OSay.cs +++ b/Content.Server/Administration/Commands/OSay.cs @@ -2,6 +2,7 @@ using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Database; using Robust.Shared.Console; @@ -24,7 +25,7 @@ public override CompletionResult GetCompletion(IConsoleShell shell, string[] arg if (args.Length == 2) { - return CompletionResult.FromHintOptions( Enum.GetNames(typeof(InGameICChatType)), + return CompletionResult.FromHintOptions( Enum.GetNames(typeof(SharedChatSystem.InGameICChatType)), // Corvax-Languages Loc.GetString("osay-command-arg-type")); } @@ -44,7 +45,8 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) return; } - var chatType = (InGameICChatType) Enum.Parse(typeof(InGameICChatType), args[1]); + var chatType = + (SharedChatSystem.InGameICChatType)Enum.Parse(typeof(SharedChatSystem.InGameICChatType), args[1]); // Corvax-Languages if (!NetEntity.TryParse(args[0], out var sourceNet) || !_entityManager.TryGetEntity(sourceNet, out var source) || !_entityManager.EntityExists(source)) { diff --git a/Content.Server/Advertise/EntitySystems/AdvertiseSystem.cs b/Content.Server/Advertise/EntitySystems/AdvertiseSystem.cs index 7f2e1281835..29333b37d05 100644 --- a/Content.Server/Advertise/EntitySystems/AdvertiseSystem.cs +++ b/Content.Server/Advertise/EntitySystems/AdvertiseSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Advertise.Components; using Content.Server.Chat.Systems; using Content.Server.Power.Components; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.VendingMachines; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -62,7 +63,7 @@ public void SayAdvertisement(EntityUid uid, AdvertiseComponent? advert = null) return; if (_prototypeManager.TryIndex(advert.Pack, out var advertisements)) - _chat.TrySendInGameICMessage(uid, Loc.GetString(_random.Pick(advertisements.Values)), InGameICChatType.Speak, hideChat: true); + _chat.TrySendInGameICMessage(uid, Loc.GetString(_random.Pick(advertisements.Values)), SharedChatSystem.InGameICChatType.Speak, hideChat: true); // Corvax-Languages } public override void Update(float frameTime) diff --git a/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs index a0a709e5fad..6ba7988b664 100644 --- a/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs +++ b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Advertise.Components; using Content.Server.Chat.Systems; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Dataset; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -42,7 +43,7 @@ public bool TrySpeak(Entity entity) return false; var message = Loc.GetString(_random.Pick(messagePack.Values), ("name", Name(entity))); - _chat.TrySendInGameICMessage(entity, message, InGameICChatType.Speak, true); + _chat.TrySendInGameICMessage(entity, message, SharedChatSystem.InGameICChatType.Speak, true); // Corvax-Languages entity.Comp.Flag = false; return true; } diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs index 6209f00419d..b850bcb99c3 100644 --- a/Content.Server/Body/Systems/RespiratorSystem.cs +++ b/Content.Server/Body/Systems/RespiratorSystem.cs @@ -9,6 +9,7 @@ using Content.Shared.Atmos; using Content.Shared.Body.Components; using Content.Shared.Body.Prototypes; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Damage; @@ -96,7 +97,7 @@ public override void Update(float frameTime) if (_gameTiming.CurTime >= respirator.LastGaspEmoteTime + respirator.GaspEmoteCooldown) { respirator.LastGaspEmoteTime = _gameTiming.CurTime; - _chat.TryEmoteWithChat(uid, respirator.GaspEmote, ChatTransmitRange.HideChat, ignoreActionBlocker: true); + _chat.TryEmoteWithChat(uid, respirator.GaspEmote, SharedChatSystem.ChatTransmitRange.HideChat, ignoreActionBlocker: true); // Corvax-Languages } TakeSuffocationDamage((uid, respirator)); diff --git a/Content.Server/Chat/Commands/LOOCCommand.cs b/Content.Server/Chat/Commands/LOOCCommand.cs index e303b9766d8..041d9fec7d2 100644 --- a/Content.Server/Chat/Commands/LOOCCommand.cs +++ b/Content.Server/Chat/Commands/LOOCCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; // Corvax-Languages using Robust.Shared.Console; using Robust.Shared.Enums; @@ -35,7 +36,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) if (string.IsNullOrEmpty(message)) return; - _e.System().TrySendInGameOOCMessage(entity, message, InGameOOCChatType.Looc, false, shell, player); + _e.System().TrySendInGameOOCMessage(entity, message, SharedChatSystem.InGameOOCChatType.Looc, false, shell, player); // Corvax-Languages } } } diff --git a/Content.Server/Chat/Commands/MeCommand.cs b/Content.Server/Chat/Commands/MeCommand.cs index e763d5656e1..59306ad2aa5 100644 --- a/Content.Server/Chat/Commands/MeCommand.cs +++ b/Content.Server/Chat/Commands/MeCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; // Corvax-Languages using Robust.Shared.Console; using Robust.Shared.Enums; @@ -37,7 +38,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) return; IoCManager.Resolve().GetEntitySystem() - .TrySendInGameICMessage(playerEntity, message, InGameICChatType.Emote, ChatTransmitRange.Normal, false, shell, player); + .TrySendInGameICMessage(playerEntity, message, SharedChatSystem.InGameICChatType.Emote, SharedChatSystem.ChatTransmitRange.Normal, false, shell, player); // Corvax-Languages } } } diff --git a/Content.Server/Chat/Commands/SayCommand.cs b/Content.Server/Chat/Commands/SayCommand.cs index df6e548e5d9..ef57e413dad 100644 --- a/Content.Server/Chat/Commands/SayCommand.cs +++ b/Content.Server/Chat/Commands/SayCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; // Corvax-Languages using Robust.Shared.Console; using Robust.Shared.Enums; @@ -37,7 +38,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) return; IoCManager.Resolve().GetEntitySystem() - .TrySendInGameICMessage(playerEntity, message, InGameICChatType.Speak, ChatTransmitRange.Normal, false, shell, player); + .TrySendInGameICMessage(playerEntity, message, SharedChatSystem.InGameICChatType.Speak, SharedChatSystem.ChatTransmitRange.Normal, false, shell, player); // Corvax-Languages } } } diff --git a/Content.Server/Chat/Commands/WhisperCommand.cs b/Content.Server/Chat/Commands/WhisperCommand.cs index 13effa34464..4179c4d5442 100644 --- a/Content.Server/Chat/Commands/WhisperCommand.cs +++ b/Content.Server/Chat/Commands/WhisperCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; // Corvax-Languages using Robust.Shared.Console; using Robust.Shared.Enums; @@ -37,7 +38,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) return; IoCManager.Resolve().GetEntitySystem() - .TrySendInGameICMessage(playerEntity, message, InGameICChatType.Whisper, ChatTransmitRange.Normal, false, shell, player); + .TrySendInGameICMessage(playerEntity, message, SharedChatSystem.InGameICChatType.Whisper, SharedChatSystem.ChatTransmitRange.Normal, false, shell, player); // Corvax-Languages } } } diff --git a/Content.Server/Chat/Systems/AutoEmoteSystem.cs b/Content.Server/Chat/Systems/AutoEmoteSystem.cs index 3d6bd535401..9ff3bbc7c31 100644 --- a/Content.Server/Chat/Systems/AutoEmoteSystem.cs +++ b/Content.Server/Chat/Systems/AutoEmoteSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Chat.Prototypes; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -46,7 +47,7 @@ public override void Update(float frameTime) if (autoEmotePrototype.WithChat) { - _chatSystem.TryEmoteWithChat(uid, autoEmotePrototype.EmoteId, autoEmotePrototype.HiddenFromChatWindow ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal); + _chatSystem.TryEmoteWithChat(uid, autoEmotePrototype.EmoteId, autoEmotePrototype.HiddenFromChatWindow ? SharedChatSystem.ChatTransmitRange.HideChat : SharedChatSystem.ChatTransmitRange.Normal); // Corvax-Languages } else { diff --git a/Content.Server/Chat/Systems/ChatSystem.Emote.cs b/Content.Server/Chat/Systems/ChatSystem.Emote.cs index fddf453ff06..87f8348d183 100644 --- a/Content.Server/Chat/Systems/ChatSystem.Emote.cs +++ b/Content.Server/Chat/Systems/ChatSystem.Emote.cs @@ -93,7 +93,8 @@ public void TryEmoteWithChat( { // not all emotes are loc'd, but for the ones that are we pass in entity var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source)); - SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker); + var language = _language.GetLanguage(source); // Corvax-Languages + SendEntityEmote(source, action, range, nameOverride, language, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker); // Corvax-Languages } // do the rest of emote event logic here diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 140afb456b8..ab5fb43f8df 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -36,6 +36,9 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Replays; +using Content.Shared._EinsteinEngine.Language; // Corvax-Languages +using Content.Server._EinsteinEngine.Language; // Corvax-Languages +using Content.Server.Speech; // Corvax-Languages using Robust.Shared.Utility; namespace Content.Server.Chat.Systems; @@ -63,6 +66,7 @@ public sealed partial class ChatSystem : SharedChatSystem [Dependency] private readonly ReplacementAccentSystem _wordreplacement = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly ExamineSystemShared _examineSystem = default!; + [Dependency] private readonly LanguageSystem _language = default!; // Corvax-Languages // Corvax-TTS-Start: Moved from Server to Shared // public const int VoiceRange = 10; // how far voice goes in world units @@ -150,7 +154,9 @@ public void TrySendInGameICMessage( IConsoleShell? shell = null, ICommonSession? player = null, string? nameOverride = null, bool checkRadioPrefix = true, - bool ignoreActionBlocker = false) + bool ignoreActionBlocker = false, + LanguagePrototype? languageOverride = null // Corvax-Languages + ) { TrySendInGameICMessage(source, message, desiredType, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, hideLog, shell, player, nameOverride, checkRadioPrefix, ignoreActionBlocker); } @@ -176,7 +182,8 @@ public void TrySendInGameICMessage( ICommonSession? player = null, string? nameOverride = null, bool checkRadioPrefix = true, - bool ignoreActionBlocker = false + bool ignoreActionBlocker = false, + LanguagePrototype? languageOverride = null // Corvax-Languages ) { if (HasComp(source)) @@ -220,6 +227,8 @@ public void TrySendInGameICMessage( message = message[1..]; } + var language = languageOverride ?? _language.GetLanguage(source); // Corvax-Languages + bool shouldCapitalize = (desiredType != InGameICChatType.Emote); bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation); // Capitalizing the word I only happens in English, so we check language here @@ -231,19 +240,25 @@ public void TrySendInGameICMessage( // Was there an emote in the message? If so, send it. if (player != null && emoteStr != message && emoteStr != null) { - SendEntityEmote(source, emoteStr, range, nameOverride, ignoreActionBlocker); + SendEntityEmote(source, emoteStr, range, nameOverride, language, ignoreActionBlocker); // Corvax-Languages } // This can happen if the entire string is sanitized out. if (string.IsNullOrEmpty(message)) return; +// Corvax-Languages-Start + // This is really terrible. I hate myself for doing this. + if (language.SpeechOverride.ChatTypeOverride is { } chatTypeOverride) + desiredType = chatTypeOverride; +// Corvax-Languages-End + // This message may have a radio prefix, and should then be whispered to the resolved radio channel if (checkRadioPrefix) { - if (TryProccessRadioMessage(source, message, out var modMessage, out var channel)) + if (TryProccessRadioMessage(source, message, out var modMessage, out var channel)) // Corvax-Languages { - SendEntityWhisper(source, modMessage, range, channel, nameOverride, hideLog, ignoreActionBlocker); + SendEntityWhisper(source, modMessage, range, channel, nameOverride, language, hideLog, ignoreActionBlocker); // Corvax Languages return; } } @@ -252,13 +267,13 @@ public void TrySendInGameICMessage( switch (desiredType) { case InGameICChatType.Speak: - SendEntitySpeak(source, message, range, nameOverride, hideLog, ignoreActionBlocker); + SendEntitySpeak(source, message, range, nameOverride, language, hideLog, ignoreActionBlocker); // Corvax-Languages break; case InGameICChatType.Whisper: - SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker); + SendEntityWhisper(source, message, range, null, nameOverride, language, hideLog, ignoreActionBlocker); // Corvax-Languages break; case InGameICChatType.Emote: - SendEntityEmote(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker); + SendEntityEmote(source, message, range, nameOverride, language, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker); // Corvax-Languages break; } } @@ -419,6 +434,7 @@ private void SendEntitySpeak( string originalMessage, ChatTransmitRange range, string? nameOverride, + LanguagePrototype language, // Corvax-Languages bool hideLog = false, bool ignoreActionBlocker = false ) @@ -426,7 +442,7 @@ private void SendEntitySpeak( if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker) return; - var message = TransformSpeech(source, originalMessage); + var message = TransformSpeech(source, FormattedMessage.RemoveMarkup(originalMessage), language); // Corvax-Languages if (message.Length == 0) return; @@ -449,15 +465,17 @@ private void SendEntitySpeak( speech = proto; } +// Corvax-Languages-Start name = FormattedMessage.EscapeText(name); - - 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))); - + // The chat message wrapped in a "x says y" string + var wrappedMessage = WrapPublicMessage(source, name, message, language: language); + // The chat message obfuscated via language obfuscation + var obfuscated = SanitizeInGameICMessage(source, _language.ObfuscateSpeech(message, language), out var emoteStr, true, _configurationManager.GetCVar(CCVars.ChatPunctuation), (!CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Parent.Name == "en") || (CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Name == "en")); + // The language-obfuscated message wrapped in a "x says y" string + var wrappedObfuscated = WrapPublicMessage(source, name, obfuscated, language: language); + + SendInVoiceRange(ChatChannel.Local, name, message, wrappedMessage, obfuscated, wrappedObfuscated, source, range, languageOverride: language); +// Corvax-Languages-End SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range); var ev = new EntitySpokeEvent(source, message, originalMessage, null, null); @@ -492,6 +510,7 @@ private void SendEntityWhisper( ChatTransmitRange range, RadioChannelPrototype? channel, string? nameOverride, + LanguagePrototype language, // Corvax-Languages bool hideLog = false, bool ignoreActionBlocker = false ) @@ -499,12 +518,10 @@ private void SendEntityWhisper( if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker) return; - var message = TransformSpeech(source, FormattedMessage.RemoveMarkupOrThrow(originalMessage)); + var message = TransformSpeech(source, FormattedMessage.RemoveMarkup(originalMessage), language); // // Corvax-Languages if (message.Length == 0) return; - 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). @@ -521,39 +538,55 @@ 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))); - - +// Corvax-Languages + var languageObfuscatedMessage = SanitizeInGameICMessage(source, + _language.ObfuscateSpeech(message, language), + out var emoteStr, + true, + _configurationManager.GetCVar(CCVars.ChatPunctuation), + (!CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Parent.Name == "en") || (CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Name == "en")); +// Corvax-Languages-End 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) // Corvax-Languages 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 canUnderstandLanguage = _language.CanUnderstand(listener, language.ID); + // How the entity perceives the message depends on whether it can understand its language + var perceivedMessage = FormattedMessage.EscapeText(canUnderstandLanguage ? message : languageObfuscatedMessage); + +// Corvax-Languages-Star + // Result is the intermediate message derived from the perceived one via obfuscation + // Wrapped message is the result wrapped in an "x says y" string + string result, wrappedMessage; + 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 + { + // Scenario 1: the listener can clearly understand the message + result = perceivedMessage; + wrappedMessage = WrapWhisperMessage(source, "chat-manager-entity-whisper-wrap-message", name, result, language); + } else if (_examineSystem.InRangeUnOccluded(source, listener, WhisperMuffledRange)) - _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 + { + // Scenerio 2: if the listener is too far, they only hear fragments of the message + result = ObfuscateMessageReadability(perceivedMessage); + wrappedMessage = WrapWhisperMessage(source, "chat-manager-entity-whisper-wrap-message", nameIdentity, result, language); + } else - _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedUnknownMessage, source, false, session.Channel); - } + { + // Scenario 3: If listener is too far and has no line of sight, they can't identify the whisperer's identity + result = ObfuscateMessageReadability(perceivedMessage); + wrappedMessage = WrapWhisperMessage(source, "chat-manager-entity-whisper-unknown-wrap-message", string.Empty, result, language); + } - _replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); + _chatManager.ChatMessageToOne(ChatChannel.Whisper, result, wrappedMessage, source, false, session.Channel); + } + var replayWrap = WrapWhisperMessage(source, "chat-manager-entity-whisper-wrap-message", name, FormattedMessage.EscapeText(message), language); +// Corvax-Languages-End var ev = new EntitySpokeEvent(source, message, originalMessage, channel, obfuscatedMessage); RaiseLocalEvent(source, ev, true); if (!hideLog) @@ -580,6 +613,7 @@ private void SendEntityEmote( string action, ChatTransmitRange range, string? nameOverride, + LanguagePrototype language, // Corvax-Languages bool hideLog = false, bool checkEmote = true, bool ignoreActionBlocker = false, @@ -597,11 +631,11 @@ private void SendEntityEmote( var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message", ("entityName", name), ("entity", ent), - ("message", FormattedMessage.RemoveMarkupOrThrow(action))); + ("message", FormattedMessage.RemoveMarkup(action))); // Corvax-Languages if (checkEmote) TryEmoteChatInput(source, action); - SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author); + SendInVoiceRange(ChatChannel.Emotes, name, action, wrappedMessage, obfuscated: "", obfuscatedWrappedMessage: "", source, range, author); // Corvax-Languages if (!hideLog) if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user} as {name}: {action}"); @@ -627,11 +661,17 @@ private void SendLOOC(EntityUid source, ICommonSession player, string message, b var wrappedMessage = Loc.GetString("chat-manager-entity-looc-wrap-message", ("entityName", name), ("message", FormattedMessage.EscapeText(message))); - - SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, player.UserId); +// Corvax-Languages-Start + 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}"); } - +// Corvax-Languages-End private void SendDeadChat(EntityUid source, ICommonSession player, string message, bool hideChat) { var clients = GetDeadChatClients(); @@ -709,17 +749,31 @@ 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) // Corvax-Languages { + var language = languageOverride ?? _language.GetLanguage(source); // Corvax-Languages 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); - } + // Corvax-Languages-Star + if (session.AttachedEntity is not { Valid: true } playerEntity) + continue; + EntityUid listener = session.AttachedEntity.Value; + // If the channel does not support languages, or the entity can understand the message, send the original message, otherwise send the obfuscated version + if (channel == ChatChannel.LOOC || channel == ChatChannel.Emotes || _language.CanUnderstand(listener, language.ID)) + { + _chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author); + } + else + { + _chatManager.ChatMessageToOne(channel, obfuscated, obfuscatedWrappedMessage, source, entHideChat, session.Channel, author: author); + } + } +// Corvax-Languages-End _replay.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); } @@ -774,9 +828,12 @@ private string SanitizeInGameOOCMessage(string message) return newMessage; } - - public string TransformSpeech(EntityUid sender, string message) +// Corvax-Languages-Start + public string TransformSpeech(EntityUid sender, string message, LanguagePrototype language) { + if (!language.SpeechOverride.RequireSpeech) + return message; // Do not apply speech accents if there's no speech involved. +// Corvax-Languages-End var ev = new TransformSpeechEvent(sender, message); RaiseLocalEvent(ev); @@ -826,7 +883,50 @@ public string SanitizeMessageReplaceWords(string message) return msg; } +// Corvax-Languages-Start + /// + /// Wraps a message sent by the specified entity into an "x says y" string. + /// + public string WrapPublicMessage(EntityUid source, string name, string message, LanguagePrototype? language = null) + { + var wrapId = GetSpeechVerb(source, message).Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message"; + return WrapMessage(wrapId, InGameICChatType.Speak, source, name, message, language); + } + + /// + /// Wraps a message whispered by the specified entity into an "x whispers y" string. + /// + public string WrapWhisperMessage(EntityUid source, LocId defaultWrap, string entityName, string message, LanguagePrototype? language = null) + { + return WrapMessage(defaultWrap, InGameICChatType.Whisper, source, entityName, message, language); + } + + /// + /// Wraps a message sent by the specified entity into the specified wrap string. + /// + public string WrapMessage(LocId wrapId, InGameICChatType chatType, EntityUid source, string entityName, string message, LanguagePrototype? language) + { + language ??= _language.GetLanguage(source); + if (language.SpeechOverride.MessageWrapOverrides.TryGetValue(chatType, out var wrapOverride)) + wrapId = wrapOverride; + var speech = GetSpeechVerb(source, message); + var verbId = language.SpeechOverride.SpeechVerbOverrides is { } verbsOverride + ? _random.Pick(verbsOverride).ToString() + : _random.Pick(speech.SpeechVerbStrings); + var color = DefaultSpeakColor; + if (language.SpeechOverride.Color is { } colorOverride) + color = Color.InterpolateBetween(color, colorOverride, colorOverride.A); + + return Loc.GetString(wrapId, + ("color", color), + ("entityName", entityName), + ("verb", Loc.GetString(verbId)), + ("fontType", language.SpeechOverride.FontId ?? speech.FontId), + ("fontSize", language.SpeechOverride.FontSize ?? speech.FontSize), + ("message", message)); + } +// Corvax-Languages-End /// /// Returns list of players and ranges for all players withing some range. Also returns observers with a range of -1. /// @@ -873,7 +973,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) // Corvax-Languages { var modifiedMessage = new StringBuilder(message); @@ -977,41 +1077,7 @@ public EntitySpokeEvent(EntityUid source, string message, string originalMessage Message = message; OriginalMessage = originalMessage; // Corvax-TTS: Spec symbol sanitize Channel = channel; - ObfuscatedMessage = obfuscatedMessage; + IsWhisper = isWhisper; // Corvax-Languages + Language = language; // Corvax-Languages } } - -/// -/// InGame IC chat is for chat that is specifically ingame (not lobby) but is also in character, i.e. speaking. -/// -// ReSharper disable once InconsistentNaming -public enum InGameICChatType : byte -{ - Speak, - Emote, - Whisper -} - -/// -/// InGame OOC chat is for chat that is specifically ingame (not lobby) but is OOC, like deadchat or LOOC. -/// -public enum InGameOOCChatType : byte -{ - Looc, - Dead -} - -/// -/// Controls transmission of chat. -/// -public enum ChatTransmitRange : byte -{ - /// Acts normal, ghosts can hear across the map, etc. - Normal, - /// Normal but ghosts are still range-limited. - GhostRangeLimit, - /// Hidden from the chat window. - HideChat, - /// Ghosts can't hear or see it at all. Regular players can if in-range. - NoGhosts -} diff --git a/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs index 878c517d924..2371182976f 100644 --- a/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs +++ b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs @@ -1,3 +1,5 @@ +using Content.Shared.Chat; // Corvax-Languages + namespace Content.Server.Chat.Systems; using Content.Shared.Chat.Prototypes; @@ -38,7 +40,7 @@ private void OnDamage(EntityUid uid, EmoteOnDamageComponent emoteOnDamage, Damag var emote = _random.Pick(emoteOnDamage.Emotes); if (emoteOnDamage.WithChat) { - _chatSystem.TryEmoteWithChat(uid, emote, emoteOnDamage.HiddenFromChatWindow ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal); + _chatSystem.TryEmoteWithChat(uid, emote, emoteOnDamage.HiddenFromChatWindow ? SharedChatSystem.ChatTransmitRange.HideChat : SharedChatSystem.ChatTransmitRange.Normal); // Corvax-Languages } else { diff --git a/Content.Server/Chat/Systems/SpeakOnUseSystem.cs b/Content.Server/Chat/Systems/SpeakOnUseSystem.cs index addec79e410..1423846f14a 100644 --- a/Content.Server/Chat/Systems/SpeakOnUseSystem.cs +++ b/Content.Server/Chat/Systems/SpeakOnUseSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Dataset; using Content.Shared.Interaction.Events; using Content.Shared.Timing; @@ -36,7 +37,7 @@ public void OnUseInHand(EntityUid uid, SpeakOnUseComponent? component, UseInHand return; var message = Loc.GetString(_random.Pick(messagePack.Values)); - _chat.TrySendInGameICMessage(uid, message, InGameICChatType.Speak, true); + _chat.TrySendInGameICMessage(uid, message, SharedChatSystem.InGameICChatType.Speak, true); // Corvax-Languages _useDelay.TryResetDelay((uid, useDelay)); } } diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs index 3893f31d25d..64bad0fa911 100644 --- a/Content.Server/Cloning/CloningSystem.cs +++ b/Content.Server/Cloning/CloningSystem.cs @@ -11,6 +11,7 @@ using Content.Server.Power.EntitySystems; using Content.Shared.Atmos; using Content.Shared.CCVar; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Chemistry.Components; using Content.Shared.Cloning; using Content.Shared.Damage; @@ -182,7 +183,7 @@ public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity 0 && clonePod.ConnectedConsole != null) - _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false); + _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), SharedChatSystem.InGameICChatType.Speak, false); // Corvax-Languages if (_robustRandom.Prob(chance)) { diff --git a/Content.Server/Cluwne/CluwneSystem.cs b/Content.Server/Cluwne/CluwneSystem.cs index f24f0143f31..8681f08a601 100644 --- a/Content.Server/Cluwne/CluwneSystem.cs +++ b/Content.Server/Cluwne/CluwneSystem.cs @@ -12,6 +12,7 @@ using Robust.Shared.Prototypes; using Content.Server.Emoting.Systems; using Content.Server.Speech.EntitySystems; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Cluwne; using Content.Shared.Interaction.Components; using Robust.Shared.Audio.Systems; @@ -92,14 +93,14 @@ private void OnEmote(EntityUid uid, CluwneComponent component, ref EmoteEvent ar if (_robustRandom.Prob(component.GiggleRandomChance)) { _audio.PlayPvs(component.SpawnSound, uid); - _chat.TrySendInGameICMessage(uid, "honks", InGameICChatType.Emote, ChatTransmitRange.Normal); + _chat.TrySendInGameICMessage(uid, "honks", SharedChatSystem.InGameICChatType.Emote, SharedChatSystem.ChatTransmitRange.Normal); // Corvax-Languages } else if (_robustRandom.Prob(component.KnockChance)) { _audio.PlayPvs(component.KnockSound, uid); _stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(component.ParalyzeTime), true); - _chat.TrySendInGameICMessage(uid, "spasms", InGameICChatType.Emote, ChatTransmitRange.Normal); + _chat.TrySendInGameICMessage(uid, "spasms", SharedChatSystem.InGameICChatType.Emote, SharedChatSystem.ChatTransmitRange.Normal); // Corvax-Languages } } diff --git a/Content.Server/EntityEffects/Effects/Emote.cs b/Content.Server/EntityEffects/Effects/Emote.cs index 00bdaec455c..d9b7db44c26 100644 --- a/Content.Server/EntityEffects/Effects/Emote.cs +++ b/Content.Server/EntityEffects/Effects/Emote.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Chat.Prototypes; using Content.Shared.EntityEffects; using JetBrains.Annotations; @@ -33,7 +34,7 @@ public override void Effect(EntityEffectBaseArgs args) var chatSys = args.EntityManager.System(); if (ShowInChat) - chatSys.TryEmoteWithChat(args.TargetEntity, EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: Force); + chatSys.TryEmoteWithChat(args.TargetEntity, EmoteId, SharedChatSystem.ChatTransmitRange.GhostRangeLimit, forceEmote: Force); // Corvax-Languages else chatSys.TryEmoteWithoutChat(args.TargetEntity, EmoteId); diff --git a/Content.Server/EntityEffects/Effects/MakeSentient.cs b/Content.Server/EntityEffects/Effects/MakeSentient.cs index c4870438486..8f8150f876e 100644 --- a/Content.Server/EntityEffects/Effects/MakeSentient.cs +++ b/Content.Server/EntityEffects/Effects/MakeSentient.cs @@ -1,7 +1,13 @@ +using System.Linq;// Corvax-Languages +using Content.Server._EinsteinEngine.Language;// Corvax-Languages using Content.Server.Ghost.Roles.Components; using Content.Server.Speech.Components; using Content.Shared.EntityEffects; +using Content.Shared._EinsteinEngine.Language; // Corvax-Languages +using Content.Shared._EinsteinEngine.Language.Components; // Corvax-Languages +using Content.Shared._EinsteinEngine.Language.Systems;// Corvax-Languages using Content.Shared.Mind.Components; +using Content.Shared._EinsteinEngine.Language.Events; // Corvax-Languages using Robust.Shared.Prototypes; namespace Content.Server.EntityEffects.Effects; @@ -21,7 +27,19 @@ public override void Effect(EntityEffectBaseArgs args) // We call this before the mind check to allow things like player-controlled mice to be able to benefit from the effect entityManager.RemoveComponent(uid); entityManager.RemoveComponent(uid); +// Corvax-Languages-Start + var speaker = entityManager.EnsureComponent(uid); + var knowledge = entityManager.EnsureComponent(uid); + var fallback = SharedLanguageSystem.FallbackLanguagePrototype; + if (!knowledge.UnderstoodLanguages.Contains(fallback)) + knowledge.UnderstoodLanguages.Add(fallback); + + if (!knowledge.SpokenLanguages.Contains(fallback)) + knowledge.SpokenLanguages.Add(fallback); + + IoCManager.Resolve().GetEntitySystem().UpdateEntityLanguages(uid); +// Corvax-Languages-End // 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/Magic/MagicSystem.cs b/Content.Server/Magic/MagicSystem.cs index 2cf5136b427..a74e63ffa41 100644 --- a/Content.Server/Magic/MagicSystem.cs +++ b/Content.Server/Magic/MagicSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat;// Corvax-Languages using Content.Shared.Magic; using Content.Shared.Magic.Events; @@ -17,6 +18,6 @@ public override void Initialize() private void OnSpellSpoken(ref SpeakSpellEvent args) { - _chat.TrySendInGameICMessage(args.Performer, Loc.GetString(args.Speech), InGameICChatType.Speak, false); + _chat.TrySendInGameICMessage(args.Performer, Loc.GetString(args.Speech), SharedChatSystem.InGameICChatType.Speak, false); // Corvax-Languages } } diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs index c9cb6cc58dc..9bad12e52f8 100644 --- a/Content.Server/Medical/DefibrillatorSystem.cs +++ b/Content.Server/Medical/DefibrillatorSystem.cs @@ -7,6 +7,7 @@ using Content.Server.Popups; using Content.Server.PowerCell; using Content.Server.Traits.Assorted; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Damage; using Content.Shared.DoAfter; using Content.Shared.Interaction; @@ -150,12 +151,12 @@ public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorCo if (_rotting.IsRotten(target)) { _chatManager.TrySendInGameICMessage(uid, Loc.GetString("defibrillator-rotten"), - InGameICChatType.Speak, true); + SharedChatSystem.InGameICChatType.Speak, true); // Corvax-Languages } else if (HasComp(target)) { _chatManager.TrySendInGameICMessage(uid, Loc.GetString("defibrillator-unrevivable"), - InGameICChatType.Speak, true); + SharedChatSystem.InGameICChatType.Speak, true); // Corvax-Languages } else { @@ -183,7 +184,7 @@ public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorCo else { _chatManager.TrySendInGameICMessage(uid, Loc.GetString("defibrillator-no-mind"), - InGameICChatType.Speak, true); + SharedChatSystem.InGameICChatType.Speak, true); // Corvax-Languages } } diff --git a/Content.Server/Mind/Commands/MakeSentientCommand.cs b/Content.Server/Mind/Commands/MakeSentientCommand.cs index 5e19d135b6f..a4a7d84fc7f 100644 --- a/Content.Server/Mind/Commands/MakeSentientCommand.cs +++ b/Content.Server/Mind/Commands/MakeSentientCommand.cs @@ -1,7 +1,11 @@ using Content.Server.Administration; using Content.Shared.Administration; using Content.Shared.Emoting; +using Content.Server._EinsteinEngine.Language; // Corvax-Languages using Content.Shared.Examine; +using Content.Shared._EinsteinEngine.Language; // Corvax-Languages +using Content.Shared._EinsteinEngine.Language.Components; // Corvax-Languages +using Content.Shared._EinsteinEngine.Language.Systems; // Corvax-Languages using Content.Shared.Mind.Components; using Content.Shared.Movement.Components; using Content.Shared.Speech; @@ -55,6 +59,15 @@ public static void MakeSentient(EntityUid uid, IEntityManager entityManager, boo { entityManager.EnsureComponent(uid); entityManager.EnsureComponent(uid); + +// Corvax-Languages-Start + var language = IoCManager.Resolve().GetEntitySystem(); + var speaker = entityManager.EnsureComponent(uid); + // If the entity already speaks some language (like monkey or robot), we do nothing else + // Otherwise, we give them the fallback language + if (speaker.SpokenLanguages.Count == 0) + language.AddLanguage(uid, SharedLanguageSystem.FallbackLanguagePrototype); +// Corvax-Languages-End } entityManager.EnsureComponent(uid); diff --git a/Content.Server/Mobs/CritMobActionsSystem.cs b/Content.Server/Mobs/CritMobActionsSystem.cs index c897102dca7..447909c488d 100644 --- a/Content.Server/Mobs/CritMobActionsSystem.cs +++ b/Content.Server/Mobs/CritMobActionsSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Chat.Systems; using Content.Server.Popups; using Content.Server.Speech.Muting; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; @@ -76,7 +77,7 @@ private void OnLastWords(EntityUid uid, MobStateActionsComponent component, Crit } lastWords += "..."; - _chat.TrySendInGameICMessage(uid, lastWords, InGameICChatType.Whisper, ChatTransmitRange.Normal, checkRadioPrefix: false, ignoreActionBlocker: true); + _chat.TrySendInGameICMessage(uid, lastWords, SharedChatSystem.InGameICChatType.Whisper, SharedChatSystem.ChatTransmitRange.Normal, checkRadioPrefix: false, ignoreActionBlocker: true); // Corvax-Languages _host.ExecuteCommand(actor.PlayerSession, "ghost"); }); diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs index 558b1fc04dc..ded808eaf53 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; // Corvax-Languages namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators; @@ -34,7 +35,7 @@ public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTi return HTNOperatorStatus.Failed; var speaker = blackboard.GetValue(NPCBlackboard.Owner); - _chat.TrySendInGameICMessage(speaker, @string, InGameICChatType.Speak, hideChat: Hidden, hideLog: Hidden); + _chat.TrySendInGameICMessage(speaker, @string, SharedChatSystem.InGameICChatType.Speak, hideChat: Hidden, hideLog: Hidden); // Corvax-Languages return base.Update(blackboard, frameTime); } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs index 8a4c655a39b..0ee695a19fc 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; // Corvax-Languages namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators; @@ -25,7 +26,7 @@ public override void Initialize(IEntitySystemManager sysManager) public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) { var speaker = blackboard.GetValue(NPCBlackboard.Owner); - _chat.TrySendInGameICMessage(speaker, Loc.GetString(Speech), InGameICChatType.Speak, hideChat: Hidden, hideLog: Hidden); + _chat.TrySendInGameICMessage(speaker, Loc.GetString(Speech), SharedChatSystem.InGameICChatType.Speak, hideChat: Hidden, hideLog: Hidden); // Corvax-Languages return base.Update(blackboard, frameTime); } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs index 2cc735194f6..4d2251737cd 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.NPC.Components; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Damage; using Content.Shared.Emag.Components; @@ -82,7 +83,7 @@ public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTi _solutionContainer.TryAddReagent(injectable.Value, treatment.Reagent, treatment.Quantity, out _); _popup.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, target); _audio.PlayPvs(botComp.InjectSound, target); - _chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, hideChat: true, hideLog: true); + _chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), SharedChatSystem.InGameICChatType.Speak, hideChat: true, hideLog: true); // Corvax-Languages return HTNOperatorStatus.Finished; } } diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index d18b044205c..2a3b56d1757 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -1,8 +1,11 @@ using Content.Server.Chat.Systems; using Content.Server.Emp; +using Content.Server._EinsteinEngine.Language; // Corvax-Languages using Content.Server.Radio.Components; using Content.Shared.Inventory.Events; using Content.Shared.Radio; +using Content.Server.Speech; // Corvax-Languages +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Radio.Components; using Content.Shared.Radio.EntitySystems; using Robust.Shared.Network; @@ -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!; // Corvax-Languages public override void Initialize() { @@ -99,8 +103,18 @@ 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); + // Corvax-Languages-Start + var parent = Transform(uid).ParentUid; + if (TryComp(parent, out ActorComponent? actor)) + { + var canUnderstand = _language.CanUnderstand(parent, args.Language.ID); + var msg = new MsgChatMessage + { + Message = canUnderstand ? args.OriginalChatMsg : args.LanguageObfuscatedChatMsg + }; + _netMan.ServerSendMessage(msg, actor.PlayerSession.Channel); + } + // Corvax-Languages-End } 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 c977fbc0489..bf561c5cb88 100644 --- a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs @@ -9,6 +9,7 @@ using Content.Server.Speech.Components; using Content.Shared.Examine; using Content.Shared.Interaction; +using Content.Server._EinsteinEngine.Language; // Corvax-Languages using Content.Shared.Power; using Content.Shared.Radio; using Content.Shared.Radio.Components; @@ -27,6 +28,7 @@ public sealed class RadioDeviceSystem : EntitySystem [Dependency] private readonly RadioSystem _radio = default!; [Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly LanguageSystem _language = default!; // Corvax-Languages // Used to prevent a shitter from using a bunch of radios to spam chat. private HashSet<(string, EntityUid)> _recentlySent = new(); @@ -216,7 +218,8 @@ 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); + var message = args.OriginalChatMsg.Message; // The chat system will handle the rest and re-obfuscate if needed. Corvax-Languages + _chat.TrySendInGameICMessage(uid, message, SharedChatSystem.InGameICChatType.Whisper, SharedChatSystem.ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false, languageOverride: args.Language); // Corvax-Languages } private void OnIntercomEncryptionChannelsChanged(Entity ent, ref EncryptionChannelsChangedEvent args) diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 3ad101e62db..5929859d1cd 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; +using Content.Server._EinsteinEngine.Language; // Corvax-Languages using Content.Server.Power.Components; using Content.Server.Radio.Components; using Content.Server.VoiceMask; @@ -8,6 +9,7 @@ using Content.Shared.Radio; using Content.Shared.Radio.Components; using Content.Shared.Speech; +using Content.Shared._EinsteinEngine.Language; // Corvax-Languages using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; @@ -29,6 +31,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!; // Corvax-Languages // set used to prevent radio feedback loops. private readonly HashSet _messages = new(); @@ -48,7 +51,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); // Corvax-Languages args.Channel = null; // prevent duplicate messages from other listeners. } } @@ -56,15 +59,24 @@ 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); + // Corvax-Languages-Start + { + var listener = component.Owner; + var msg = args.OriginalChatMsg; + if (listener != null && !_language.CanUnderstand(listener, args.Language.ID)) + msg = args.LanguageObfuscatedChatMsg; + + _netMan.ServerSendMessage(new MsgChatMessage { Message = msg}, actor.PlayerSession.Channel); + } + // Corvax-Languages-End } /// /// 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) // Corvax-Languages { - SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup); + SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup, language: language); // Corvax-Languages } /// @@ -72,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) + public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, LanguagePrototype? language = null, bool escapeMarkup = true) // Corvax-Languages + // Corvax-Languages-Start { + if (language == null) + language = _language.GetLanguage(messageSource); + + if (!language.SpeechOverride.AllowRadio) + return; + // Corvax-Languages-End // TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this. if (!_messages.Add(message)) return; @@ -98,26 +117,17 @@ public void SendRadioMessage(EntityUid messageSource, string message, RadioChann var content = escapeMarkup ? FormattedMessage.EscapeText(message) : message; +// Corvax-Languages-Start + var wrappedMessage = WrapRadioMessage(messageSource, channel, name, content, language); + var msg = new ChatMessage(ChatChannel.Radio, content, wrappedMessage, NetEntity.Invalid, null); - 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)); - - // 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, radioSource, chatMsg); + // ... you guess it + var obfuscated = _language.ObfuscateSpeech(content, language); + var obfuscatedWrapped = WrapRadioMessage(messageSource, channel, name, obfuscated, language); + var notUdsMsg = new ChatMessage(ChatChannel.Radio, obfuscated, obfuscatedWrapped, NetEntity.Invalid, null); + var ev = new RadioReceiveEvent(messageSource, channel, msg, notUdsMsg, language, radioSource); +// Corvax-Languages-End var sendAttemptEv = new RadioSendAttemptEvent(channel, radioSource); RaiseLocalEvent(ref sendAttemptEv); RaiseLocalEvent(radioSource, ref sendAttemptEv); @@ -161,10 +171,29 @@ 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); // Corvax-Languages _messages.Remove(message); } +// Corvax-Languages-Start + private string WrapRadioMessage(EntityUid source, RadioChannelPrototype channel, string name, string message, LanguagePrototype language) + { + // TODO: code duplication with ChatSystem.WrapMessage + var speech = _chat.GetSpeechVerb(source, message); + var languageColor = channel.Color; + if (language.SpeechOverride.Color is { } colorOverride) + languageColor = Color.InterpolateBetween(languageColor, colorOverride, colorOverride.A); + return Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap", + ("color", channel.Color), + ("languageColor", languageColor), + ("fontType", language.SpeechOverride.FontId ?? speech.FontId), + ("fontSize", language.SpeechOverride.FontSize ?? speech.FontSize), + ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), + ("channel", $"\\[{channel.LocalizedName}\\]"), + ("name", name), + ("message", message)); + } +// Corvax-Languages-End /// private bool HasActiveServer(MapId mapId, string channelId) { diff --git a/Content.Server/Radio/RadioEvent.cs b/Content.Server/Radio/RadioEvent.cs index fafa66674e3..bfefc095aa6 100644 --- a/Content.Server/Radio/RadioEvent.cs +++ b/Content.Server/Radio/RadioEvent.cs @@ -1,11 +1,25 @@ using Content.Shared.Chat; +using Content.Shared._EinsteinEngine.Language;// Corvax-Languages 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, EntityUid RadioSource, MsgChatMessage ChatMsg); +public readonly record struct RadioReceiveEvent( + // Corvax-Languages-Start + EntityUid MessageSource, + RadioChannelPrototype Channel, + ChatMessage OriginalChatMsg, + ChatMessage LanguageObfuscatedChatMsg, + LanguagePrototype Language, + EntityUid RadioSource +); +// Corvax-Languages-End /// /// Use this event to cancel sending message per receiver /// diff --git a/Content.Server/RatKing/RatKingSystem.cs b/Content.Server/RatKing/RatKingSystem.cs index 4b82dba3359..7bb618f2bf1 100644 --- a/Content.Server/RatKing/RatKingSystem.cs +++ b/Content.Server/RatKing/RatKingSystem.cs @@ -6,6 +6,7 @@ using Content.Server.NPC.Systems; using Content.Server.Popups; using Content.Shared.Atmos; +using Content.Shared.Chat; // Corvax-Languages using Content.Shared.Dataset; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; @@ -124,7 +125,7 @@ public override void DoCommandCallout(EntityUid uid, RatKingComponent component) return; var msg = Random.Pick(datasetPrototype.Values); - _chat.TrySendInGameICMessage(uid, msg, InGameICChatType.Speak, true); + _chat.TrySendInGameICMessage(uid, msg, SharedChatSystem.InGameICChatType.Speak, true); // Corvax-Languages } } } diff --git a/Content.Server/Speech/EntitySystems/ListeningSystem.cs b/Content.Server/Speech/EntitySystems/ListeningSystem.cs index ea3569e055c..019e44ec77a 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!; // Corvax-Languages [Dependency] private readonly SharedTransformSystem _xforms = default!; public override void Initialize() @@ -18,10 +19,11 @@ public override void Initialize() private void OnSpeak(EntitySpokeEvent ev) { - PingListeners(ev.Source, ev.Message, ev.ObfuscatedMessage); + PingListeners(ev.Source, ev.Message, ev.IsWhisper); // Corvax-Languages + } - public void PingListeners(EntityUid source, string message, string? obfuscatedMessage) + public void PingListeners(EntityUid source, string message, bool isWhisper) // Corvax-Languages { // TODO whispering / audio volume? Microphone sensitivity? // for now, whispering just arbitrarily reduces the listener's max range. @@ -32,7 +34,7 @@ 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); + var obfuscatedEv = !isWhisper ? null : new ListenEvent(_chat.ObfuscateMessageReadability(message), source); // Corvax-Languages var query = EntityQueryEnumerator(); while(query.MoveNext(out var listenerUid, out var listener, out var xform)) diff --git a/Content.Server/Speech/Muting/MutingSystem.cs b/Content.Server/Speech/Muting/MutingSystem.cs index 238d501e249..579166dcea3 100644 --- a/Content.Server/Speech/Muting/MutingSystem.cs +++ b/Content.Server/Speech/Muting/MutingSystem.cs @@ -1,3 +1,4 @@ +using Content.Server._EinsteinEngine.Language; // Corvax-Languages using Content.Server.Abilities.Mime; using Content.Server.Chat.Systems; using Content.Server.Popups; @@ -12,6 +13,7 @@ namespace Content.Server.Speech.Muting { public sealed class MutingSystem : EntitySystem { + [Dependency] private readonly LanguageSystem _languages = default!; // Corvax-Languages [Dependency] private readonly PopupSystem _popupSystem = default!; public override void Initialize() { @@ -47,7 +49,11 @@ private void OnScreamAction(EntityUid uid, MutedComponent component, ScreamActio private void OnSpeakAttempt(EntityUid uid, MutedComponent component, SpeakAttemptEvent args) { - // TODO something better than this. + // Corvax-Languages-Start + var language = _languages.GetLanguage(uid); + if (!language.SpeechOverride.RequireSpeech) + return; // Cannot mute if there's no speech involved + // Corvax-Languages-End if (HasComp(uid)) _popupSystem.PopupEntity(Loc.GetString("mime-cant-speak"), uid, uid); diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs index 0e694a801eb..318643fe48e 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs @@ -51,6 +51,6 @@ private void OnSpeechSent(EntityUid uid, SurveillanceCameraSpeakerComponent comp ("originalName", nameEv.Name)); // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios - _chatSystem.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Speak, ChatTransmitRange.GhostRangeLimit, nameOverride: name); + _chatSystem.TrySendInGameICMessage(uid, args.Message, SharedChatSystem.InGameICChatType.Speak, SharedChatSystem.ChatTransmitRange.GhostRangeLimit, nameOverride: name); // Corvax-Languages } } diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs index 190a2d0263e..5f5374d3c70 100644 --- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs @@ -23,6 +23,7 @@ using Robust.Shared.Random; using System.Linq; using System.Numerics; +using Content.Shared.Chat; // Corvax-Languages namespace Content.Server.Weapons.Melee; @@ -243,7 +244,7 @@ private void OnSpeechHit(EntityUid owner, MeleeSpeechComponent comp, MeleeHitEve if (comp.Battlecry != null)//If the battlecry is set to empty, doesn't speak { - _chat.TrySendInGameICMessage(args.User, comp.Battlecry, InGameICChatType.Speak, true, true, checkRadioPrefix: false); //Speech that isn't sent to chat or adminlogs + _chat.TrySendInGameICMessage(args.User, comp.Battlecry, SharedChatSystem.InGameICChatType.Speak, true, true, checkRadioPrefix: false); //Speech that isn't sent to chat or adminlogs. Corvax-Language } } diff --git a/Content.Server/_EinsteinEngine/Language/Commands/AdminLanguageCommand.cs b/Content.Server/_EinsteinEngine/Language/Commands/AdminLanguageCommand.cs new file mode 100644 index 00000000000..c53aac83003 --- /dev/null +++ b/Content.Server/_EinsteinEngine/Language/Commands/AdminLanguageCommand.cs @@ -0,0 +1,77 @@ +using Content.Server.Administration; +using Content.Shared._EinsteinEngine.Language; +using Content.Shared._EinsteinEngine.Language.Components; +using Content.Shared._EinsteinEngine.Language.Components.Translators; +using Content.Shared._EinsteinEngine.Language.Systems; +using Content.Shared.Administration; +using Robust.Shared.Prototypes; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Content.Server._EinsteinEngine.Language.Commands; + +[ToolshedCommand(Name = "language"), AdminCommand(AdminFlags.Admin)] +public sealed class AdminLanguageCommand : ToolshedCommand +{ + private LanguageSystem? _languagesField; + private LanguageSystem Languages => _languagesField ??= GetSys(); + + [CommandImplementation("add")] + public EntityUid AddLanguage( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef> @ref, + [CommandArgument] bool canSpeak = true, + [CommandArgument] bool canUnderstand = true + ) + { + var language = @ref.Evaluate(ctx)!; + + if (language == SharedLanguageSystem.UniversalPrototype) + { + EnsureComp(input); + Languages.UpdateEntityLanguages(input); + } + else + { + EnsureComp(input); + Languages.AddLanguage(input, language, canSpeak, canUnderstand); + } + + return input; + } + + [CommandImplementation("rm")] + public EntityUid RemoveLanguage( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef> @ref, + [CommandArgument] bool removeSpeak = true, + [CommandArgument] bool removeUnderstand = true + ) + { + var language = @ref.Evaluate(ctx)!; + if (language == SharedLanguageSystem.UniversalPrototype && HasComp(input)) + { + RemComp(input); + EnsureComp(input); + } + // We execute this branch even in case of universal so that it gets removed if it was added manually to the LanguageKnowledge + Languages.RemoveLanguage(input, language, removeSpeak, removeUnderstand); + + return input; + } + + [CommandImplementation("lsspoken")] + public IEnumerable> ListSpoken([PipedArgument] EntityUid input) + { + return Languages.GetSpokenLanguages(input); + } + + [CommandImplementation("lsunderstood")] + public IEnumerable> ListUnderstood([PipedArgument] EntityUid input) + { + return Languages.GetUnderstoodLanguages(input); + } +} diff --git a/Content.Server/_EinsteinEngine/Language/Commands/AdminTranslatorCommand.cs b/Content.Server/_EinsteinEngine/Language/Commands/AdminTranslatorCommand.cs new file mode 100644 index 00000000000..242af0f3f6d --- /dev/null +++ b/Content.Server/_EinsteinEngine/Language/Commands/AdminTranslatorCommand.cs @@ -0,0 +1,156 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Server.Administration; +using Content.Shared._EinsteinEngine.Language; +using Content.Shared._EinsteinEngine.Language.Components; +using Content.Shared._EinsteinEngine.Language.Components.Translators; +using Content.Shared._EinsteinEngine.Language.Systems; +using Content.Shared.Administration; +using Robust.Server.Containers; +using Robust.Shared.Prototypes; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Content.Server._EinsteinEngine.Language.Commands; + +[ToolshedCommand(Name = "translator"), AdminCommand(AdminFlags.Admin)] +public sealed class AdminTranslatorCommand : ToolshedCommand +{ + private LanguageSystem? _languagesField; + private ContainerSystem? _containersField; + + private ContainerSystem Containers => _containersField ??= GetSys(); + private LanguageSystem Languages => _languagesField ??= GetSys(); + + [CommandImplementation("addlang")] + public EntityUid AddLanguage( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef> @ref, + [CommandArgument] bool addSpeak = true, + [CommandArgument] bool addUnderstand = true + ) + { + var language = @ref.Evaluate(ctx)!; + // noob trap - needs a universallanguagespeakercomponent + if (language == SharedLanguageSystem.UniversalPrototype) + throw new ArgumentException(Loc.GetString("command-language-error-this-will-not-work")); + + if (!TryGetTranslatorComp(input, out var translator)) + throw new ArgumentException(Loc.GetString("command-language-error-not-a-translator", ("entity", input))); + + if (addSpeak && !translator.SpokenLanguages.Contains(language)) + translator.SpokenLanguages.Add(language); + if (addUnderstand && !translator.UnderstoodLanguages.Contains(language)) + translator.UnderstoodLanguages.Add(language); + + UpdateTranslatorHolder(input); + + return input; + } + + [CommandImplementation("rmlang")] + public EntityUid RemoveLanguage( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef> @ref, + [CommandArgument] bool removeSpeak = true, + [CommandArgument] bool removeUnderstand = true + ) + { + var language = @ref.Evaluate(ctx)!; + if (!TryGetTranslatorComp(input, out var translator)) + throw new ArgumentException(Loc.GetString("command-language-error-not-a-translator", ("entity", input))); + + if (removeSpeak) + translator.SpokenLanguages.Remove(language); + if (removeUnderstand) + translator.UnderstoodLanguages.Remove(language); + + UpdateTranslatorHolder(input); + + return input; + } + + [CommandImplementation("addrequired")] + public EntityUid AddRequiredLanguage( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef> @ref) + { + var language = @ref.Evaluate(ctx)!; + if (!TryGetTranslatorComp(input, out var translator)) + throw new ArgumentException(Loc.GetString("command-language-error-not-a-translator", ("entity", input))); + + if (!translator.RequiredLanguages.Contains(language)) + { + translator.RequiredLanguages.Add(language); + UpdateTranslatorHolder(input); + } + + return input; + } + + [CommandImplementation("rmrequired")] + public EntityUid RemoveRequiredLanguage( + [CommandInvocationContext] IInvocationContext ctx, + [PipedArgument] EntityUid input, + [CommandArgument] ValueRef> @ref) + { + var language = @ref.Evaluate(ctx)!; + if (!TryGetTranslatorComp(input, out var translator)) + throw new ArgumentException(Loc.GetString("command-language-error-not-a-translator", ("entity", input))); + + if (translator.RequiredLanguages.Remove(language)) + UpdateTranslatorHolder(input); + + return input; + } + + [CommandImplementation("lsspoken")] + public IEnumerable> ListSpoken([PipedArgument] EntityUid input) + { + if (!TryGetTranslatorComp(input, out var translator)) + return []; + return translator.SpokenLanguages; + } + + [CommandImplementation("lsunderstood")] + public IEnumerable> ListUnderstood([PipedArgument] EntityUid input) + { + if (!TryGetTranslatorComp(input, out var translator)) + return []; + return translator.UnderstoodLanguages; + } + + [CommandImplementation("lsrequired")] + public IEnumerable> ListRequired([PipedArgument] EntityUid input) + { + if (!TryGetTranslatorComp(input, out var translator)) + return []; + return translator.RequiredLanguages; + } + + private bool TryGetTranslatorComp(EntityUid uid, [NotNullWhen(true)] out BaseTranslatorComponent? translator) + { + if (TryComp(uid, out var handheld)) + translator = handheld; + else if (TryComp(uid, out var implant)) + translator = implant; + else if (TryComp(uid, out var intrinsic)) + translator = intrinsic; + else + translator = null; + + return translator != null; + } + + private void UpdateTranslatorHolder(EntityUid translator) + { + if (!Containers.TryGetContainingContainer(translator, out var cont) + || cont.Owner is not { Valid: true } holder) + return; + + Languages.UpdateEntityLanguages(holder); + } +} diff --git a/Content.Server/_EinsteinEngine/Language/Commands/ListLanguagesCommand.cs b/Content.Server/_EinsteinEngine/Language/Commands/ListLanguagesCommand.cs new file mode 100644 index 00000000000..f70b32e4755 --- /dev/null +++ b/Content.Server/_EinsteinEngine/Language/Commands/ListLanguagesCommand.cs @@ -0,0 +1,58 @@ +using System.Linq; +using Content.Shared.Administration; +using Robust.Shared.Console; +using Robust.Shared.Enums; + +namespace Content.Server._EinsteinEngine.Language.Commands; + +[AnyCommand] +public sealed class ListLanguagesCommand : IConsoleCommand +{ + public string Command => "languagelist"; + public string Description => Loc.GetString("command-list-langs-desc"); + public string Help => Loc.GetString("command-list-langs-help", ("command", Command)); + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not { } player) + { + shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server")); + return; + } + + if (player.Status != SessionStatus.InGame) + return; + + if (player.AttachedEntity is not {} playerEntity) + { + shell.WriteError(Loc.GetString("shell-must-be-attached-to-entity")); + return; + } + + var languages = IoCManager.Resolve().GetEntitySystem(); + var currentLang = languages.GetLanguage(playerEntity).ID; + + shell.WriteLine(Loc.GetString("command-language-spoken")); + var spoken = languages.GetSpokenLanguages(playerEntity); + for (int i = 0; i < spoken.Count; i++) + { + var lang = spoken[i]; + shell.WriteLine(lang == currentLang + ? Loc.GetString("command-language-current-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang))) + : Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang)))); + } + + shell.WriteLine(Loc.GetString("command-language-understood")); + var understood = languages.GetUnderstoodLanguages(playerEntity); + for (int i = 0; i < understood.Count; i++) + { + var lang = understood[i]; + shell.WriteLine(Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang)))); + } + } + + private string LanguageName(string id) + { + return Loc.GetString($"language-{id}-name"); + } +} diff --git a/Content.Server/_EinsteinEngine/Language/Commands/SayLanguageCommand.cs b/Content.Server/_EinsteinEngine/Language/Commands/SayLanguageCommand.cs new file mode 100644 index 00000000000..6cdabec5b9e --- /dev/null +++ b/Content.Server/_EinsteinEngine/Language/Commands/SayLanguageCommand.cs @@ -0,0 +1,52 @@ +using Content.Server.Chat.Systems; +using Content.Shared.Administration; +using Content.Shared.Chat; +using Robust.Shared.Console; +using Robust.Shared.Enums; + +namespace Content.Server._EinsteinEngine.Language.Commands; + +[AnyCommand] +public sealed class SayLanguageCommand : IConsoleCommand +{ + public string Command => "saylang"; + public string Description => Loc.GetString("command-saylang-desc"); + public string Help => Loc.GetString("command-saylang-help", ("command", Command)); + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not { } player) + { + shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server")); + return; + } + + if (player.Status != SessionStatus.InGame) + return; + + if (player.AttachedEntity is not {} playerEntity) + { + shell.WriteError(Loc.GetString("shell-must-be-attached-to-entity")); + return; + } + + if (args.Length < 2) + return; + + 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(); + + if (!SelectLanguageCommand.TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var language)) + { + shell.WriteError(failReason); + return; + } + + chats.TrySendInGameICMessage(playerEntity, message, SharedChatSystem.InGameICChatType.Speak, SharedChatSystem.ChatTransmitRange.Normal, false, shell, player, languageOverride: language); + } +} diff --git a/Content.Server/_EinsteinEngine/Language/Commands/SelectLanguageCommand.cs b/Content.Server/_EinsteinEngine/Language/Commands/SelectLanguageCommand.cs new file mode 100644 index 00000000000..d6fe399d6e1 --- /dev/null +++ b/Content.Server/_EinsteinEngine/Language/Commands/SelectLanguageCommand.cs @@ -0,0 +1,90 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared._EinsteinEngine.Language; +using Content.Shared.Administration; +using Robust.Shared.Console; +using Robust.Shared.Enums; + +namespace Content.Server._EinsteinEngine.Language.Commands; + +[AnyCommand] +public sealed class SelectLanguageCommand : IConsoleCommand +{ + public string Command => "languageselect"; + public string Description => Loc.GetString("command-language-select-desc"); + public string Help => Loc.GetString("command-language-select-help", ("command", Command)); + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not { } player) + { + shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server")); + return; + } + + if (player.Status != SessionStatus.InGame) + return; + + if (player.AttachedEntity is not { } playerEntity) + { + shell.WriteError(Loc.GetString("shell-must-be-attached-to-entity")); + return; + } + + if (args.Length < 1) + return; + + var languageId = args[0]; + + var languages = IoCManager.Resolve().GetEntitySystem(); + + if (!TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var language)) + { + shell.WriteError(failReason); + return; + } + + languages.SetLanguage(playerEntity, language.ID); + } + + + // TODO: find a better place for this method + /// + /// Tries to parse the input argument as either a language ID or the position of the language in the list of languages + /// the entity can speak. Returns true if sucessful. + /// + public static bool TryParseLanguageArgument( + LanguageSystem languageSystem, + EntityUid speaker, + string input, + [NotNullWhen(false)] out string? failureReason, + [NotNullWhen(true)] out LanguagePrototype? language) + { + failureReason = null; + language = null; + + if (int.TryParse(input, out var num)) + { + // The argument is a number + var spoken = languageSystem.GetSpokenLanguages(speaker); + if (num > 0 && num - 1 < spoken.Count) + language = languageSystem.GetLanguagePrototype(spoken[num - 1]); + + if (language != null) // the ability to speak it is implied + return true; + + failureReason = Loc.GetString("command-language-invalid-number", ("total", spoken.Count)); + return false; + } + else + { + // The argument is a language ID + language = languageSystem.GetLanguagePrototype(input); + + if (language != null && languageSystem.CanSpeak(speaker, language.ID)) + return true; + + failureReason = Loc.GetString("command-language-invalid-language", ("id", input)); + return false; + } + } +} diff --git a/Content.Server/_EinsteinEngine/Language/LanguageKnowledgeComponent.cs b/Content.Server/_EinsteinEngine/Language/LanguageKnowledgeComponent.cs new file mode 100644 index 00000000000..4c0a7af92d6 --- /dev/null +++ b/Content.Server/_EinsteinEngine/Language/LanguageKnowledgeComponent.cs @@ -0,0 +1,23 @@ +using Content.Shared._EinsteinEngine.Language; +using Robust.Shared.Prototypes; + +namespace Content.Server._EinsteinEngine.Language; + +/// +/// Stores data about entities' intrinsic language knowledge. +/// +[RegisterComponent] +public sealed partial class LanguageKnowledgeComponent : Component +{ + /// + /// List of languages this entity can speak without any external tools. + /// + [DataField("speaks", required: true)] + public List> SpokenLanguages = new(); + + /// + /// List of languages this entity can understand without any external tools. + /// + [DataField("understands", required: true)] + public List> UnderstoodLanguages = new(); +} diff --git a/Content.Server/_EinsteinEngine/Language/LanguageSystem.cs b/Content.Server/_EinsteinEngine/Language/LanguageSystem.cs new file mode 100644 index 00000000000..c5dd1e65cc1 --- /dev/null +++ b/Content.Server/_EinsteinEngine/Language/LanguageSystem.cs @@ -0,0 +1,233 @@ +using System.Linq; +using Content.Shared._EinsteinEngine.Language; +using Content.Shared._EinsteinEngine.Language.Components; +using Content.Shared._EinsteinEngine.Language.Events; +using Content.Shared._EinsteinEngine.Language.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Server._EinsteinEngine.Language; + +public sealed partial class LanguageSystem : SharedLanguageSystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInitLanguageKnowledge); + SubscribeLocalEvent(OnInitLanguageSpeaker); + SubscribeLocalEvent(OnGetLanguageState); + SubscribeLocalEvent(OnDetermineUniversalLanguages); + SubscribeNetworkEvent(OnClientSetLanguage); + + SubscribeLocalEvent((uid, _, _) => UpdateEntityLanguages(uid)); + SubscribeLocalEvent((uid, _, _) => UpdateEntityLanguages(uid)); + } + + #region event handling + private void OnInitLanguageKnowledge(Entity ent, ref MapInitEvent args) + { + EnsureComp(ent.Owner); + } + + private void OnInitLanguageSpeaker(Entity ent, ref MapInitEvent args) + { + if (string.IsNullOrEmpty(ent.Comp.CurrentLanguage)) + ent.Comp.CurrentLanguage = ent.Comp.SpokenLanguages.FirstOrDefault(UniversalPrototype); + + UpdateEntityLanguages(ent!); + } + + private void OnGetLanguageState(Entity entity, ref ComponentGetState args) + { + args.State = new LanguageSpeakerComponent.State + { + CurrentLanguage = entity.Comp.CurrentLanguage, + SpokenLanguages = entity.Comp.SpokenLanguages, + UnderstoodLanguages = entity.Comp.UnderstoodLanguages + }; + } + + private void OnDetermineUniversalLanguages(Entity entity, ref DetermineEntityLanguagesEvent ev) + { + // We only add it as a spoken language; CanUnderstand checks for ULSC itself. + if (entity.Comp.Enabled) + ev.SpokenLanguages.Add(UniversalPrototype); + } + + + private void OnClientSetLanguage(LanguagesSetMessage message, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity is not { Valid: true } uid) + return; + + var language = GetLanguagePrototype(message.CurrentLanguage); + if (language == null || !CanSpeak(uid, language.ID)) + return; + + SetLanguage(uid, language.ID); + } + + #endregion + + #region public api + + public bool CanUnderstand(Entity ent, ProtoId language) + { + if (language == UniversalPrototype || TryComp(ent, out var uni) && uni.Enabled) + return true; + + return Resolve(ent, ref ent.Comp, logMissing: false) && ent.Comp.UnderstoodLanguages.Contains(language); + } + + public bool CanSpeak(Entity ent, ProtoId language) + { + if (!Resolve(ent, ref ent.Comp, logMissing: false)) + return false; + + return ent.Comp.SpokenLanguages.Contains(language); + } + + /// + /// Returns the current language of the given entity, assumes Universal if it's not a language speaker. + /// + public LanguagePrototype GetLanguage(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, logMissing: false) + || string.IsNullOrEmpty(ent.Comp.CurrentLanguage) + || !_prototype.TryIndex(ent.Comp.CurrentLanguage, out var proto) + ) + return Universal; + + return proto; + } + + /// + /// Returns the list of languages this entity can speak. + /// + /// This simply returns the value of . + public List> GetSpokenLanguages(EntityUid uid) + { + return TryComp(uid, out var component) ? component.SpokenLanguages : []; + } + + /// + /// Returns the list of languages this entity can understand. + /// This simply returns the value of . + public List> GetUnderstoodLanguages(EntityUid uid) + { + return TryComp(uid, out var component) ? component.UnderstoodLanguages : []; + } + + public void SetLanguage(Entity ent, ProtoId language) + { + if (!CanSpeak(ent, language) + || !Resolve(ent, ref ent.Comp) + || ent.Comp.CurrentLanguage == language) + return; + + ent.Comp.CurrentLanguage = language; + RaiseLocalEvent(ent, new LanguagesUpdateEvent(), true); + Dirty(ent); + } + + /// + /// Adds a new language to the respective lists of intrinsically known languages of the given entity. + /// + public void AddLanguage( + EntityUid uid, + ProtoId language, + bool addSpoken = true, + bool addUnderstood = true) + { + EnsureComp(uid, out var knowledge); + EnsureComp(uid, out var speaker); + + if (addSpoken && !knowledge.SpokenLanguages.Contains(language)) + knowledge.SpokenLanguages.Add(language); + + if (addUnderstood && !knowledge.UnderstoodLanguages.Contains(language)) + knowledge.UnderstoodLanguages.Add(language); + + UpdateEntityLanguages((uid, speaker)); + } + + /// + /// Removes a language from the respective lists of intrinsically known languages of the given entity. + /// + public void RemoveLanguage( + Entity ent, + ProtoId language, + bool removeSpoken = true, + bool removeUnderstood = true) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + if (removeSpoken) + ent.Comp.SpokenLanguages.Remove(language); + + if (removeUnderstood) + ent.Comp.UnderstoodLanguages.Remove(language); + + // We don't ensure that the entity has a speaker comp. If it doesn't... Well, woe be the caller of this method. + UpdateEntityLanguages(ent.Owner); + } + + /// + /// Ensures the given entity has a valid language as its current language. + /// If not, sets it to the first entry of its SpokenLanguages list, or universal if it's empty. + /// + /// True if the current language was modified, false otherwise. + public bool EnsureValidLanguage(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + if (!ent.Comp.SpokenLanguages.Contains(ent.Comp.CurrentLanguage)) + { + ent.Comp.CurrentLanguage = ent.Comp.SpokenLanguages.FirstOrDefault(UniversalPrototype); + RaiseLocalEvent(ent, new LanguagesUpdateEvent()); + Dirty(ent); + return true; + } + + return false; + } + + /// + /// Immediately refreshes the cached lists of spoken and understood languages for the given entity. + /// + public void UpdateEntityLanguages(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + var ev = new DetermineEntityLanguagesEvent(); + // We add the intrinsically known languages first so other systems can manipulate them easily + if (TryComp(ent, out var knowledge)) + { + foreach (var spoken in knowledge.SpokenLanguages) + ev.SpokenLanguages.Add(spoken); + + foreach (var understood in knowledge.UnderstoodLanguages) + ev.UnderstoodLanguages.Add(understood); + } + + RaiseLocalEvent(ent, ref ev); + + ent.Comp.SpokenLanguages.Clear(); + ent.Comp.UnderstoodLanguages.Clear(); + + ent.Comp.SpokenLanguages.AddRange(ev.SpokenLanguages); + ent.Comp.UnderstoodLanguages.AddRange(ev.UnderstoodLanguages); + + // If EnsureValidLanguage returns true, it also raises a LanguagesUpdateEvent, so we try to avoid raising it twice in that case. + if (!EnsureValidLanguage(ent)) + RaiseLocalEvent(ent, new LanguagesUpdateEvent()); + + Dirty(ent); + } + + #endregion +} diff --git a/Content.Server/_EinsteinEngine/Language/TranslatorImplantSystem.cs b/Content.Server/_EinsteinEngine/Language/TranslatorImplantSystem.cs new file mode 100644 index 00000000000..64e33a302e3 --- /dev/null +++ b/Content.Server/_EinsteinEngine/Language/TranslatorImplantSystem.cs @@ -0,0 +1,66 @@ +using Content.Shared._EinsteinEngine.Language.Components; +using Content.Shared._EinsteinEngine.Language.Events; +using Content.Shared.Implants.Components; +using Robust.Shared.Containers; + +namespace Content.Server._EinsteinEngine.Language; + +public sealed class TranslatorImplantSystem : EntitySystem +{ + [Dependency] private readonly LanguageSystem _language = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnImplant); + SubscribeLocalEvent(OnDeImplant); + SubscribeLocalEvent(OnDetermineLanguages); + } + + private void OnImplant(EntityUid uid, TranslatorImplantComponent component, EntGotInsertedIntoContainerMessage args) + { + if (args.Container.ID != ImplanterComponent.ImplantSlotId) + return; + + var implantee = Transform(uid).ParentUid; + if (implantee is not { Valid: true } || !TryComp(implantee, out var knowledge)) + return; + + component.Enabled = true; + // To operate an implant, you need to know its required language intrinsically, because like... it connects to your brain or something. + // So external translators or other implants can't help you operate it. + component.SpokenRequirementSatisfied = TranslatorSystem.CheckLanguagesMatch( + component.RequiredLanguages, knowledge.SpokenLanguages, component.RequiresAllLanguages); + + component.UnderstoodRequirementSatisfied = TranslatorSystem.CheckLanguagesMatch( + component.RequiredLanguages, knowledge.UnderstoodLanguages, component.RequiresAllLanguages); + + _language.UpdateEntityLanguages(implantee); + } + + private void OnDeImplant(EntityUid uid, TranslatorImplantComponent component, EntGotRemovedFromContainerMessage args) + { + // Even though the description of this event says it gets raised BEFORE reparenting, that's actually false... + component.Enabled = component.SpokenRequirementSatisfied = component.UnderstoodRequirementSatisfied = false; + + if (TryComp(uid, out var subdermal) && subdermal.ImplantedEntity is { Valid: true} implantee) + _language.UpdateEntityLanguages(implantee); + } + + private void OnDetermineLanguages(EntityUid uid, ImplantedComponent component, ref DetermineEntityLanguagesEvent args) + { + // TODO: might wanna find a better solution, i just can't come up with something viable + foreach (var implant in component.ImplantContainer.ContainedEntities) + { + if (!TryComp(implant, out var translator) || !translator.Enabled) + continue; + + if (translator.SpokenRequirementSatisfied) + foreach (var language in translator.SpokenLanguages) + args.SpokenLanguages.Add(language); + + if (translator.UnderstoodRequirementSatisfied) + foreach (var language in translator.UnderstoodLanguages) + args.UnderstoodLanguages.Add(language); + } + } +} diff --git a/Content.Server/_EinsteinEngine/Language/TranslatorSystem.cs b/Content.Server/_EinsteinEngine/Language/TranslatorSystem.cs new file mode 100644 index 00000000000..415b8887d3d --- /dev/null +++ b/Content.Server/_EinsteinEngine/Language/TranslatorSystem.cs @@ -0,0 +1,165 @@ +using System.Linq; +using Content.Server.Popups; +using Content.Server.PowerCell; +using Content.Shared._EinsteinEngine.Language; +using Content.Shared._EinsteinEngine.Language.Components; +using Content.Shared._EinsteinEngine.Language.Components.Translators; +using Content.Shared._EinsteinEngine.Language.Events; +using Content.Shared._EinsteinEngine.Language.Systems; +using Content.Shared.Interaction; +using Content.Shared.PowerCell; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server._EinsteinEngine.Language; + +public sealed class TranslatorSystem : SharedTranslatorSystem +{ + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly LanguageSystem _language = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnDetermineLanguages); + SubscribeLocalEvent(OnProxyDetermineLanguages); + + SubscribeLocalEvent(OnTranslatorInserted); + SubscribeLocalEvent(OnTranslatorParentChanged); + SubscribeLocalEvent(OnTranslatorToggle); + SubscribeLocalEvent(OnPowerCellSlotEmpty); + } + + private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent component, DetermineEntityLanguagesEvent ev) + { + if (!component.Enabled + || component.LifeStage >= ComponentLifeStage.Removing + || !TryComp(uid, out var knowledge) + || !_powerCell.HasActivatableCharge(uid)) + return; + + CopyLanguages(component, ev, knowledge); + } + + private void OnProxyDetermineLanguages(EntityUid uid, HoldsTranslatorComponent component, DetermineEntityLanguagesEvent ev) + { + if (!TryComp(uid, out var knowledge)) + return; + + foreach (var (translator, translatorComp) in component.Translators.ToArray()) + { + if (!translatorComp.Enabled || !_powerCell.HasActivatableCharge(uid)) + continue; + + if (!_containers.TryGetContainingContainer(translator, out var container) || container.Owner != uid) + { + component.Translators.RemoveWhere(it => it.Owner == translator); + continue; + } + + CopyLanguages(translatorComp, ev, knowledge); + } + } + + private void OnTranslatorInserted(EntityUid translator, HandheldTranslatorComponent component, EntGotInsertedIntoContainerMessage args) + { + if (args.Container.Owner is not {Valid: true} holder || !HasComp(holder)) + return; + + var intrinsic = EnsureComp(holder); + intrinsic.Translators.Add((translator, component)); + + _language.UpdateEntityLanguages(holder); + } + + private void OnTranslatorParentChanged(EntityUid translator, HandheldTranslatorComponent component, EntParentChangedMessage args) + { + if (!HasComp(args.OldParent)) + return; + + // Update the translator on the next tick - this is necessary because there's a good chance the removal from a container + // Was caused by the player moving the translator within their inventory rather than removing it. + // If that is not the case, then OnProxyDetermineLanguages will remove this translator from HoldsTranslatorComponent.Translators. + Timer.Spawn(0, () => + { + if (Exists(args.OldParent) && HasComp(args.OldParent)) + _language.UpdateEntityLanguages(args.OldParent.Value); + }); + } + + private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponent translatorComp, ActivateInWorldEvent args) + { + if (!translatorComp.ToggleOnInteract) + return; + + // This will show a popup if false + var hasPower = _powerCell.HasDrawCharge(translator); + var isEnabled = !translatorComp.Enabled && hasPower; + + translatorComp.Enabled = isEnabled; + _powerCell.SetDrawEnabled(translator, isEnabled); + + if (_containers.TryGetContainingContainer(translator, out var holderCont) + && holderCont.Owner is var holder + && TryComp(holder, out var languageComp)) + { + // The first new spoken language added by this translator, or null + var firstNewLanguage = translatorComp.SpokenLanguages.FirstOrDefault(it => !languageComp.SpokenLanguages.Contains(it)); + _language.UpdateEntityLanguages(holder); + + // Update the current language of the entity if necessary + if (isEnabled && translatorComp.SetLanguageOnInteract && firstNewLanguage is {}) + _language.SetLanguage((holder, languageComp), firstNewLanguage); + } + + OnAppearanceChange(translator, translatorComp); + + if (hasPower) + { + var loc = isEnabled ? "translator-component-turnon" : "translator-component-shutoff"; + var message = Loc.GetString(loc, ("translator", translator)); + _popup.PopupEntity(message, translator, args.User); + } + } + + private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorComponent component, PowerCellSlotEmptyEvent args) + { + component.Enabled = false; + _powerCell.SetDrawEnabled(translator, false); + OnAppearanceChange(translator, component); + + if (_containers.TryGetContainingContainer(translator, out var holderCont) && HasComp(holderCont.Owner)) + _language.UpdateEntityLanguages(holderCont.Owner); + } + + private void CopyLanguages(BaseTranslatorComponent from, DetermineEntityLanguagesEvent to, LanguageKnowledgeComponent knowledge) + { + var addSpoken = CheckLanguagesMatch(from.RequiredLanguages, knowledge.SpokenLanguages, from.RequiresAllLanguages); + var addUnderstood = CheckLanguagesMatch(from.RequiredLanguages, knowledge.UnderstoodLanguages, from.RequiresAllLanguages); + + if (addSpoken) + foreach (var language in from.SpokenLanguages) + to.SpokenLanguages.Add(language); + + if (addUnderstood) + foreach (var language in from.UnderstoodLanguages) + to.UnderstoodLanguages.Add(language); + } + + /// + /// Checks whether any OR all required languages are provided. Used for utility purposes. + /// + public static bool CheckLanguagesMatch(ICollection> required, ICollection> provided, bool requireAll) + { + if (required.Count == 0) + return true; + + return requireAll + ? required.All(provided.Contains) + : required.Any(provided.Contains); + } +} diff --git a/Content.Server/_EinsteinEngine/Language/UniversalLanguageSpeakerComponent.cs b/Content.Server/_EinsteinEngine/Language/UniversalLanguageSpeakerComponent.cs new file mode 100644 index 00000000000..3159d901910 --- /dev/null +++ b/Content.Server/_EinsteinEngine/Language/UniversalLanguageSpeakerComponent.cs @@ -0,0 +1,12 @@ +namespace Content.Server._EinsteinEngine.Language; + +// +// Signifies that this entity can speak and understand any language. +// Applies to such entities as ghosts. +// +[RegisterComponent] +public sealed partial class UniversalLanguageSpeakerComponent : Component +{ + [DataField] + public bool Enabled = true; +} diff --git a/Content.Server/_EinsteinEngine/Traits/Assorted/ForeingerTraitComponent.cs b/Content.Server/_EinsteinEngine/Traits/Assorted/ForeingerTraitComponent.cs new file mode 100644 index 00000000000..5cee2894d1c --- /dev/null +++ b/Content.Server/_EinsteinEngine/Traits/Assorted/ForeingerTraitComponent.cs @@ -0,0 +1,37 @@ +using Content.Shared._EinsteinEngine.Language; +using Content.Shared._EinsteinEngine.Language.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Server._EinsteinEngine.Traits.Assorted; + +/// +/// When applied to a not-yet-spawned player entity, removes from the lists of their languages +/// and gives them a translator instead. +/// +[RegisterComponent] +public sealed partial class ForeignerTraitComponent : Component +{ + /// + /// The "base" language that is to be removed and substituted with a translator. + /// By default, equals to the fallback language, which is GalacticCommon. + /// + [DataField] + public ProtoId BaseLanguage = SharedLanguageSystem.FallbackLanguagePrototype; + + /// + /// Whether this trait prevents the entity from understanding the base language. + /// + public bool CantUnderstand = true; + + /// + /// Whether this trait prevents the entity from speaking the base language. + /// + public bool CantSpeak = true; + + /// + /// The base translator prototype to use when creating a translator for the entity. + /// + [DataField(required: true)] + public EntProtoId BaseTranslator = default!; + +} diff --git a/Content.Server/_EinsteinEngine/Traits/Assorted/ForeingerTraitSystem.cs b/Content.Server/_EinsteinEngine/Traits/Assorted/ForeingerTraitSystem.cs new file mode 100644 index 00000000000..aea95a1fc58 --- /dev/null +++ b/Content.Server/_EinsteinEngine/Traits/Assorted/ForeingerTraitSystem.cs @@ -0,0 +1,105 @@ +using System.Linq; +using Content.Server._EinsteinEngine.Language; +using Content.Server.Hands.Systems; +using Content.Server.Storage.EntitySystems; +using Content.Shared._EinsteinEngine.Language; +using Content.Shared._EinsteinEngine.Language.Components; +using Content.Shared._EinsteinEngine.Language.Components.Translators; +using Content.Shared.Clothing.Components; +using Content.Shared.Inventory; +using Content.Shared.Storage; +using Robust.Shared.Prototypes; + +namespace Content.Server._EinsteinEngine.Traits.Assorted; + + +public sealed partial class ForeignerTraitSystem : EntitySystem +{ + [Dependency] private readonly EntityManager _entMan = default!; + [Dependency] private readonly HandsSystem _hands = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly LanguageSystem _languages = default!; + [Dependency] private readonly StorageSystem _storage = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnSpawn); // TraitSystem adds it after PlayerSpawnCompleteEvent so it's fine + } + + private void OnSpawn(Entity entity, ref ComponentInit args) + { + if (entity.Comp.CantUnderstand && !entity.Comp.CantSpeak) + Log.Warning($"Allowing entity {entity.Owner} to speak a language but not understand it leads to undefined behavior."); + + if (!TryComp(entity, out var knowledge)) + { + Log.Warning($"Entity {entity.Owner} does not have a LanguageKnowledge but has a ForeignerTrait!"); + return; + } + + var alternateLanguage = knowledge.SpokenLanguages.Find(it => it != entity.Comp.BaseLanguage); + if (alternateLanguage == default) + { + Log.Warning($"Entity {entity.Owner} does not have an alternative language to choose from (must have at least one non-GC for ForeignerTrait)!"); + return; + } + + if (TryGiveTranslator(entity.Owner, entity.Comp.BaseTranslator, entity.Comp.BaseLanguage, alternateLanguage, out var translator)) + { + _languages.RemoveLanguage(entity.Owner, entity.Comp.BaseLanguage, entity.Comp.CantSpeak, entity.Comp.CantUnderstand); + } + } + + /// + /// Tries to create and give the entity a translator to translator that translates speech between the two specified languages. + /// + public bool TryGiveTranslator( + EntityUid uid, + string baseTranslatorPrototype, + ProtoId translatorLanguage, + ProtoId entityLanguage, + out EntityUid result) + { + result = EntityUid.Invalid; + if (translatorLanguage == entityLanguage) + return false; + + var translator = _entMan.SpawnNextToOrDrop(baseTranslatorPrototype, uid); + result = translator; + + if (!TryComp(translator, out var handheld)) + { + handheld = AddComp(translator); + handheld.ToggleOnInteract = true; + handheld.SetLanguageOnInteract = true; + } + + // Allows to speak the specified language and requires entities language. + handheld.SpokenLanguages = [translatorLanguage]; + handheld.UnderstoodLanguages = [translatorLanguage]; + handheld.RequiredLanguages = [entityLanguage]; + + // Try to put it in entities hand + if (_hands.TryPickupAnyHand(uid, translator, false, false, false)) + return true; + + // Try to find a valid clothing slot on the entity and equip the translator there + if (TryComp(translator, out var clothing) + && clothing.Slots != SlotFlags.NONE + && _inventory.TryGetSlots(uid, out var slots) + && slots.Any(it => _inventory.TryEquip(uid, translator, it.Name, true, false))) + return true; + + // Try to put the translator into entities bag, if it has one + if (_inventory.TryGetSlotEntity(uid, "back", out var bag) + && TryComp(bag, out var storage) + && _storage.Insert(bag.Value, translator, out _, null, storage, false, false)) + return true; + + // If all of the above has failed, just drop it at the same location as the entity + // This should ideally never happen, but who knows. + Transform(translator).Coordinates = Transform(uid).Coordinates; + + return true; + } +} diff --git a/Content.Server/_EinsteinEngine/Traits/Assorted/LanguageKnowledgeModiferSystem.cs b/Content.Server/_EinsteinEngine/Traits/Assorted/LanguageKnowledgeModiferSystem.cs new file mode 100644 index 00000000000..88151808d6b --- /dev/null +++ b/Content.Server/_EinsteinEngine/Traits/Assorted/LanguageKnowledgeModiferSystem.cs @@ -0,0 +1,33 @@ +using Content.Server._EinsteinEngine.Language; + +namespace Content.Server._EinsteinEngine.Traits.Assorted; + +public sealed class LanguageKnowledgeModifierSystem : EntitySystem +{ + [Dependency] private readonly LanguageSystem _languages = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnStartup); + } + + private void OnStartup(Entity entity, ref ComponentInit args) + { + if (!TryComp(entity, out var knowledge)) + { + Log.Warning($"Entity {entity.Owner} does not have a LanguageKnowledge but has a LanguageKnowledgeModifier!"); + return; + } + + foreach (var spokenLanguage in entity.Comp.NewSpokenLanguages) + { + _languages.AddLanguage(entity, spokenLanguage, true, false); + } + + foreach (var understoodLanguage in entity.Comp.NewUnderstoodLanguages) + { + _languages.AddLanguage(entity, understoodLanguage, false, true); + } + } +} diff --git a/Content.Server/_EinsteinEngine/Traits/Assorted/LanguageKnowledgeModifierComponent.cs b/Content.Server/_EinsteinEngine/Traits/Assorted/LanguageKnowledgeModifierComponent.cs new file mode 100644 index 00000000000..12c3a125a65 --- /dev/null +++ b/Content.Server/_EinsteinEngine/Traits/Assorted/LanguageKnowledgeModifierComponent.cs @@ -0,0 +1,20 @@ +namespace Content.Server._EinsteinEngine.Traits.Assorted; + +/// +/// Used for traits that modify entities' language knowledge. +/// +[RegisterComponent] +public sealed partial class LanguageKnowledgeModifierComponent : Component +{ + /// + /// List of languages this entity will learn to speak. + /// + [DataField("speaks")] + public List NewSpokenLanguages = new(); + + /// + /// List of languages this entity will learn to understand. + /// + [DataField("understands")] + public List NewUnderstoodLanguages = new(); +} diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs index b36bc043a0a..8cd276217ae 100644 --- a/Content.Shared/Chat/SharedChatSystem.cs +++ b/Content.Shared/Chat/SharedChatSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Radio; using Content.Shared.Speech; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; // Corvax-Languages using Robust.Shared.Utility; namespace Content.Shared.Chat; @@ -265,4 +266,45 @@ public static string GetStringInsideTag(ChatMessage message, string tag) tagStart += tag.Length + 2; return rawmsg.Substring(tagStart, tagEnd - tagStart); } + +// Corvax-Languages-Start + /// + /// InGame IC chat is for chat that is specifically ingame (not lobby) but is also in character, i.e. speaking. + /// +// ReSharper disable once InconsistentNaming + [Serializable, NetSerializable] + public enum InGameICChatType : byte + { + Speak, + Emote, + Whisper, + Telepathic + } + + /// + /// InGame OOC chat is for chat that is specifically ingame (not lobby) but is OOC, like deadchat or LOOC. + /// + [Serializable, NetSerializable] + public enum InGameOOCChatType : byte + { + Looc, + Dead + } + + /// + /// Controls transmission of chat. + /// + [Serializable, NetSerializable] + public enum ChatTransmitRange : byte + { + /// Acts normal, ghosts can hear across the map, etc. + Normal, + /// Normal but ghosts are still range-limited. + GhostRangeLimit, + /// Hidden from the chat window. + HideChat, + /// Ghosts can't hear or see it at all. Regular players can if in-range. + NoGhosts + } + // Corvax-Languages-End } diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index 863d9da970f..33953b8e6da 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"; // Corvax-Languages public static readonly BoundKeyFunction OpenEmotesMenu = "OpenEmotesMenu"; public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu"; public static readonly BoundKeyFunction OpenGuidebook = "OpenGuidebook"; diff --git a/Content.Shared/_EinsteinEngine/Language/Components/LanguageSpeakerComponent.cs b/Content.Shared/_EinsteinEngine/Language/Components/LanguageSpeakerComponent.cs new file mode 100644 index 00000000000..d08a65083c8 --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/Components/LanguageSpeakerComponent.cs @@ -0,0 +1,45 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._EinsteinEngine.Language.Components; + +/// +/// Stores the current state of the languages the entity can speak and understand. +/// +/// +/// All fields of this component are populated during a DetermineEntityLanguagesEvent. +/// They are not to be modified externally. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class LanguageSpeakerComponent : Component +{ + public override bool SendOnlyToOwner => true; + + /// + /// The current language the entity uses when speaking. + /// Other listeners will hear the entity speak in this language. + /// + [DataField] + public string CurrentLanguage = ""; // The language system will override it on mapinit + + /// + /// List of languages this entity can speak at the current moment. + /// + [DataField] + public List> SpokenLanguages = []; + + /// + /// List of languages this entity can understand at the current moment. + /// + [DataField] + public List> UnderstoodLanguages = []; + + [Serializable, NetSerializable] + public sealed class State : ComponentState + { + public string CurrentLanguage = default!; + public List> SpokenLanguages = default!; + public List> UnderstoodLanguages = default!; + } +} diff --git a/Content.Shared/_EinsteinEngine/Language/Components/TranslatorImplantComponent.cs b/Content.Shared/_EinsteinEngine/Language/Components/TranslatorImplantComponent.cs new file mode 100644 index 00000000000..0343c77c751 --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/Components/TranslatorImplantComponent.cs @@ -0,0 +1,21 @@ +using Content.Shared._EinsteinEngine.Language.Components.Translators; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared._EinsteinEngine.Language.Components; + +/// +/// An implant that allows the implantee to speak and understand other languages. +/// +[RegisterComponent] +public sealed partial class TranslatorImplantComponent : BaseTranslatorComponent +{ + /// + /// Whether the implantee knows the languages necessary to speak using this implant. + /// + public bool SpokenRequirementSatisfied = false; + + /// + /// Whether the implantee knows the languages necessary to understand translations of this implant. + /// + public bool UnderstoodRequirementSatisfied = false; +} diff --git a/Content.Shared/_EinsteinEngine/Language/Components/Translators/BaseTranslatorComponent.cs b/Content.Shared/_EinsteinEngine/Language/Components/Translators/BaseTranslatorComponent.cs new file mode 100644 index 00000000000..7702bf12610 --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/Components/Translators/BaseTranslatorComponent.cs @@ -0,0 +1,37 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared._EinsteinEngine.Language.Components.Translators; + +public abstract partial class BaseTranslatorComponent : Component +{ + /// + /// The list of additional languages this translator allows the wielder to speak. + /// + [DataField("spoken")] + public List> SpokenLanguages = new(); + + /// + /// The list of additional languages this translator allows the wielder to understand. + /// + [DataField("understood")] + 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")] + 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("requiresAll")] + public bool RequiresAllLanguages = false; + + [DataField("enabled")] + public bool Enabled = true; +} diff --git a/Content.Shared/_EinsteinEngine/Language/Components/Translators/HandheldTranslatorComponent.cs b/Content.Shared/_EinsteinEngine/Language/Components/Translators/HandheldTranslatorComponent.cs new file mode 100644 index 00000000000..f7b82e5a62e --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/Components/Translators/HandheldTranslatorComponent.cs @@ -0,0 +1,25 @@ +namespace Content.Shared._EinsteinEngine.Language.Components.Translators; + +/// +/// 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] + public bool ToggleOnInteract = true; + + /// + /// If true, when this translator is turned on, the entities' current spoken language will be set + /// to the first new language added by this translator. + /// + /// + /// This should generally be used for translators that translate speech between two languages. + /// + [DataField] + public bool SetLanguageOnInteract = true; +} diff --git a/Content.Shared/_EinsteinEngine/Language/Components/Translators/HoldsTranslatorComponent.cs b/Content.Shared/_EinsteinEngine/Language/Components/Translators/HoldsTranslatorComponent.cs new file mode 100644 index 00000000000..3be76ede6b5 --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/Components/Translators/HoldsTranslatorComponent.cs @@ -0,0 +1,13 @@ +namespace Content.Shared._EinsteinEngine.Language.Components.Translators; + +/// +/// Applied internally to the holder of [HandheldTranslatorComponent]. +/// Do not use directly. Use [HandheldTranslatorComponent] instead. +/// +[RegisterComponent] +public sealed partial class HoldsTranslatorComponent : Component +{ + [NonSerialized] + public HashSet> Translators = new(); + +} diff --git a/Content.Shared/_EinsteinEngine/Language/Components/Translators/IntrinsicTranslatorComponent.cs b/Content.Shared/_EinsteinEngine/Language/Components/Translators/IntrinsicTranslatorComponent.cs new file mode 100644 index 00000000000..e51a91d4054 --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/Components/Translators/IntrinsicTranslatorComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Shared._EinsteinEngine.Language.Components.Translators; + +/// +/// 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 : Translators.BaseTranslatorComponent +{ +} diff --git a/Content.Shared/_EinsteinEngine/Language/Events/DetermineEntityLanguagesEvent.cs b/Content.Shared/_EinsteinEngine/Language/Events/DetermineEntityLanguagesEvent.cs new file mode 100644 index 00000000000..d146cae993f --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/Events/DetermineEntityLanguagesEvent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared._EinsteinEngine.Language.Events; + +/// +/// Raised in order to determine the list of languages the entity can speak and understand at the given moment. +/// Typically raised on an entity after a language agent (e.g. a translator) has been added to or removed from them. +/// +[ByRefEvent] +public record struct DetermineEntityLanguagesEvent +{ + /// + /// The list of all languages the entity may speak. + /// By default, contains the languages this entity speaks intrinsically. + /// + public HashSet> SpokenLanguages = new(); + + /// + /// The list of all languages the entity may understand. + /// By default, contains the languages this entity understands intrinsically. + /// + public HashSet> UnderstoodLanguages = new(); + + public DetermineEntityLanguagesEvent() {} +} diff --git a/Content.Shared/_EinsteinEngine/Language/Events/LanguagesSetMessage.cs b/Content.Shared/_EinsteinEngine/Language/Events/LanguagesSetMessage.cs new file mode 100644 index 00000000000..a7067bfe84d --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/Events/LanguagesSetMessage.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared._EinsteinEngine.Language.Events +{ + /// + /// Sent from the client to the server when it needs to want to set his currentLangauge. + /// Yeah im using this instead of ExecuteCommand... Better right? + /// + [Serializable, NetSerializable] + public sealed class LanguagesSetMessage(string currentLanguage) : EntityEventArgs + { + public string CurrentLanguage = currentLanguage; + } +} diff --git a/Content.Shared/_EinsteinEngine/Language/Events/LanguagesUpdateEvent.cs b/Content.Shared/_EinsteinEngine/Language/Events/LanguagesUpdateEvent.cs new file mode 100644 index 00000000000..c1b6b55d75b --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/Events/LanguagesUpdateEvent.cs @@ -0,0 +1,12 @@ +namespace Content.Shared._EinsteinEngine.Language.Events; + +/// +/// Raised on an entity when its list of languages changes. +/// +/// +/// This is raised both on the server and on the client. +/// The client raises it broadcast after receiving a new language comp state from the server. +/// +public sealed class LanguagesUpdateEvent : EntityEventArgs +{ +} diff --git a/Content.Shared/_EinsteinEngine/Language/LanguagePrototype.cs b/Content.Shared/_EinsteinEngine/Language/LanguagePrototype.cs new file mode 100644 index 00000000000..4fb20961e1b --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/LanguagePrototype.cs @@ -0,0 +1,84 @@ +using Content.Shared.Chat; +using Robust.Shared.Prototypes; + +namespace Content.Shared._EinsteinEngine.Language; + +[Prototype("language")] +public sealed partial class LanguagePrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// Obfuscation method used by this language. By default, uses + /// + [DataField("obfuscation")] + public ObfuscationMethod Obfuscation = ObfuscationMethod.Default; + + /// + /// Speech overrides used for messages sent in this language. + /// + [DataField("speech")] + public SpeechOverrideInfo SpeechOverride = new(); + + #region utility + /// + /// The in-world name of this language, localized. + /// + public string Name => Loc.GetString($"language-{ID}-name"); + + /// + /// The in-world description of this language, localized. + /// + public string Description => Loc.GetString($"language-{ID}-description"); + #endregion utility +} + +[DataDefinition] +public sealed partial class SpeechOverrideInfo +{ + /// + /// Color which text in this language will be blended with. + /// Alpha blending is used, which means the alpha component of the color controls the intensity of this color. + /// + [DataField] + public Color? Color = null; + + [DataField] + public string? FontId; + + [DataField] + public int? FontSize; + + [DataField] + public bool AllowRadio = true; + + /// + /// If false, the entity can use this language even when it's unable to speak (i.e. muffled or muted), + /// and accents are not applied to messages in this language. + /// + [DataField] + public bool RequireSpeech = true; + + /// + /// If not null, all messages in this language will be forced to be spoken in this chat type. + /// + [DataField] + public SharedChatSystem.InGameICChatType? ChatTypeOverride; + + /// + /// Speech verb overrides. If not provided, the default ones for the entity are used. + /// + [DataField] + public List? SpeechVerbOverrides; + + /// + /// Overrides for different kinds chat message wraps. If not provided, the default ones are used. + /// + /// + /// Currently, only local chat and whispers support this. Radio and emotes are unaffected. + /// This is horrible. + /// + [DataField] + public Dictionary MessageWrapOverrides = new(); +} diff --git a/Content.Shared/_EinsteinEngine/Language/ObfuscationMethods.cs b/Content.Shared/_EinsteinEngine/Language/ObfuscationMethods.cs new file mode 100644 index 00000000000..509d19a70fc --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/ObfuscationMethods.cs @@ -0,0 +1,184 @@ +using System.Text; +using Content.Shared._EinsteinEngine.Language.Systems; + +namespace Content.Shared._EinsteinEngine.Language; + +[ImplicitDataDefinitionForInheritors] +public abstract partial class ObfuscationMethod +{ + /// + /// The fallback obfuscation method, replaces the message with the string "<?>". + /// + public static readonly ObfuscationMethod Default = new ReplacementObfuscation + { + Replacement = new List { "" } + }; + + /// + /// Obfuscates the provided message and writes the result into the provided StringBuilder. + /// Implementations should use the context's pseudo-random number generator and provide stable obfuscations. + /// + internal abstract void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context); + + /// + /// Obfuscates the provided message. This method should only be used for debugging purposes. + /// For all other purposes, use instead. + /// + public string Obfuscate(string message) + { + var builder = new StringBuilder(); + Obfuscate(builder, message, IoCManager.Resolve().GetEntitySystem()); + return builder.ToString(); + } +} + +/// +/// The most primitive method of obfuscation - replaces the entire message with one random replacement phrase. +/// Similar to ReplacementAccent. Base for all replacement-based obfuscation methods. +/// +public partial class ReplacementObfuscation : ObfuscationMethod +{ + /// + /// A list of replacement phrases used in the obfuscation process. + /// + [DataField(required: true)] + public List Replacement = []; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + var idx = context.PseudoRandomNumber(message.GetHashCode(), 0, Replacement.Count - 1); + builder.Append(Replacement[idx]); + } +} + +/// +/// Obfuscates the provided message by replacing each word with a random number of syllables in the range (min, max), +/// preserving the original punctuation to a resonable extent. +/// +/// +/// The words are obfuscated in a stable manner, such that every particular word will be obfuscated the same way throughout one round. +/// This means that particular words can be memorized within a round, but not across rounds. +/// +public sealed partial class SyllableObfuscation : ReplacementObfuscation +{ + [DataField] + public int MinSyllables = 1; + + [DataField] + public int MaxSyllables = 4; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + const char eof = (char) 0; // Special character to mark the end of the message in the code below + + var wordBeginIndex = 0; + var hashCode = 0; + + for (var i = 0; i <= message.Length; i++) + { + var ch = i < message.Length ? char.ToLower(message[i]) : eof; + var isWordEnd = char.IsWhiteSpace(ch) || IsPunctuation(ch) || ch == eof; + + // If this is a normal char, add it to the hash sum + if (!isWordEnd) + hashCode = hashCode * 31 + ch; + + // If a word ends before this character, construct a new word and append it to the new message. + if (isWordEnd) + { + var wordLength = i - wordBeginIndex; + if (wordLength > 0) + { + var newWordLength = context.PseudoRandomNumber(hashCode, MinSyllables, MaxSyllables); + + for (var j = 0; j < newWordLength; j++) + { + var index = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); + builder.Append(Replacement[index]); + } + } + + hashCode = 0; + wordBeginIndex = i + 1; + } + + // If this message concludes a word (i.e. is a whitespace or a punctuation mark), append it to the message + if (isWordEnd && ch != eof) + builder.Append(ch); + } + } + + private static bool IsPunctuation(char ch) + { + return ch is '.' or '!' or '?' or ',' or ':'; + } +} + +/// +/// Obfuscates each sentence in the message by concatenating a number of obfuscation phrases. +/// The number of phrases in the obfuscated message is proportional to the length of the original message. +/// +public sealed partial class PhraseObfuscation : ReplacementObfuscation +{ + [DataField] + public int MinPhrases = 1; + + [DataField] + public int MaxPhrases = 4; + + /// + /// A string used to separate individual phrases within one sentence. Default is a space. + /// + [DataField] + public string Separator = " "; + + /// + /// A power to which the number of characters in the original message is raised to determine the number of phrases in the result. + /// Default is 1/3, i.e. the cubic root of the original number. + /// + /// + /// Using the default proportion, you will need at least 27 characters for 2 phrases, at least 64 for 3, at least 125 for 4, etc. + /// Increasing the proportion to 1/4 will result in the numbers changing to 81, 256, 625, etc. + /// + [DataField] + public float Proportion = 1f / 3; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + var sentenceBeginIndex = 0; + var hashCode = 0; + + for (var i = 0; i < message.Length; i++) + { + var ch = char.ToLower(message[i]); + if (!IsPunctuation(ch) && i != message.Length - 1) + { + hashCode = hashCode * 31 + ch; + continue; + } + + var length = i - sentenceBeginIndex; + if (length > 0) + { + var newLength = (int) Math.Clamp(Math.Pow(length, Proportion) - 1, MinPhrases, MaxPhrases); + + for (var j = 0; j < newLength; j++) + { + var phraseIdx = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); + var phrase = Replacement[phraseIdx]; + builder.Append(phrase); + builder.Append(Separator); + } + } + sentenceBeginIndex = i + 1; + + if (IsPunctuation(ch)) + builder.Append(ch).Append(' '); // TODO: this will turn '...' into '. . . ' + } + } + + private static bool IsPunctuation(char ch) + { + return ch is '.' or '!' or '?'; // Doesn't include mid-sentence punctuation like the comma + } +} diff --git a/Content.Shared/_EinsteinEngine/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/_EinsteinEngine/Language/Systems/SharedLanguageSystem.cs new file mode 100644 index 00000000000..614549388af --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/Systems/SharedLanguageSystem.cs @@ -0,0 +1,66 @@ +using System.Text; +using Content.Shared.GameTicking; +using Robust.Shared.Prototypes; + +namespace Content.Shared._EinsteinEngine.Language.Systems; + +public abstract class SharedLanguageSystem : EntitySystem +{ + /// + /// The language used as a fallback in cases where an entity suddenly becomes a language speaker (e.g. the usage of make-sentient) + /// + [ValidatePrototypeId] + public static readonly string FallbackLanguagePrototype = "GalacticCommon"; + + /// + /// The language whose speakers are assumed to understand and speak every language. Should never be added directly. + /// + [ValidatePrototypeId] + public static readonly string UniversalPrototype = "Universal"; + + /// + /// A cached instance of + /// + public static LanguagePrototype Universal { get; private set; } = default!; + + [Dependency] protected readonly IPrototypeManager _prototype = default!; + [Dependency] protected readonly SharedGameTicker _ticker = default!; + + public override void Initialize() + { + Universal = _prototype.Index("Universal"); + } + + public LanguagePrototype? GetLanguagePrototype(ProtoId id) + { + _prototype.TryIndex(id, out var proto); + return proto; + } + + /// + /// Obfuscate a message using the given language. + /// + public string ObfuscateSpeech(string message, LanguagePrototype language) + { + var builder = new StringBuilder(); + language.Obfuscation.Obfuscate(builder, message, this); + + return builder.ToString(); + } + + /// + /// Generates a stable pseudo-random number in the range (min, max) (inclusively) for the given seed. + /// One seed always corresponds to one number, however the resulting number also depends on the current round number. + /// This method is meant to be used in to provide stable obfuscation. + /// + internal int PseudoRandomNumber(int seed, int min, int max) + { + // Using RobustRandom or System.Random here is a bad idea because this method can get called hundreds of times per message. + // Each call would require us to allocate a new instance of random, which would lead to lots of unnecessary calculations. + // Instead, we use a simple but effective algorithm derived from the C language. + // It does not produce a truly random number, but for the purpose of obfuscating messages in an RP-based game it's more than alright. + seed = seed ^ (_ticker.RoundId * 127); + var random = seed * 1103515245 + 12345; + return min + Math.Abs(random) % (max - min + 1); + } +} diff --git a/Content.Shared/_EinsteinEngine/Language/Systems/SharedTranslatorSystem.cs b/Content.Shared/_EinsteinEngine/Language/Systems/SharedTranslatorSystem.cs new file mode 100644 index 00000000000..b9464c37849 --- /dev/null +++ b/Content.Shared/_EinsteinEngine/Language/Systems/SharedTranslatorSystem.cs @@ -0,0 +1,44 @@ +using System.Linq; +using Content.Shared._EinsteinEngine.Language.Components.Translators; +using Content.Shared.Examine; +using Content.Shared.Toggleable; + +namespace Content.Shared._EinsteinEngine.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 understoodLanguageNames = component.UnderstoodLanguages + .Select(it => Loc.GetString($"language-{it}-name")); + var spokenLanguageNames = component.SpokenLanguages + .Select(it => Loc.GetString($"language-{it}-name")); + var requiredLanguageNames = component.RequiredLanguages + .Select(it => Loc.GetString($"language-{it}-name")); + + args.PushMarkup(Loc.GetString("translator-examined-langs-understood", ("languages", string.Join(", ", understoodLanguageNames)))); + args.PushMarkup(Loc.GetString("translator-examined-langs-spoken", ("languages", string.Join(", ", spokenLanguageNames)))); + + args.PushMarkup(Loc.GetString(component.RequiresAllLanguages ? "translator-examined-requires-all" : "translator-examined-requires-any", + ("languages", string.Join(", ", requiredLanguageNames)))); + + args.PushMarkup(Loc.GetString(component.Enabled ? "translator-examined-enabled" : "translator-examined-disabled")); + } + + 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/Fonts/Copperplate.otf b/Resources/Fonts/Copperplate.otf new file mode 100644 index 00000000000..40d45aa46b6 Binary files /dev/null and b/Resources/Fonts/Copperplate.otf differ diff --git a/Resources/Fonts/Mangat.ttf b/Resources/Fonts/Mangat.ttf new file mode 100644 index 00000000000..de4c11beba5 Binary files /dev/null and b/Resources/Fonts/Mangat.ttf differ diff --git a/Resources/Fonts/Noganas.ttf b/Resources/Fonts/Noganas.ttf new file mode 100644 index 00000000000..afa0c82f03e Binary files /dev/null and b/Resources/Fonts/Noganas.ttf differ diff --git a/Resources/Fonts/RubikBubbles.ttf b/Resources/Fonts/RubikBubbles.ttf new file mode 100644 index 00000000000..a653715c6cd Binary files /dev/null and b/Resources/Fonts/RubikBubbles.ttf differ diff --git a/Resources/Locale/en-US/_EinsteinEngine/Language/commands.ftl b/Resources/Locale/en-US/_EinsteinEngine/Language/commands.ftl new file mode 100644 index 00000000000..82427737620 --- /dev/null +++ b/Resources/Locale/en-US/_EinsteinEngine/Language/commands.ftl @@ -0,0 +1,22 @@ +command-list-langs-desc = List languages your current entity can speak at the current moment. +command-list-langs-help = Usage: {$command} +command-saylang-desc = Send a message in a specific language. +command-saylang-help = Usage: {$command} . Example: {$command} GalacticCommon "Hello World!" +command-language-select-desc = Select the currently spoken language of your entity. +command-language-select-help = Usage: {$command} . Example: {$command} GalacticCommon + +# toolshed + +command-description-language-add = Adds a new language to the piped entity. The two last arguments indicate whether it should be spoken/understood. Example: 'self language:add "Canilunzt" true true' +command-description-language-rm = Removes a language from the piped entity. Works similarly to language:add. Example: 'self language:rm "GalacticCommon" true true'. +command-description-language-lsspoken = Lists all languages the entity can speak. Example: 'self language:lsspoken' +command-description-language-lsunderstood = Lists all languages the entity can understand. Example: 'self language:lssunderstood' +command-description-translator-addlang = Adds a new target language to the piped translator entity. See language:add for details. +command-description-translator-rmlang = Removes a target language from the piped translator entity. See language:rm for details. +command-description-translator-addrequired = Adds a new required language to the piped translator entity. Example: 'ent 1234 translator:addrequired "GalacticCommon"' +command-description-translator-rmrequired = Removes a required language from the piped translator entity. Example: 'ent 1234 translator:rmrequired "GalacticCommon"' +command-description-translator-lsspoken = Lists all spoken languages for the piped translator entity. Example: 'ent 1234 translator:lsspoken' +command-description-translator-lsunderstood = Lists all understood languages for the piped translator entity. Example: 'ent 1234 translator:lssunderstood' +command-description-translator-lsrequired = Lists all required languages for the piped translator entity. Example: 'ent 1234 translator:lsrequired' +command-language-error-this-will-not-work = This will not work. +command-language-error-not-a-translator = Entity {$entity} is not a translator. diff --git a/Resources/Locale/en-US/_EinsteinEngine/Language/language-menu.ftl b/Resources/Locale/en-US/_EinsteinEngine/Language/language-menu.ftl new file mode 100644 index 00000000000..83687d0f1a6 --- /dev/null +++ b/Resources/Locale/en-US/_EinsteinEngine/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/_EinsteinEngine/Language/languages-sign.ftl b/Resources/Locale/en-US/_EinsteinEngine/Language/languages-sign.ftl new file mode 100644 index 00000000000..51d24e3b9db --- /dev/null +++ b/Resources/Locale/en-US/_EinsteinEngine/Language/languages-sign.ftl @@ -0,0 +1,5 @@ +chat-sign-language-message-wrap = [italic][BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] [BubbleContent]{$verb} [font="{$fontType}" size={$fontSize}][color={$color}]{$message}[/color][/font][/italic][/BubbleContent] +chat-sign-language-whisper-wrap = [italic][BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] [BubbleContent]subtly gestures [font="{$fontType}" size={$fontSize}][color={$color}]{$message}[/color][/font][/italic][/BubbleContent] +chat-speech-verb-sign-1 = gestures +chat-speech-verb-sign-2 = signs +chat-speech-verb-sign-3 = waves diff --git a/Resources/Locale/en-US/_EinsteinEngine/Language/languages.ftl b/Resources/Locale/en-US/_EinsteinEngine/Language/languages.ftl new file mode 100644 index 00000000000..af5d60354c2 --- /dev/null +++ b/Resources/Locale/en-US/_EinsteinEngine/Language/languages.ftl @@ -0,0 +1,74 @@ +language-Universal-name = Universal +language-Universal-description = What are you? + +language-GalacticCommon-name = Galactic common +language-GalacticCommon-description = The standard Galatic language, most commonly used for inter-species communications and legal work. + +language-Bubblish-name = Bubblish +language-Bubblish-description = The language of Slimes. Being a mixture of bubbling noises and pops it's very difficult to speak for humans without the use of mechanical aids. + +language-RootSpeak-name = Rootspeak +language-RootSpeak-description = The strange whistling-style language spoken by the Diona. + +language-Draconic-name = Draconic +language-Draconic-description = The common language of lizard-people, composed of sibilant hisses and rattles. + +language-SolCommon-name = Sol common +language-SolCommon-description = The language common to species from the Sol System. + +language-Moffic-name = Moffic +language-Moffic-description = The language of the mothpeople borders on complete unintelligibility. + +language-RobotTalk-name = RobotTalk +language-RobotTalk-description = A language consisting of harsh binary chirps, whistles, hisses, and whines. Organic tongues cannot speak it without aid from special translators. + +language-GraySpeak-name = Grey-Speak +language-GraySpeak-description = The language of the grays, a funny sounding language. Pisk. + +language-Sign-name = Galactic Sign Language +language-Sign-description = GSL for short, this sign language is prevalent among mute and deaf people. + +language-Cat-name = Cat +language-Cat-description = Meow + +language-Dog-name = Dog +language-Dog-description = Bark! + +language-Fox-name = Fox +language-Fox-description = Yeeps! + +language-Xeno-name = Xeno +language-Xeno-description = Sssss! + +language-Monkey-name = Monkey +language-Monkey-description = oooook! + +language-Mouse-name = Mouse +language-Mouse-description = Squeeek! + +language-Chicken-name = Chicken +language-Chicken-description = Coot! + +language-Duck-name = Duck +language-Duck-description = Quack! + +language-Cow-name = Cow +language-Cow-description = Moooo! + +language-Sheep-name = Sheep +language-Sheep-description = Baaah! + +language-Kangaroo-name = Kangaroo +language-Kangaroo-description = Chuu! + +language-Pig-name = Pig +language-Pig-description = Oink! + +language-Crab-name = Crab +language-Crab-description = Click! + +language-Kobold-name = Kobold +language-Kobold-description = Hiss! + +language-Hissing-name = Hissing +language-Hissing-description = Hiss! diff --git a/Resources/Locale/en-US/_EinsteinEngine/Language/technologies.ftl b/Resources/Locale/en-US/_EinsteinEngine/Language/technologies.ftl new file mode 100644 index 00000000000..901a48061c5 --- /dev/null +++ b/Resources/Locale/en-US/_EinsteinEngine/Language/technologies.ftl @@ -0,0 +1,2 @@ +research-technology-basic-translation = Basic Translation +research-technology-advanced-translation = Advanced Translation diff --git a/Resources/Locale/en-US/_EinsteinEngine/Language/translator.ftl b/Resources/Locale/en-US/_EinsteinEngine/Language/translator.ftl new file mode 100644 index 00000000000..8070d03be29 --- /dev/null +++ b/Resources/Locale/en-US/_EinsteinEngine/Language/translator.ftl @@ -0,0 +1,13 @@ +translator-component-shutoff = The {$translator} shuts off. +translator-component-turnon = The {$translator} turns on. +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. + +translator-examined-langs-understood = It can translate from: [color=green]{$languages}[/color]. +translator-examined-langs-spoken = It can translate to: [color=green]{$languages}[/color]. +translator-examined-requires-any = It requires you to know at least one of these languages: [color=yellow]{$languages}[/color]. +translator-examined-requires-all = It requires you to know all of these languages: [color=yellow]{$languages}[/color]. +translator-examined-enabled = It appears to be [color=green]active[/color]. +translator-examined-disabled = It appears to be [color=red]turned off[/color]. diff --git a/Resources/Locale/en-US/_EinsteinEngine/Traits/traits.ftl b/Resources/Locale/en-US/_EinsteinEngine/Traits/traits.ftl new file mode 100644 index 00000000000..8bc7aa02087 --- /dev/null +++ b/Resources/Locale/en-US/_EinsteinEngine/Traits/traits.ftl @@ -0,0 +1,12 @@ +trait-foreignerlight-name = Foreigner (light) +trait-foreignerlight-desc = + You struggle to learn this station's primary language, and as such, cannot speak it. You can, however, comprehend what others say in that language. + To help you overcome this obstacle, you are equipped with a translator that helps you speak in this station's primary language. +trait-foreigner-name = Foreigner +trait-foreigner-desc = + For one reason or another you do not speak this station's primary language. + Instead, you have a translator issued to you that only you can use. +trait-signlanguage-name = Sign Language +trait-signlanguage-desc = + You can understand and use Galactic Sign Language (GSL). + If you are mute for any reason, you can still communicate with sign language. diff --git a/Resources/Locale/en-US/chat/managers/chat-manager.ftl b/Resources/Locale/en-US/chat/managers/chat-manager.ftl index 59b927742bb..434d44e9226 100644 --- a/Resources/Locale/en-US/chat/managers/chat-manager.ftl +++ b/Resources/Locale/en-US/chat/managers/chat-manager.ftl @@ -22,11 +22,11 @@ chat-manager-server-wrap-message = [bold]{$message}[/bold] chat-manager-sender-announcement = Central Command chat-manager-sender-announcement-wrap-message = [font size=14][bold]{$sender} Announcement:[/font][font size=12] {$message}[/bold][/font] -chat-manager-entity-say-wrap-message = [BubbleHeader][bold][Name]{$entityName}[/Name][/bold][/BubbleHeader] {$verb}, [font={$fontType} size={$fontSize}]"[BubbleContent]{$message}[/BubbleContent]"[/font] -chat-manager-entity-say-bold-wrap-message = [BubbleHeader][bold][Name]{$entityName}[/Name][/bold][/BubbleHeader] {$verb}, [font={$fontType} size={$fontSize}]"[BubbleContent][bold]{$message}[/bold][/BubbleContent]"[/font] +chat-manager-entity-say-wrap-message = [BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] {$verb}, [font="{$fontType}" size={$fontSize}]"[color={$color}][BubbleContent]{$message}[/BubbleContent][/color]"[/font] +chat-manager-entity-say-bold-wrap-message = [BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] {$verb}, [font="{$fontType}" size={$fontSize}]"[color={$color}][BubbleContent][bold]{$message}[/bold][/BubbleContent][/color]"[/font] -chat-manager-entity-whisper-wrap-message = [font size=11][italic][BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] whispers,"[BubbleContent]{$message}[/BubbleContent]"[/italic][/font] -chat-manager-entity-whisper-unknown-wrap-message = [font size=11][italic][BubbleHeader]Someone[/BubbleHeader] whispers, "[BubbleContent]{$message}[/BubbleContent]"[/italic][/font] +chat-manager-entity-whisper-wrap-message = [font size=11][italic][BubbleHeader][Name]{$entityName}[/Name][/BubbleHeader] whispers,"[color={$color}][BubbleContent]{$message}[/BubbleContent][/color]"[/italic][/font] +chat-manager-entity-whisper-unknown-wrap-message = [font size=11][italic][BubbleHeader]Someone[/BubbleHeader] whispers, "[color={$color}][BubbleContent]{$message}[/BubbleContent][/color]"[/italic][/font] # THE() is not used here because the entity and its name can technically be disconnected if a nameOverride is passed... chat-manager-entity-me-wrap-message = [italic]{ PROPER($entity) -> diff --git a/Resources/Locale/en-US/headset/headset-component.ftl b/Resources/Locale/en-US/headset/headset-component.ftl index 44cdd0853d3..11c78e6ffab 100644 --- a/Resources/Locale/en-US/headset/headset-component.ftl +++ b/Resources/Locale/en-US/headset/headset-component.ftl @@ -1,6 +1,6 @@ # Chat window radio wrap (prefix and postfix) -chat-radio-message-wrap = [color={$color}]{$channel} [bold]{$name}[/bold] {$verb}, [font={$fontType} size={$fontSize}]"{$message}"[/font][/color] -chat-radio-message-wrap-bold = [color={$color}]{$channel} [bold]{$name}[/bold] {$verb}, [font={$fontType} size={$fontSize}][bold]"{$message}"[/bold][/font][/color] +chat-radio-message-wrap = [color={$color}]{$channel} {$name} {$verb}, [font="{$fontType}" size={$fontSize}]"[/color][color={$languageColor}]{$message}[/color][color={$color}]"[/font][/color] +chat-radio-message-wrap-bold = [color={$color}]{$channel} {$name} {$verb}, [font="{$fontType}" size={$fontSize}][bold]"[/color][color={$languageColor}]{$message}[/color][color={$color}]"[/bold][/font][/color] examine-headset-default-channel = Use {$prefix} for the default channel ([color={$color}]{$channel}[/color]). diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index 366739caf40..a026658b705 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -269,6 +269,13 @@ intensitySlope: 20 maxIntensity: 20 canCreateVacuum: false # its for killing the borg not the station + - type: LanguageKnowledge # Corvax-Languages + 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 8e05642c0e9..b7e27c633a5 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -229,8 +229,11 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: ReplacementAccent - accent: chicken + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Chicken + understands: + - Chicken - type: SentienceTarget flavorKind: station-event-random-sentience-flavor-organic - type: NpcFactionMember @@ -513,8 +516,11 @@ amount: 2 - type: Extractable grindableSolutionName: food - - type: ReplacementAccent - accent: mothroach + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Moffic + understands: + - Moffic - type: ZombieAccentOverride accent: zombieMoth - type: Vocal @@ -653,8 +659,11 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: ReplacementAccent - accent: duck + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Duck + understands: + - Duck - type: SentienceTarget flavorKind: station-event-random-sentience-flavor-organic - type: NpcFactionMember @@ -902,8 +911,11 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Voice/Arachnid/arachnid_chitter.ogg - - type: ReplacementAccent - accent: crab + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Crab + understands: + - Crab - type: Bloodstream bloodMaxVolume: 50 bloodReagent: CopperBlood @@ -1146,8 +1158,11 @@ - type: Inventory speciesId: kangaroo templateId: kangaroo - - type: ReplacementAccent - accent: kangaroo + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Kangaroo + understands: + - Kangaroo - type: Butcherable spawned: - id: FoodMeat @@ -1340,7 +1355,12 @@ - type: Speech speechSounds: Monkey speechVerb: Monkey - - type: MonkeyAccent + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Monkey + understands: + - Monkey + - Kobold - type: SentienceTarget flavorKind: station-event-random-sentience-flavor-primate - type: AlwaysRevolutionaryConvertible @@ -1376,7 +1396,13 @@ - type: Speech speechSounds: Monkey speechVerb: Monkey - - type: MonkeyAccent + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Monkey + understands: + - Monkey + - Kobold + - GalacticCommon - type: NpcFactionMember factions: - Syndicate @@ -1419,8 +1445,12 @@ - type: NameIdentifier group: Kobold - type: LizardAccent - - type: ReplacementAccent - accent: kobold + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Kobold + understands: + - Kobold + - Monkey - type: Speech speechSounds: Lizard speechVerb: Reptilian @@ -1697,8 +1727,11 @@ spawned: - id: FoodMeatRat amount: 1 - - type: ReplacementAccent - accent: mouse + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Mouse + understands: + - Mouse - type: Tag tags: - Trash @@ -2076,7 +2109,11 @@ - type: Vocal sounds: Unsexed: Parrot - - type: ParrotAccent + - type: LanguageKnowledge # Corvax-Languages + speaks: + - GalacticCommon + understands: + - GalacticCommon - type: InteractionPopup successChance: 0.6 interactSuccessString: petting-success-bird @@ -2341,8 +2378,11 @@ - type: MeleeChemicalInjector transferAmount: 0.75 solution: melee - - type: ReplacementAccent - accent: xeno + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Xeno + understands: + - Xeno - type: InteractionPopup successChance: 0.5 interactSuccessString: petting-success-tarantula @@ -2522,6 +2562,11 @@ - type: Tag tags: - VimPilot + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Hissing + understands: + - Hissing - type: entity name: possum @@ -2596,6 +2641,11 @@ - type: Tag tags: - VimPilot + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Hissing + understands: + - Hissing - type: entity name: fox @@ -2674,6 +2724,11 @@ - type: Tag tags: - VimPilot + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Fox + understands: + - Fox - type: entity name: corgi @@ -2716,8 +2771,6 @@ spawned: - id: FoodMeat amount: 2 - - type: ReplacementAccent - accent: dog - type: InteractionPopup interactSuccessString: petting-success-dog interactFailureString: petting-failure-generic @@ -2734,6 +2787,12 @@ - type: Tag tags: - VimPilot + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Dog + understands: + - Dog + - GalacticCommon - type: entity name: corrupted corgi @@ -2865,8 +2924,11 @@ spawned: - id: FoodMeat amount: 3 - - type: ReplacementAccent - accent: cat + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Cat + understands: + - Cat - type: InteractionPopup successChance: 0.7 interactSuccessString: petting-success-cat @@ -2946,6 +3008,12 @@ - type: NpcFactionMember factions: - Syndicate + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Cat + understands: + - Cat + - GalacticCommon - type: MeleeWeapon altDisarm: false angle: 0 @@ -3104,6 +3172,11 @@ - type: Tag tags: - VimPilot + - type: LanguageKnowledge # WHAT DOES THE SLOTH SAY??????? + speaks: + - Hissing + understands: + - Hissing - type: entity name: ferret @@ -3158,6 +3231,11 @@ - type: Tag tags: - VimPilot + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Hissing + understands: + - Hissing - type: entity name: hamster @@ -3255,8 +3333,11 @@ spawned: - id: FoodMeat amount: 1 - - type: ReplacementAccent - accent: mouse + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Mouse + understands: + - Mouse - type: Tag tags: - VimPilot @@ -3362,8 +3443,11 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Animals/pig_oink.ogg - - type: ReplacementAccent - accent: pig + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Pig + understands: + - Pig - type: SentienceTarget flavorKind: station-event-random-sentience-flavor-organic - type: NpcFactionMember @@ -3443,6 +3527,12 @@ reformTime: 10 popupText: diona-reform-attempt reformPrototype: MobDionaReformed + - type: LanguageKnowledge # Corvax-Languages + speaks: + - RootSpeak + understands: + - GalacticCommon + - RootSpeak - type: entity parent: MobDionaNymph diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml index 3b6c4e8ed92..dbeba6315c3 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml @@ -15,8 +15,11 @@ - type: Sprite sprite: Mobs/Aliens/Argocyte/argocyte_common.rsi - type: SolutionContainerManager - - type: ReplacementAccent - accent: xeno + - type: LanguageKnowledge # Corvax-Languages + 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/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 131b317fbf5..1b39cec01f9 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -36,6 +36,12 @@ - VimPilot - type: StealTarget stealGroup: AnimalIan + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Dog + understands: + - GalacticCommon + - Dog - type: entity name: Old Ian @@ -148,6 +154,12 @@ - VimPilot - type: StealTarget stealGroup: AnimalNamedCat + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Cat + understands: + - GalacticCommon + - Cat - type: entity name: Exception @@ -168,7 +180,12 @@ - VimPilot - type: StealTarget stealGroup: AnimalNamedCat - + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Cat + understands: + - GalacticCommon + - Cat - type: entity name: Floppa id: MobCatFloppa @@ -307,8 +324,12 @@ spawned: - id: FoodMeat amount: 2 - - type: ReplacementAccent - accent: dog + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Dog + understands: + - GalacticCommon + - Dog - type: InteractionPopup successChance: 0.5 interactSuccessString: petting-success-dog @@ -400,8 +421,12 @@ spawned: - id: FoodMeat amount: 3 - - type: ReplacementAccent - accent: dog + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Dog + understands: + - GalacticCommon + - Dog - type: InteractionPopup successChance: 0.7 interactSuccessString: petting-success-dog @@ -559,6 +584,12 @@ - VimPilot - type: StealTarget stealGroup: AnimalRenault + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Fox + understands: + - GalacticCommon + - Fox - type: entity name: Hamlet @@ -612,6 +643,12 @@ flavors: - meaty - sadness + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Mouse + understands: + - GalacticCommon + - Mouse - type: entity name: Shiva @@ -783,6 +820,12 @@ gender: female - type: TTS # Corvax-TTS voice: Dryad + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Bubblish + understands: + - GalacticCommon + - Bubblish - type: entity name: Pun Pun @@ -818,6 +861,13 @@ attributes: proper: true gender: male + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Monkey + understands: + - GalacticCommon + - Monkey + - Kobold - type: entity name: Tropico @@ -845,3 +895,9 @@ # - type: AlwaysRevolutionaryConvertible - type: StealTarget stealGroup: AnimalTropico + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Crab + understands: + - GalacticCommon + - Crab diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index 72597995137..843b9858d3b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -123,6 +123,13 @@ gender: male - type: TTS # Corvax-TTS voice: Rat + - type: LanguageKnowledge # Corvax-Languages + speaks: + - GalacticCommon + - Mouse + understands: + - GalacticCommon + - Mouse - type: entity id: MobRatKingBuff @@ -200,6 +207,14 @@ - map: [ "enum.DamageStateVisualLayers.BaseUnshaded"] state: eyes shader: unshaded + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Mouse + understands: + - GalacticCommon + - Mouse + + - type: SpriteMovement movementLayers: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml index a7eee79464f..5c543699ba0 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml @@ -77,3 +77,4 @@ - RevenantTheme - type: Speech speechVerb: Ghost + - type: UniversalLanguageSpeaker diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml b/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml index f08fe36544e..a4c7d7f3866 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml @@ -36,7 +36,7 @@ speedModifierThresholds: 60: 0.7 80: 0.5 - + - type: entity name: shadow cat parent: BaseShadowMob @@ -64,4 +64,9 @@ gender: epicene - type: Tag tags: - - VimPilot \ No newline at end of file + - VimPilot + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Cat + understands: + - Cat diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index 857115e10f1..e0389e87c76 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -111,6 +111,13 @@ - type: ProtectedFromStepTriggers - type: NoSlip - type: Insulated + - type: LanguageKnowledge # Corvax-Languages + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk - type: entity parent: MobSiliconBase diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index 54705bf40d1..3c690ac8709 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -120,8 +120,11 @@ abstract: true description: It looks so much like jelly. I wonder what it tastes like? components: - - type: ReplacementAccent - accent: slimes + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Bubblish + understands: + - Bubblish - type: GhostTakeoverAvailable - type: GhostRole makeSentient: true diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index a51fe522381..7dfc63e3b9b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -165,8 +165,11 @@ - type: FootstepModifier footstepSoundCollection: collection: FootstepBounce - - type: ReplacementAccent - accent: kangaroo + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Kangaroo + understands: + - Kangaroo - type: entity id: MobKangarooSpaceSalvage @@ -242,8 +245,11 @@ - type: MeleeChemicalInjector solution: melee transferAmount: 4 - - type: ReplacementAccent - accent: xeno + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Xeno + understands: + - Xeno - type: InteractionPopup successChance: 0.20 interactSuccessString: petting-success-tarantula @@ -346,8 +352,11 @@ - type: MeleeChemicalInjector solution: melee transferAmount: 6 - - type: ReplacementAccent - accent: xeno + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Xeno + understands: + - Xeno - type: InteractionPopup successChance: 0.2 interactSuccessString: petting-success-snake diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index f4a79d110ee..053ee650494 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -119,6 +119,11 @@ molsPerSecondPerUnitMass: 0.0005 - type: Speech speechVerb: LargeMob + - type: LanguageKnowledge # Corvax-Languages + speaks: + - Xeno + understands: + - Xeno - type: entity name: praetorian @@ -228,6 +233,13 @@ - type: Tag tags: - CannotSuicide + - type: LanguageKnowledge # Corvax-Languages + speaks: + - GalacticCommon + - Xeno + understands: + - GalacticCommon + - Xeno - type: entity name: ravager diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index c02629c4d6f..a87321718a7 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -60,6 +60,7 @@ - type: Tag tags: - BypassInteractionRangeChecks + - type: UniversalLanguageSpeaker # Corvax-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 ffbc46e94c3..2d96c7eef64 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml @@ -7,3 +7,4 @@ - type: MovementSpeedModifier baseSprintSpeed: 24 baseWalkSpeed: 16 + - type: UniversalLanguageSpeaker # Corvax-Lang diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 5cebd2cf853..b23021fe07f 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -213,7 +213,12 @@ - FootstepSound - DoorBumpOpener - AnomalyHost - + - type: LanguageKnowledge # Corvax-Languages + # This is here so all with no LanguageSpeaker at least spawn with the default languages. + speaks: + - GalacticCommon + understands: + - GalacticCommon - type: entity save: false parent: diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index bf357e1f102..8fe56bcd991 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -40,4 +40,4 @@ sizeMaps: 32: sprite: Mobs/Species/Human/displacement.rsi - state: jumpsuit-female \ No newline at end of file + state: jumpsuit-female diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml index ad09fd57578..f79316dab9f 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml @@ -24,7 +24,7 @@ accent: zombieMoth - type: Speech speechVerb: Moth - allowedEmotes: ['Chitter', 'Squeak'] + allowedEmotes: [ 'Chitter', 'Squeak' ] - type: TypingIndicator proto: moth - type: Butcherable diff --git a/Resources/Prototypes/Entities/Objects/Fun/pai.yml b/Resources/Prototypes/Entities/Objects/Fun/pai.yml index 9301d90a8b9..83a6103e1c4 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/pai.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/pai.yml @@ -75,6 +75,26 @@ Searching: { state: pai-searching-overlay } On: { state: pai-on-overlay } - type: StationMap + - type: LanguageKnowledge # Corvax-Languages + # Corvax-Languages + speaks: + - SolCommon + - RobotTalk + - GraySpeak + - Draconic + - RootSpeak + - Moffic + - Bubblish + - GalacticCommon + understands: + - SolCommon + - RobotTalk + - GraySpeak + - Draconic + - RootSpeak + - Moffic + - Bubblish + - GalacticCommon - type: entity parent: [ PersonalAI, BaseSyndicateContraband] diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index b5b3f770e57..b0ca5548967 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -350,6 +350,11 @@ - FauxTileAstroSnow - OreBagOfHolding - DeviceQuantumSpinInverter + - BubblishTranslator + - DraconicTranslator + - SolCommonTranslator + - RootSpeakTranslator + - XenoTranslator - type: EmagLatheRecipes emagDynamicRecipes: - BoxBeanbag diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 8205fd9f224..d16dff56a48 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -104,6 +104,13 @@ price: 100 - type: Appearance - type: WiresVisuals + - type: LanguageKnowledge # Corvax-Languages + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk - type: entity parent: VendingMachine diff --git a/Resources/Prototypes/Research/civilianservices.yml b/Resources/Prototypes/Research/civilianservices.yml index b990eb6ae40..02ea35ae950 100644 --- a/Resources/Prototypes/Research/civilianservices.yml +++ b/Resources/Prototypes/Research/civilianservices.yml @@ -229,3 +229,32 @@ recipeUnlocks: - BluespaceBeaker - SyringeBluespace + +- type: technology + id: BasicTranslation + name: research-technology-basic-translation + icon: + sprite: _EinsteinEngine/Objects/Devices/translator.rsi + state: icon + discipline: CivilianServices + tier: 2 + cost: 10000 + recipeUnlocks: + - BubblishTranslator + - DraconicTranslator + - SolCommonTranslator + - RootSpeakTranslator + - MofficTranslator + +- type: technology + id: AdvancedTranslation + name: research-technology-advanced-translation + icon: + sprite: _EinsteinEngine/Objects/Devices/translator.rsi + state: icon + discipline: CivilianServices + tier: 3 + cost: 15000 + recipeUnlocks: + - XenoTranslator + - AnimalTranslator diff --git a/Resources/Prototypes/_EinsteinEngine/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/_EinsteinEngine/Entities/Objects/Devices/translators.yml new file mode 100644 index 00000000000..27865f450df --- /dev/null +++ b/Resources/Prototypes/_EinsteinEngine/Entities/Objects/Devices/translators.yml @@ -0,0 +1,197 @@ +- type: entity + id: TranslatorUnpowered + parent: BaseItem + name: translator + description: Translates speech. + components: + - type: Sprite + sprite: _EinsteinEngine/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: Clothing # To allow equipping translators on the neck slot + slots: [neck, pocket] + equipDelay: 0.3 + unequipDelay: 0.3 + quickEquip: false # Would conflict + +- type: entity + id: Translator + parent: [ TranslatorUnpowered, PowerCellSlotMediumItem ] + suffix: Powered + components: + - type: PowerCellDraw + drawRate: 1 + +- type: entity + id: TranslatorEmpty + parent: Translator + suffix: Empty + components: + - type: ItemSlots + slots: + cell_slot: + name: power-cell-slot-component-slot-name-default + +- type: entity + id: TranslatorForeigner + parent: [ Translator, PowerCellSlotHighItem ] + name: foreigner's translator + description: A special-issue translator that helps foreigner's speak and understand this station's primary language. + +- type: entity + id: BubblishTranslator + parent: [ TranslatorEmpty ] + name: Bubblish translator + description: Translates speech between Bubblish and Galactic Common, helping communicate with slimes and slime people. + components: + - type: HandheldTranslator + spoken: + - GalacticCommon + - Bubblish + understood: + - GalacticCommon + - Bubblish + requires: + - GalacticCommon + - Bubblish + +- type: entity + id: GraySpeakTranslator + parent: [ TranslatorEmpty ] + name: GraySpeak translator + description: Translates speech between Gray Speak and Galactic Common, helping communicate with the Grays. + components: + - type: HandheldTranslator + spoken: + - GalacticCommon + - GraySpeak + understood: + - GalacticCommon + - GraySpeak + requires: + - GalacticCommon + - GraySpeak + +- type: entity + id: DraconicTranslator + parent: [ TranslatorEmpty ] + name: Draconic translator + description: Translates speech between Draconic and Galactic Common, making it easier to understand your local Uniathi. + components: + - type: HandheldTranslator + spoken: + - GalacticCommon + - Draconic + understood: + - GalacticCommon + - Draconic + requires: + - GalacticCommon + - Draconic + +- type: entity + id: SolCommonTranslator + parent: [ TranslatorEmpty ] + name: Sol Common translator + description: Translates speech between Sol Common and Galactic Common. Like a true Earthman! + components: + - type: HandheldTranslator + spoken: + - GalacticCommon + - SolCommon + understood: + - GalacticCommon + - SolCommon + requires: + - GalacticCommon + - SolCommon + +- type: entity + id: RootSpeakTranslator + parent: [ TranslatorEmpty ] + name: RootSpeak translator + description: Translates speech between RootSpeak and Galactic Common. You may now speak for the trees. + components: + - type: HandheldTranslator + spoken: + - GalacticCommon + - RootSpeak + understood: + - GalacticCommon + - RootSpeak + requires: + - GalacticCommon + - RootSpeak + +- type: entity + id: MofficTranslator + parent: [ TranslatorEmpty ] + name: Moffic translator + description: Translates speech between Moffic and Galactic Common, helping you understand the buzzes of your pet mothroach! + components: + - type: HandheldTranslator + spoken: + - GalacticCommon + - Moffic + understood: + - GalacticCommon + - Moffic + requires: + - GalacticCommon + - Moffic + +- type: entity + id: XenoTranslator + parent: [ TranslatorEmpty ] + name: Xeno translator + description: Translates speech between Xeno and Galactic Common. This will probably not help you survive an encounter, though. + components: + - type: HandheldTranslator + spoken: + - GalacticCommon + - Xeno + understood: + - GalacticCommon + - Xeno + requires: + - GalacticCommon + +- type: entity + id: AnimalTranslator + parent: [ TranslatorEmpty ] + name: Animal translator + description: Translates all the cutes noises that most animals make into a more understandable form! + components: + - type: HandheldTranslator + understood: + - Cat + - Dog + - Fox + - Monkey + - Mouse + - Chicken + - Duck + - Cow + - Sheep + - Kangaroo + - Pig + - Crab + - Hissing + - Kobold + requires: + - GalacticCommon + setLanguageOnInteract: false diff --git a/Resources/Prototypes/_EinsteinEngine/Language/languages.yml b/Resources/Prototypes/_EinsteinEngine/Language/languages.yml new file mode 100644 index 00000000000..08398c63952 --- /dev/null +++ b/Resources/Prototypes/_EinsteinEngine/Language/languages.yml @@ -0,0 +1,954 @@ +# The universal language, assumed if the entity has a UniversalLanguageSpeakerComponent. +# Do not use otherwise. Making an entity explicitly understand/speak this language will NOT have the desired effect. +- type: language + id: Universal + obfuscation: + !type:ReplacementObfuscation + replacement: + - "*incomprehensible*" # Never actually used + +# The common galactic tongue. +- type: language + id: GalacticCommon + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - a + - ado + - ago + - aj + - ajn + - al + - alt + - am + - amas + - an + - ang + - ante + - ap + - ard + - arma + - aro + - as + - aur + - aut + - aw + - ba + - bal + - bao + - be + - beau + - bel + - bi + - bit + - blu + - bo + - bod + - boj + - bojn + - bu + - but + - ca + - caj + - ce + - cer + - chun + - ci + - cion + - coj + - cor + - da + - daj + - dan + - de + - den + - dis + - do + - dor + - dorm + - eco + - ego + - ek + - eks + - en + - ero + - es + - est + - et + - eve + - fa + - fe + - fel + - fla + - foj + - fra + - fraz + - fros + - ful + - fut + - ga + - gan + - gar + - gi + - gis + - go + - gran + - ha + - han + - hav + - hom + - hong + - hu + - hum + - hushi + - ia + - iaj + - ica + - id + - idon + - il + - in + - ing + - io + - is + - iton + - iza + - ja + - ji + - jirou + - joj + - ka + - kaj + - kajo + - kan + - ke + - ket + - ki + - kna + - krio + - ku + - kui + - kuk + - kun + - kur + - la + - laca + - leng + - les + - li + - liao + - lib + - ling + - lis + - lo + - lon + - long + - lu + - lud + - ma + - mal + - man + - me + - mego + - mero + - mi + - mia + - min + - mo + - moj + - mola + - mon + - mul + - ne + - ner + - ni + - nio + - nu + - of + - oj + - om + - ou + - pe + - pi + - plan + - pli + - po + - por + - post + - pre + - prin + - pru + - pu + - pur + - qiu + - que + - ra + - ras + - re + - ri + - rig + - ril + - ro + - roj + - ron + - roso + - rou + - ru + - sa + - san + - sci + - sek + - shi + - shiia + - shiue + - shiwu + - shu + - shui + - si + - siaj + - sku + - so + - som + - sti + - str + - stre + - su + - suno + - ta + - tan + - tas + - te + - tel + - tem + - the + - ti + - tian + - tita + - tiu + - to + - toj + - ton + - tran + - tre + - tri + - trin + - tro + - trus + - un + - undo + - uno + - uz + - va + - var + - varm + - vas + - ve + - vek + - ven + - ves + - vi + - via + - vin + - vino + - vint + - vir + - von + - vu + - whe + - wu + - yong + - zem + - zo + - zoj + - zon + +# Spoken by slimes. +- type: language + id: Bubblish + speech: + color: "#DDFFF8" + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 + replacement: + - blob + - plop + - pop + - bop + - boop + +# Spoken by moths. +- type: language + id: Moffic + speech: + color: "#FFF3DD" + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 # Replacements are really short + maxSyllables: 4 + replacement: + - år + - i + - går + - sek + - mo + - ff + - ok + - gj + - ø + - gå + - la + - le + - lit + - ygg + - van + - dår + - næ + - møt + - idd + - hvo + - ja + - på + - han + - så + - ån + - det + - att + - nå + - gö + - bra + - int + - tyc + - om + - när + - två + - må + - dag + - sjä + - vii + - vuo + - eil + - tun + - käyt + - teh + - vä + - hei + - huo + - suo + - ää + - ten + - ja + - heu + - stu + - uhr + - kön + - we + - hön + +# Spoken by dionas. +- type: language + id: RootSpeak + speech: + color: "#F3FFDD" + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 5 + replacement: + - hs + - zt + - kr + - st + - sh + +# Spoken by the Lizard race. +- type: language + id: Draconic + speech: + color: "#DDFFE0" + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 + replacement: + - za + - az + - ze + - ez + - zi + - iz + - zo + - oz + - zu + - uz + - zs + - sz + - ha + - ah + - he + - eh + - hi + - ih + - ho + - oh + - hu + - uh + - hs + - sh + - la + - al + - le + - el + - li + - il + - lo + - ol + - lu + - ul + - ls + - sl + - ka + - ak + - ke + - ek + - ki + - ik + - ko + - ok + - ku + - uk + - ks + - sk + - sa + - as + - se + - es + - si + - is + - so + - os + - su + - us + - ss + - ss + - ra + - ar + - re + - er + - ri + - ir + - ro + - or + - ru + - ur + - rs + - sr + - a + - a + - e + - e + - i + - i + - o + - o + - u + - u + - s + - s + +# The common language of the Sol system. +- type: language + id: SolCommon + speech: + color: "#F9F0C7" + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 + replacement: + - tao + - shi + - tzu + - yi + - com + - be + - is + - i + - op + - vi + - ed + - lec + - mo + - cle + - te + - dis + - e + +- type: language + id: RobotTalk + speech: + fontId: Monospace + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 10 # Crazy + replacement: + - 0 + - 1 + +# Spoken by grays. +- type: language + id: GraySpeak + speech: + color: "#BCD3BA" + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 1 + replacement: + - thusd + - gleep + - glorm + - beego + - mochigi + - leedar + - kibeab + - cheego + - sipa + - yoomba + - toupa + - nitbal + - jorl + - glorp + - jumba + - jambu + - mo + - tii + - gimbo + - dom jot + - gramba + - yaer + - gnarp + - sipsi + - vroma + - eitei + - deni + - booma + - nuuka + - doomba + - lorf + - tssshi + - glorm-vroma + - pisk + - piskun + - douda + - beepi + - beepimo + - woota + - wotua + - sqoshi + - snekorf + - tsshi-suni + - suni + - erth + - kieel + - ouu + - dreen + - oo-moo + - mox + - bogos + - beba + - keboo + - meilmo + - cheezi + - glumpi + - glumph + - schui + - goiip + - stupee + - ortimesi + - waba-ku + - waba-beepi + - brimbi + - finni + - indurstandar + - gloef + - dinne + - chalkee + - bweee + - keb + - clune + - slipi + - geena + - jamga + - ouuti + - seben + - ei's + - eif + - graba + - graba-u + - mobie + - tek + - mou + - zooti + - flomi + - zigeef + - too + - tee + - milx + - jambupisk + - thikee + - embinum + - jazee + - vru + - keb + - thui + - wamba't + - modeni + - not + - fum + - noot + - splorp + - sporp + - spop + - zazz + - pushee + - deek + - tronk + - blagh + - blaagh + - zat + - blag + - zim + - flii + - jorp + - jurm + - beben + - toie + - tin + - plod're + - mozz'm + - thui-wa + - thui-wa're + - gleepzz + - pu + - ziip + - frup + - pib + - quz + - woglump + - yii + - yim + - bormpt + - twii + - zii + - bli + - zwep + - ooo + - oom + - yeta + - lopi + - mu + - mee + - zwee + - zwoo + - lupi + - uumi + - ouli + - euni + - zwu + - sebon's + - sebon + - ei + - glump + - bogo + - pisku + - glormun + - buugo + - beeg + - yuumba + - yoombun + - tuupa + - toupun + - nitbul + - nitbun + - wuuta + - wootun + - moue + - glumpun + - schuu + - schuiun + - gnurp + - gnarpun + - flumi + - flumun + - blu + - blin + - glurm + - sipsun + - supsi + - uoo + - bebun + - buba + - grumba + - grambun + - urth + - erthun + - fanni + - grabun + - gruba + - leedars + - kibeabs + - sipas + - vromas + - eiteis + - boomas + - nuukas + - me-doombas + - tssshis + - glorm-vromas + - beepis + - beepimos + - sqoshis + - snekorfi + - tssi-sunis + - sunis + - cheezis + - stupees + - ortimesis + - waba-kus + - waba-beepis + - brimbis + - dinnes + - chalkees + - slipis + - geenas + - pushees + - deeks + - twiis + - pusk + - dinnun + - dunne + - buupi + - beepun + - duuda + - doudun + - dom-un + - dum jut + - oum + +# Languages spoken by various critters. +- type: language + id: Cat + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - murr + - meow + - purr + - mrow + +- type: language + id: Dog + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - woof + - bark + - ruff + - bork + - raff + - garr + +- type: language + id: Fox + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - ruff + - raff + - garr + - yip + - yap + - myah + +- type: language + id: Xeno + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # I was crazy once + replacement: + - s + - S + +- type: language + id: Monkey + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # They locked me in a room... + replacement: + - o + - k + +- type: language + id: Mouse + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 3 + replacement: + - squ + - eak + - pi + - ep + - chuu + - ee + - fwi + - he + +- type: language + id: Chicken + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - co + - coo + - ot + +- type: language + id: Duck + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - qu + - ack + - quack + +- type: language + id: Cow + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - moo + - mooo + +- type: language + id: Sheep + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - ba + - baa + - aa + +- type: language + id: Kangaroo + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - shre + - ack + - chuu + - choo + +- type: language + id: Pig + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - oink # Please someone come up with something better + +- type: language + id: Crab + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - click + - clack + - ti + - pi + - tap + - cli + - ick + +- type: language + id: Kobold + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 + replacement: + - yip + - yap + - gar + - grr + - ar + - scre + - et + - gronk + - hiss + - ss + - ee + +- type: language + id: Hissing + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 + replacement: + - hss + - iss + - ss + - is + + +# Example of a sign language. Not currently used anyhow. +- type: language + id: Sign + speech: + allowRadio: false + requireSpeech: false + color: "#dddddd" + messageWrapOverrides: + Speak: chat-sign-language-message-wrap + Whisper: chat-sign-language-whisper-wrap + speechVerbOverrides: + - chat-speech-verb-sign-1 + - chat-speech-verb-sign-2 + - chat-speech-verb-sign-3 + obfuscation: + !type:ReplacementObfuscation + replacement: + - something + - a cryptic message to you + - a signal to you + - a message + - a rude expression to you + - a sad expression to you + - a happy expression to you diff --git a/Resources/Prototypes/_EinsteinEngine/Recipes/Lathes/language.yml b/Resources/Prototypes/_EinsteinEngine/Recipes/Lathes/language.yml new file mode 100644 index 00000000000..c7c58b47f55 --- /dev/null +++ b/Resources/Prototypes/_EinsteinEngine/Recipes/Lathes/language.yml @@ -0,0 +1,71 @@ +- type: latheRecipe + id: BubblishTranslator + result: BubblishTranslator + 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: XenoTranslator + result: XenoTranslator + completetime: 2 + materials: + Steel: 200 + Plastic: 50 + Gold: 50 + Plasma: 50 + Silver: 50 + +- type: latheRecipe + id: AnimalTranslator + result: AnimalTranslator + completetime: 2 + materials: + Steel: 200 + Plastic: 50 + Gold: 50 + Plasma: 50 + Silver: 5 diff --git a/Resources/Prototypes/_EinsteinEngine/Traits/quirks.yml b/Resources/Prototypes/_EinsteinEngine/Traits/quirks.yml new file mode 100644 index 00000000000..e3ab739f7df --- /dev/null +++ b/Resources/Prototypes/_EinsteinEngine/Traits/quirks.yml @@ -0,0 +1,34 @@ +# - type: trait +# id: ForeignerLight +# category: Quirks +# name: trait-foreignerlight-name +# description: trait-foreignerlight-desc +# requirements: +# - !type:TraitGroupExclusionRequirement +# prototypes: [ Foreigner ] +# components: +# - type: ForeignerTrait +# cantUnderstand: false # Allows to understand +# baseTranslator: TranslatorForeigner + +# - type: trait +# id: Foreigner +# category: Quirks +# name: trait-foreigner-name +# description: trait-foreigner-desc +# components: +# - type: ForeignerTrait +# baseTranslator: TranslatorForeigner + +- type: trait + id: SignLanguage + category: Quirks + name: trait-signlanguage-name + description: trait-signlanguage-desc + components: + - type: LanguageKnowledgeModifier + speaks: + - Sign + understands: + - Sign + diff --git a/Resources/Textures/_EinsteinEngine/Interface/language.png b/Resources/Textures/_EinsteinEngine/Interface/language.png new file mode 100644 index 00000000000..2b39424d12d Binary files /dev/null and b/Resources/Textures/_EinsteinEngine/Interface/language.png differ diff --git a/Resources/Textures/_EinsteinEngine/Objects/Devices/translator.rsi/icon.png b/Resources/Textures/_EinsteinEngine/Objects/Devices/translator.rsi/icon.png new file mode 100644 index 00000000000..6871c808ccd Binary files /dev/null and b/Resources/Textures/_EinsteinEngine/Objects/Devices/translator.rsi/icon.png differ diff --git a/Resources/Textures/_EinsteinEngine/Objects/Devices/translator.rsi/meta.json b/Resources/Textures/_EinsteinEngine/Objects/Devices/translator.rsi/meta.json new file mode 100644 index 00000000000..0202c0c39c7 --- /dev/null +++ b/Resources/Textures/_EinsteinEngine/Objects/Devices/translator.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 2, + "license": "CC-BY-SA-3.0", + "copyright": "baystation12", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "translator" + } + ] +} diff --git a/Resources/Textures/_EinsteinEngine/Objects/Devices/translator.rsi/translator.png b/Resources/Textures/_EinsteinEngine/Objects/Devices/translator.rsi/translator.png new file mode 100644 index 00000000000..6c54a0b8636 Binary files /dev/null and b/Resources/Textures/_EinsteinEngine/Objects/Devices/translator.rsi/translator.png differ diff --git a/Resources/Textures/_EinsteinEngine/Traits/inconveniences.yml b/Resources/Textures/_EinsteinEngine/Traits/inconveniences.yml new file mode 100644 index 00000000000..7718074cc4a --- /dev/null +++ b/Resources/Textures/_EinsteinEngine/Traits/inconveniences.yml @@ -0,0 +1,22 @@ +- type: trait + id: ForeignerLight + category: Mental + points: 1 + requirements: + - !type:TraitGroupExclusionRequirement + prototypes: [ Foreigner ] + components: + - type: ForeignerTrait + cantUnderstand: false # Allows to understand + baseTranslator: TranslatorForeigner + +- type: trait + id: Foreigner + category: Mental + points: 2 + requirements: # TODO: Add a requirement to know at least 1 non-gc language + - !type:TraitGroupExclusionRequirement + prototypes: [ ForeignerLight ] + components: + - type: ForeignerTrait + baseTranslator: TranslatorForeigner diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 3b8158b7c7a..7aff0b7f434 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -187,6 +187,9 @@ binds: - function: OpenCharacterMenu type: State key: C +- function: OpenLanguageMenu # Corvax-Language + type: State # Corvax-Language + key: L # Corvax-Language - function: OpenEmotesMenu type: State key: Y