From 85a832572cea8ad4caeb7472e17169fb7aa266d1 Mon Sep 17 00:00:00 2001 From: Dirius Date: Thu, 5 Mar 2026 19:23:25 -0500 Subject: [PATCH 01/15] Languages (and soft chat rework) --- Content.Client/Input/ContentContexts.cs | 1 + Content.Client/Options/UI/OptionsMenu.xaml | 2 + .../Options/UI/Tabs/KeyRebindTab.xaml.cs | 1 + .../MenuBar/GameTopMenuBarUIController.cs | 4 + .../MenuBar/Widgets/GameTopMenuBar.xaml | 11 + .../Language/EntitySystems/LanguageSystem.cs | 99 ++ .../_DEN/Options/UI/Tabs/DenTab.xaml | 15 + .../_DEN/Options/UI/Tabs/DenTab.xaml.cs | 17 + .../Language/Controls/LanguageContainer.cs | 146 +++ .../Systems/Language/LanguageUIController.cs | 300 +++++ .../Language/Windows/LanguageWindow.xaml | 19 + .../Language/Windows/LanguageWindow.xaml.cs | 16 + .../Animals/Systems/ParrotMemorySystem.cs | 16 +- .../Anomaly/AnomalySystem.Generator.cs | 2 +- .../Cargo/Systems/CargoSystem.Funds.cs | 6 +- .../Cargo/Systems/CargoSystem.Orders.cs | 4 +- Content.Server/Chat/Systems/ChatSystem.cs | 74 +- .../Systems/CriminalRecordsConsoleSystem.cs | 4 +- Content.Server/Holopad/HolopadSystem.cs | 6 +- Content.Server/Lathe/LatheSystem.cs | 2 +- .../Radio/EntitySystems/HeadsetSystem.cs | 9 +- .../Radio/EntitySystems/RadioDeviceSystem.cs | 13 +- .../Radio/EntitySystems/RadioSystem.cs | 12 +- Content.Server/Radio/RadioEvent.cs | 2 + .../Systems/ResearchSystem.Console.cs | 2 +- .../Robotics/Systems/RoboticsConsoleSystem.cs | 2 +- .../Salvage/JobBoard/SalvageJobBoardSystem.cs | 2 +- Content.Server/Salvage/SalvageSystem.cs | 2 +- .../EntitySystems/EmitterSystem.cs | 2 +- .../Speech/Components/ListenWireAction.cs | 2 +- .../EntitySystems/BlockListeningSystem.cs | 7 +- .../Speech/EntitySystems/ListeningSystem.cs | 9 +- Content.Server/Speech/Muting/MutingSystem.cs | 7 +- Content.Server/Speech/SpeechNoiseSystem.cs | 7 +- .../SurveillanceCameraMicrophoneSystem.cs | 11 +- .../SurveillanceCameraSpeakerSystem.cs | 7 +- Content.Server/Telephone/TelephoneSystem.cs | 14 +- .../Trigger/Systems/RattleOnTriggerSystem.cs | 2 +- .../VendingMachines/VendingMachineSystem.cs | 7 +- .../Systems/DatasetVocalizationSystem.cs | 7 +- .../Systems/RadioVocalizationSystem.cs | 4 +- .../Systems/VocalizationSystem.cs | 10 +- .../Systems/ParrotMemorySystem.Language.cs | 157 +++ .../_DEN/Chat/SharedChatEvents.Language.cs | 30 + .../_DEN/Chat/Systems/ChatSystem.Language.cs | 442 ++++++++ .../_DEN/Holopad/HolopadSystem.Language.cs | 18 + .../_DEN/Language/Commands/LanguageCommand.cs | 145 +++ .../Language/Commands/SetLanguageCommand.cs | 63 ++ .../Language/EntitySystems/GestaltSystem.cs | 119 ++ .../Language/EntitySystems/LanguageSystem.cs | 73 ++ .../EntitySystems/SyllableScramblingSystem.cs | 218 ++++ .../UniversalLanguageSpeakerSystem.cs | 42 + .../EntitySystems/HeadsetSystem.Language.cs | 63 ++ .../RadioDeviceSystem.Language.cs | 70 ++ .../EntitySystems/RadioSystem.Language.cs | 179 +++ .../_DEN/Radio/RadioEvent.Language.cs | 13 + .../BlockListeningSystem.Language.cs | 19 + .../EntitySystems/ListeningSystem.Language.cs | 64 ++ .../Speech/Muting/MutingSystem.Language.cs | 35 + .../_DEN/Speech/SpeechSoundSystem.Language.cs | 43 + ...eillanceCameraMicrophoneSystem.Language.cs | 58 + ...urveillanceCameraSpeakerSystem.Language.cs | 56 + .../_DEN/Telephone/TelephoneEvents.cs | 27 + .../Telephone/TelephoneSystem.Language.cs | 150 +++ .../VendingMachineSystem.Language.cs | 17 + .../DatasetVocalizationSystem.Language.cs | 23 + .../RadioVocalizationSystem.Language.cs | 50 + .../Systems/VocalizationSystem.Language.cs | 83 ++ .../ActionBlocker/ActionBlockerSystem.cs | 3 +- .../Administration/AdminFrozenSystem.cs | 7 +- .../Components/ParrotMemoryComponent.cs | 4 +- Content.Shared/Bed/Sleep/SleepingSystem.cs | 5 +- Content.Shared/Chat/SharedChatEvents.cs | 4 +- Content.Shared/Chat/SharedChatSystem.cs | 22 + .../SharedTypingIndicatorSystem.cs | 3 +- .../EntitySystems/SharedHandsSystem.Relay.cs | 2 + .../SharedSubdermalImplantSystem.Relays.cs | 2 + Content.Shared/Input/ContentKeyFunctions.cs | 2 + .../Inventory/InventorySystem.Relay.cs | 1 + .../Systems/MobStateSystem.Subscribers.cs | 5 +- Content.Shared/Speech/ListenEvent.cs | 2 + Content.Shared/Speech/SpeakAttemptEvent.cs | 5 +- Content.Shared/Speech/SpeechSystem.cs | 6 +- .../Telephone/TelephoneComponent.cs | 2 + .../Trigger/Systems/TriggerSystem.Voice.cs | 20 +- .../ActionBlockerSystem.Language.cs | 16 + .../AdminFrozenSystem.Language.cs | 19 + .../_DEN/Bed/Sleep/SleepingSystem.Language.cs | 31 + Content.Shared/_DEN/CCVars/DenCCVars.cs | 37 + .../_DEN/Chat/SharedChatSystem.Language.cs | 138 +++ .../SharedTypingIndicatorSystem.Language.cs | 34 + .../SharedHandsSystem.Relay.Language.cs | 13 + ...dSubdermalImplantSystem.Relays.Language.cs | 13 + .../InventorySystem.Relay.Languages.cs | 12 + .../Language/Components/AudibleComponent.cs | 7 + .../ChatChannelWhitelistComponent.cs | 28 + .../Components/ChildLanguageComponent.cs | 13 + .../Language/Components/GestaltComponent.cs | 29 + .../Components/GestaltHostComponent.cs | 4 + .../LanguageCommunicatorComponent.cs | 25 + .../Language/Components/LanguageComponent.cs | 32 + .../LanguageFontSuppressionComponent.cs | 7 + .../LineOfSightLanguageComponent.cs | 7 + .../Components/MinimumFluencyComponent.cs | 14 + .../Components/RadioTransmittableComponent.cs | 4 + .../Components/ReplaceSpeakerNameComponent.cs | 8 + .../SpeechTransformableComponent.cs | 5 + .../Components/SyllableScramblingComponent.cs | 17 + .../Components/TranslatedLanguageComponent.cs | 6 + .../Components/TranslatorComponent.cs | 38 + .../UnconsciousLanguageComponent.cs | 7 + .../UniversalLanguageSpeakerComponent.cs | 8 + .../Components/VisualNameComponent.cs | 4 + .../Components/WhisperMuffleComponent.cs | 14 + .../ChatChannelWhitelistSystem.cs | 44 + .../EntitySystems/ChildLanguageSystem.cs | 32 + .../EntitySystems/LanguageRelaySystem.cs | 53 + .../LineOfSightLanguageSystem.cs | 37 + .../EntitySystems/MinimumFluencySystem.cs | 35 + .../EntitySystems/ReplaceSpeakerNameSystem.cs | 16 + .../EntitySystems/SharedLanguageSystem.API.cs | 436 +++++++ .../EntitySystems/SharedLanguageSystem.cs | 192 ++++ .../SharedSyllableScramblingSystem.cs | 3 + .../SpeechTransformableSystem.cs | 35 + .../EntitySystems/TranslatorSystem.cs | 196 ++++ .../EntitySystems/VisualNameSystem.cs | 23 + .../EntitySystems/WhisperMuffleSystem.cs | 63 ++ .../_DEN/Language/HideFontsMessage.cs | 14 + .../_DEN/Language/LanguageEvents.cs | 83 ++ .../Prototypes/LanguageFluencyPrototype.cs | 42 + .../Language/Prototypes/LanguagePrototype.cs | 98 ++ .../Prototypes/LanguageWrapperPrototype.cs | 30 + .../Mobs/Systems/MobStateSystem.Language.cs | 26 + .../_DEN/Speech/ListenEvent.Language.cs | 35 + .../_DEN/Speech/SpeakLanguageAttemptEvent.cs | 13 + .../_DEN/Speech/SpeechSystem.Language.cs | 17 + .../Telephone/TelephoneComponent.Language.cs | 9 + .../TriggerOnVoiceComponent.Language.cs | 13 + .../Systems/TriggerSystem.Voice.Language.cs | 67 ++ .../_DEN/chat/language/chat-language.ftl | 38 + .../Locale/en-US/_DEN/commands/language.ftl | 8 + .../Locale/en-US/_DEN/datasets/words.ftl | 1001 +++++++++++++++++ .../_DEN/escape-menu/ui/options-menu.ftl | 2 + .../en-US/_DEN/language/language-basic.ftl | 3 + .../_DEN/language/language-debug-parent.ftl | 11 + .../_DEN/language/language-fluencies.ftl | 6 + .../en-US/_DEN/language/language-sign.ftl | 18 + .../_DEN/language/language-telepathy.ftl | 4 + .../en-US/_DEN/language/language-ui.ftl | 13 + .../_DEN/language/language-universal.ftl | 3 + .../en-US/_DEN/language/language-xeno.ftl | 10 + Resources/Prototypes/Body/species_base.yml | 4 + .../Prototypes/Entities/Mobs/NPCs/xeno.yml | 6 + .../Entities/Mobs/Player/observer.yml | 1 + .../Structures/Machines/vending_machines.yml | 8 + Resources/Prototypes/_DEN/Datasets/words.yml | 5 + .../Objects/Devices/translator_implants.yml | 11 + .../Entities/Objects/Devices/translators.yml | 46 + .../Objects/Misc/translator_implanters.yml | 13 + Resources/Prototypes/_DEN/Language/basic.yml | 260 +++++ .../_DEN/Language/debug-related.yml | 57 + .../Prototypes/_DEN/Language/fluency.yml | 30 + Resources/Prototypes/_DEN/Language/sign.yml | 54 + .../Prototypes/_DEN/Language/telepathy.yml | 26 + .../Prototypes/_DEN/Language/universal.yml | 3 + .../_DEN/Language/wrappers/wrappers.yml | 39 + Resources/Prototypes/_DEN/Language/xeno.yml | 31 + Resources/Prototypes/_DEN/tags.yml | 3 + .../_DEN/Interface/language-solid-full.svg | 1 + .../language-solid-full.svg.192dpi.png | Bin 0 -> 1512 bytes .../Objects/Devices/translator.rsi/icon.png | Bin 0 -> 278 bytes .../Objects/Devices/translator.rsi/meta.json | 17 + .../Devices/translator.rsi/translator.png | Bin 0 -> 202 bytes Resources/keybinds.yml | 4 + 174 files changed, 7068 insertions(+), 107 deletions(-) create mode 100644 Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs create mode 100644 Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml create mode 100644 Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs create mode 100644 Content.Client/_DEN/UserInterface/Systems/Language/Controls/LanguageContainer.cs create mode 100644 Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs create mode 100644 Content.Client/_DEN/UserInterface/Systems/Language/Windows/LanguageWindow.xaml create mode 100644 Content.Client/_DEN/UserInterface/Systems/Language/Windows/LanguageWindow.xaml.cs create mode 100644 Content.Server/_DEN/Animals/Systems/ParrotMemorySystem.Language.cs create mode 100644 Content.Server/_DEN/Chat/SharedChatEvents.Language.cs create mode 100644 Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs create mode 100644 Content.Server/_DEN/Holopad/HolopadSystem.Language.cs create mode 100644 Content.Server/_DEN/Language/Commands/LanguageCommand.cs create mode 100644 Content.Server/_DEN/Language/Commands/SetLanguageCommand.cs create mode 100644 Content.Server/_DEN/Language/EntitySystems/GestaltSystem.cs create mode 100644 Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs create mode 100644 Content.Server/_DEN/Language/EntitySystems/SyllableScramblingSystem.cs create mode 100644 Content.Server/_DEN/Language/EntitySystems/UniversalLanguageSpeakerSystem.cs create mode 100644 Content.Server/_DEN/Radio/EntitySystems/HeadsetSystem.Language.cs create mode 100644 Content.Server/_DEN/Radio/EntitySystems/RadioDeviceSystem.Language.cs create mode 100644 Content.Server/_DEN/Radio/EntitySystems/RadioSystem.Language.cs create mode 100644 Content.Server/_DEN/Radio/RadioEvent.Language.cs create mode 100644 Content.Server/_DEN/Speech/EntitySystems/BlockListeningSystem.Language.cs create mode 100644 Content.Server/_DEN/Speech/EntitySystems/ListeningSystem.Language.cs create mode 100644 Content.Server/_DEN/Speech/Muting/MutingSystem.Language.cs create mode 100644 Content.Server/_DEN/Speech/SpeechSoundSystem.Language.cs create mode 100644 Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.Language.cs create mode 100644 Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.Language.cs create mode 100644 Content.Server/_DEN/Telephone/TelephoneEvents.cs create mode 100644 Content.Server/_DEN/Telephone/TelephoneSystem.Language.cs create mode 100644 Content.Server/_DEN/VendingMachines/VendingMachineSystem.Language.cs create mode 100644 Content.Server/_DEN/Vocalization/Systems/DatasetVocalizationSystem.Language.cs create mode 100644 Content.Server/_DEN/Vocalization/Systems/RadioVocalizationSystem.Language.cs create mode 100644 Content.Server/_DEN/Vocalization/Systems/VocalizationSystem.Language.cs create mode 100644 Content.Shared/_DEN/ActionBlocker/ActionBlockerSystem.Language.cs create mode 100644 Content.Shared/_DEN/Administration/AdminFrozenSystem.Language.cs create mode 100644 Content.Shared/_DEN/Bed/Sleep/SleepingSystem.Language.cs create mode 100644 Content.Shared/_DEN/CCVars/DenCCVars.cs create mode 100644 Content.Shared/_DEN/Chat/SharedChatSystem.Language.cs create mode 100644 Content.Shared/_DEN/Chat/TypingIndicator/SharedTypingIndicatorSystem.Language.cs create mode 100644 Content.Shared/_DEN/Hands/EntitySystems/SharedHandsSystem.Relay.Language.cs create mode 100644 Content.Shared/_DEN/Implants/SharedSubdermalImplantSystem.Relays.Language.cs create mode 100644 Content.Shared/_DEN/Inventory/InventorySystem.Relay.Languages.cs create mode 100644 Content.Shared/_DEN/Language/Components/AudibleComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/ChatChannelWhitelistComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/ChildLanguageComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/GestaltComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/GestaltHostComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/LanguageCommunicatorComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/LanguageComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/LanguageFontSuppressionComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/LineOfSightLanguageComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/MinimumFluencyComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/RadioTransmittableComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/ReplaceSpeakerNameComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/SpeechTransformableComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/SyllableScramblingComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/TranslatedLanguageComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/TranslatorComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/UnconsciousLanguageComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/UniversalLanguageSpeakerComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/VisualNameComponent.cs create mode 100644 Content.Shared/_DEN/Language/Components/WhisperMuffleComponent.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/ChatChannelWhitelistSystem.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/ChildLanguageSystem.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/LanguageRelaySystem.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/LineOfSightLanguageSystem.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/MinimumFluencySystem.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/ReplaceSpeakerNameSystem.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/SharedSyllableScramblingSystem.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/SpeechTransformableSystem.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/TranslatorSystem.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/VisualNameSystem.cs create mode 100644 Content.Shared/_DEN/Language/EntitySystems/WhisperMuffleSystem.cs create mode 100644 Content.Shared/_DEN/Language/HideFontsMessage.cs create mode 100644 Content.Shared/_DEN/Language/LanguageEvents.cs create mode 100644 Content.Shared/_DEN/Language/Prototypes/LanguageFluencyPrototype.cs create mode 100644 Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs create mode 100644 Content.Shared/_DEN/Language/Prototypes/LanguageWrapperPrototype.cs create mode 100644 Content.Shared/_DEN/Mobs/Systems/MobStateSystem.Language.cs create mode 100644 Content.Shared/_DEN/Speech/ListenEvent.Language.cs create mode 100644 Content.Shared/_DEN/Speech/SpeakLanguageAttemptEvent.cs create mode 100644 Content.Shared/_DEN/Speech/SpeechSystem.Language.cs create mode 100644 Content.Shared/_DEN/Telephone/TelephoneComponent.Language.cs create mode 100644 Content.Shared/_DEN/Trigger/Components/Triggers/TriggerOnVoiceComponent.Language.cs create mode 100644 Content.Shared/_DEN/Trigger/Systems/TriggerSystem.Voice.Language.cs create mode 100644 Resources/Locale/en-US/_DEN/chat/language/chat-language.ftl create mode 100644 Resources/Locale/en-US/_DEN/commands/language.ftl create mode 100644 Resources/Locale/en-US/_DEN/datasets/words.ftl create mode 100644 Resources/Locale/en-US/_DEN/escape-menu/ui/options-menu.ftl create mode 100644 Resources/Locale/en-US/_DEN/language/language-basic.ftl create mode 100644 Resources/Locale/en-US/_DEN/language/language-debug-parent.ftl create mode 100644 Resources/Locale/en-US/_DEN/language/language-fluencies.ftl create mode 100644 Resources/Locale/en-US/_DEN/language/language-sign.ftl create mode 100644 Resources/Locale/en-US/_DEN/language/language-telepathy.ftl create mode 100644 Resources/Locale/en-US/_DEN/language/language-ui.ftl create mode 100644 Resources/Locale/en-US/_DEN/language/language-universal.ftl create mode 100644 Resources/Locale/en-US/_DEN/language/language-xeno.ftl create mode 100644 Resources/Prototypes/_DEN/Datasets/words.yml create mode 100644 Resources/Prototypes/_DEN/Entities/Objects/Devices/translator_implants.yml create mode 100644 Resources/Prototypes/_DEN/Entities/Objects/Devices/translators.yml create mode 100644 Resources/Prototypes/_DEN/Entities/Objects/Misc/translator_implanters.yml create mode 100644 Resources/Prototypes/_DEN/Language/basic.yml create mode 100644 Resources/Prototypes/_DEN/Language/debug-related.yml create mode 100644 Resources/Prototypes/_DEN/Language/fluency.yml create mode 100644 Resources/Prototypes/_DEN/Language/sign.yml create mode 100644 Resources/Prototypes/_DEN/Language/telepathy.yml create mode 100644 Resources/Prototypes/_DEN/Language/universal.yml create mode 100644 Resources/Prototypes/_DEN/Language/wrappers/wrappers.yml create mode 100644 Resources/Prototypes/_DEN/Language/xeno.yml create mode 100644 Resources/Textures/_DEN/Interface/language-solid-full.svg create mode 100644 Resources/Textures/_DEN/Interface/language-solid-full.svg.192dpi.png create mode 100644 Resources/Textures/_DEN/Objects/Devices/translator.rsi/icon.png create mode 100644 Resources/Textures/_DEN/Objects/Devices/translator.rsi/meta.json create mode 100644 Resources/Textures/_DEN/Objects/Devices/translator.rsi/translator.png diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 01e7dc367c3..cce17f50c00 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -65,6 +65,7 @@ public static void SetupContexts(IInputContextContainer contexts) human.AddFunction(ContentKeyFunctions.AltUseItemInHand); human.AddFunction(ContentKeyFunctions.OpenCharacterMenu); human.AddFunction(ContentKeyFunctions.OpenEmotesMenu); + human.AddFunction(ContentKeyFunctions.OpenLanguageMenu); // DEN: Languages human.AddFunction(ContentKeyFunctions.ActivateItemInWorld); human.AddFunction(ContentKeyFunctions.ThrowItemInHand); human.AddFunction(ContentKeyFunctions.AltActivateItemInWorld); diff --git a/Content.Client/Options/UI/OptionsMenu.xaml b/Content.Client/Options/UI/OptionsMenu.xaml index 21c4b64ce8f..69d49a03d33 100644 --- a/Content.Client/Options/UI/OptionsMenu.xaml +++ b/Content.Client/Options/UI/OptionsMenu.xaml @@ -1,5 +1,6 @@ @@ -9,5 +10,6 @@ + diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index f1041eb4166..a3987e781c1 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -232,6 +232,7 @@ void AddToggleCvarCheckBox(string checkBoxName, CVarDef cvar) AddButton(ContentKeyFunctions.OpenAHelp); AddButton(ContentKeyFunctions.OpenActionsMenu); AddButton(ContentKeyFunctions.OpenEmotesMenu); + AddButton(ContentKeyFunctions.OpenLanguageMenu); // DEN: Languages AddButton(ContentKeyFunctions.ToggleRoundEndSummaryWindow); AddButton(ContentKeyFunctions.OpenEntitySpawnWindow); AddButton(ContentKeyFunctions.OpenSandboxWindow); diff --git a/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs b/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs index e314310bc0c..a47b68fb950 100644 --- a/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs +++ b/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs @@ -1,3 +1,4 @@ +using Content.Client._DEN.UserInterface.Systems.Language; using Content.Client.UserInterface.Systems.Actions; using Content.Client.UserInterface.Systems.Admin; using Content.Client.UserInterface.Systems.Bwoink; @@ -24,6 +25,7 @@ public sealed class GameTopMenuBarUIController : UIController [Dependency] private readonly SandboxUIController _sandbox = default!; [Dependency] private readonly GuidebookUIController _guidebook = default!; [Dependency] private readonly EmotesUIController _emotes = default!; + [Dependency] private readonly LanguageUIController _language = default!; // DEN: Languages private GameTopMenuBar? GameTopMenuBar => UIManager.GetActiveUIWidgetOrNull(); @@ -47,6 +49,7 @@ public void UnloadButtons() _action.UnloadButton(); _sandbox.UnloadButton(); _emotes.UnloadButton(); + _language.UnloadButton(); // DEN: Languages } public void LoadButtons() @@ -60,5 +63,6 @@ public void LoadButtons() _action.LoadButton(); _sandbox.LoadButton(); _emotes.LoadButton(); + _language.LoadButton(); // DEN: Languages } } diff --git a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml index 8bfd1bebca0..8027b3a7222 100644 --- a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml +++ b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml @@ -73,6 +73,17 @@ HorizontalExpand="True" AppendStyleClass="{x:Static style:StyleClass.ButtonSquare}" /> + + >? OnLanguageEntityUpdate; + public event Action?>? OnLanguageCommunicatorUpdate; + + public override void Initialize() + { + base.Initialize(); + + _cfg.OnValueChanged(DenCCVars.HideLanguageFonts, SetHideLanguageFonts); + _playerManager.LocalPlayerAttached += OnLocalPlayerAttached; + + SubscribeLocalEvent(OnLanguageComponentHandleState); + SubscribeLocalEvent(OnLanguageCommunicatorHandleState); + } + + private void OnLocalPlayerAttached(EntityUid newEntity) + { + RaiseNetworkEvent(new HideFontsMessage(_cfg.GetCVar(DenCCVars.HideLanguageFonts))); + } + + private void SetHideLanguageFonts(bool hide) + { + RaiseNetworkEvent(new HideFontsMessage(hide)); + } + + public void TrySetSpokenLanguage(Entity lang) + { + if (_playerManager.LocalEntity is not { } localEnt || + !TryComp(localEnt, out var localComm)) + return; + + var request = new RequestSetSpokenLanguageEvent(GetNetEntity(lang)); + RaiseNetworkEvent(request); + + OnLanguageCommunicatorUpdate?.Invoke(lang); + } + + private void OnLanguageComponentHandleState(Entity ent, ref AfterAutoHandleStateEvent evt) + { + LanguageUpdated(ent); + } + + private void OnLanguageCommunicatorHandleState(Entity ent, + ref AfterAutoHandleStateEvent evt) + { + if (_playerManager.LocalEntity == ent) + { + var currLang = GetCurrentLanguageEntity(ent); + OnLanguageCommunicatorUpdate?.Invoke(currLang); + } + } + + protected override void OnLanguageRemoved(Entity holder, Entity language) + { + if (_playerManager.LocalEntity == holder) + { + OnLanguageEntityUpdate?.Invoke(language); + } + } + + public Entity? GetLocalCommunicator() + { + if (_playerManager.LocalEntity is { } localEnt && TryComp(localEnt, out var localCommunicator)) + return (localEnt, localCommunicator); + + return null; + } + + public override void OnLanguageUpdated(Entity lang) + { + if (!Resolve(lang, ref lang.Comp)) + return; + + LanguageUpdated((lang, lang.Comp)); + } + + private void LanguageUpdated(Entity ent) + { + if (_playerManager.LocalEntity is { } localEnt && localEnt == ent.Comp.Holder) + { + OnLanguageEntityUpdate?.Invoke(ent); + } + } +} diff --git a/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml new file mode 100644 index 00000000000..559134d3828 --- /dev/null +++ b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs new file mode 100644 index 00000000000..b9e3a68da31 --- /dev/null +++ b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs @@ -0,0 +1,17 @@ +using Content.Shared._DEN.CCVars; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._DEN.Options.UI.Tabs; + +[GenerateTypedNameReferences] +public sealed partial class DenTab : Control +{ + public DenTab() + { + RobustXamlLoader.Load(this); + + Control.AddOptionCheckBox(DenCCVars.HideLanguageFonts, HideLanguageFonts); + } +} diff --git a/Content.Client/_DEN/UserInterface/Systems/Language/Controls/LanguageContainer.cs b/Content.Client/_DEN/UserInterface/Systems/Language/Controls/LanguageContainer.cs new file mode 100644 index 00000000000..9aebcae771c --- /dev/null +++ b/Content.Client/_DEN/UserInterface/Systems/Language/Controls/LanguageContainer.cs @@ -0,0 +1,146 @@ +using Content.Client._DEN.Language.EntitySystems; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Examine; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client._DEN.UserInterface.Systems.Language.Controls; + +public sealed class LanguageContainer : Control +{ + private IEntityManager _entities; + private IPlayerManager _player; + private IPrototypeManager _proto; + private LanguageSystem _language; + + public event Action? SpeakPressed; + + public Entity? LanguageEnt { get; private set; } + + private Label _languageName; + private Button _languageButton; + private RichTextLabel _description; + private BoxContainer _header; + + public LanguageContainer(IEntityManager entities, IPlayerManager player, IPrototypeManager proto, LanguageSystem language) + { + _entities = entities; + _player = player; + _proto = proto; + _language = language; + + var container = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Vertical, + HorizontalExpand = true, + }; + + _header = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; + + _languageName = new Label + { + Name = "LanguageName", + HorizontalExpand = true, + }; + + _languageButton = new Button { Text = Loc.GetString("language-ui-speak-language") }; + _languageButton.OnPressed += _ => SpeakPressed?.Invoke(); + + _header.AddChild(_languageName); + _header.AddChild(_languageButton); + + container.AddChild(_header); + + var cbody = new CollapsibleBody + { + HorizontalExpand = false, + Margin = new Thickness(4f, 4f), + }; + + var body = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Vertical, + HorizontalExpand = true, + }; + + _description = new RichTextLabel { HorizontalExpand = true }; + body.AddChild(_description); + cbody.AddChild(body); + + var collapsible = new Collapsible(Loc.GetString("language-ui-language-description"), cbody) + { + Orientation = BoxContainer.LayoutOrientation.Vertical, + HorizontalExpand = true, + }; + container.AddChild(collapsible); + + var wrapper = new PanelContainer + { + Margin = new Thickness(4f), + }; + wrapper.StyleClasses.Add("PdaBorderRect"); + wrapper.AddChild(container); + + AddChild(wrapper); + + SpeakPressed += OnLanguageChosen; + } + + public void UpdateLanguage(Entity language) + { + LanguageEnt = language; + UpdateData(); + } + + public void SetCurrentSpoken(bool current) + { + if (current) + { + if (_header.Children.Contains(_languageButton)) + _header.RemoveChild(_languageButton); + } + else + { + if (!_header.Children.Contains(_languageButton)) + _header.AddChild(_languageButton); + } + } + + private void UpdateData() + { + if (LanguageEnt == null || _player.LocalEntity == null) + return; + + var langProto = _proto.Index(LanguageEnt.Value.Comp.Language); + var fluencyProto = _proto.Index(LanguageEnt.Value.Comp.Fluency); + + _languageName.Text = langProto.LocalizedName; + + _languageButton.Disabled = !LanguageEnt.Value.Comp.Speaks; + + var desc = FormattedMessage.FromMarkupPermissive( + Loc.GetString("language-ui-language-fluency", + ("fluency", Loc.GetString(fluencyProto.Name)), + ("color", Color.InterpolateBetween(Color.Red, Color.Green, (float)(fluencyProto.Understanding / 100.0))))); + desc.PushNewline(); + desc.AddMarkupPermissive(langProto.LocalizedDescription); + + var ev = new ExaminedEvent(desc, LanguageEnt.Value, _player.LocalEntity.Value, true, !desc.IsEmpty); + _entities.EventBus.RaiseLocalEvent(LanguageEnt.Value, ev); + + _description.SetMessage(ev.GetTotalMessage()); + } + + private void OnLanguageChosen() + { + if (LanguageEnt != null) + _language.TrySetSpokenLanguage(LanguageEnt.Value); + } +} diff --git a/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs b/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs new file mode 100644 index 00000000000..cfbd23264a6 --- /dev/null +++ b/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs @@ -0,0 +1,300 @@ +using System.Linq; +using Content.Client._DEN.Language.EntitySystems; +using Content.Client._DEN.UserInterface.Systems.Language.Controls; +using Content.Client._DEN.UserInterface.Systems.Language.Windows; +using Content.Client.CrewManifest.UI; +using Content.Client.Gameplay; +using Content.Client.UserInterface.Controls; +using Content.Client.UserInterface.Systems.MenuBar.Widgets; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Input; +using Content.Shared.Interaction.Events; +using JetBrains.Annotations; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controllers; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Input.Binding; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client._DEN.UserInterface.Systems.Language; + +[UsedImplicitly] +public sealed class LanguageUIController : UIController, IOnStateChanged, IOnSystemChanged +{ + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IEntityManager _entities = default!; + + [UISystemDependency] private readonly LanguageSystem _languageSystem = default!; + + private MenuButton? LanguageButton => + UIManager.GetActiveUIWidgetOrNull()?.LanguageButton; + + private LanguageWindow? _window; + + private Dictionary _languageContainers = new(); + + public void UnloadButton() + { + if (LanguageButton == null) + return; + + LanguageButton.Pressed = false; + LanguageButton.OnPressed -= LanguageButtonPressed; + } + + public void LoadButton() + { + if (LanguageButton == null) + return; + + LanguageButton.OnPressed += LanguageButtonPressed; + } + + private void LanguageButtonPressed(BaseButton.ButtonEventArgs args) + { + ToggleWindow(); + } + + private void ToggleWindow() + { + if (_window == null) + return; + + if (_window.IsOpen) + { + _window.Close(); + return; + } + + _window.Open(); + } + + private void OnLanguageCommunicatorUpdated(Entity? currentLang) + { + if (_window == null) + return; + + LanguageContainer? speakingContainer = null; + if (_window.CurrentlySpeaking.ChildCount > 0 && + _window.CurrentlySpeaking.Children.First() is LanguageContainer languageContainer) + { + speakingContainer = languageContainer; + } + + if (speakingContainer is not null) + { + if (speakingContainer.LanguageEnt == currentLang) + { + speakingContainer.SetCurrentSpoken(true); + return; + } + + speakingContainer.SetCurrentSpoken(false); + if (_window.CurrentlySpeaking.Children.Contains(speakingContainer)) + _window.CurrentlySpeaking.RemoveChild(speakingContainer); + if (!_window.LanguageList.Children.Contains(speakingContainer)) + _window.LanguageList.AddChild(speakingContainer); + } + + if (currentLang is { } currLangEnt) + { + if (_languageContainers.TryGetValue(currLangEnt, out var container)) + { + container.SetCurrentSpoken(true); + if (_window.LanguageList.Children.Contains(container)) + { + _window.LanguageList.RemoveChild(container); + } + + if (!_window.CurrentlySpeaking.Children.Contains(container)) + { + _window.CurrentlySpeaking.AddChild(container); + } + } + else + { + var newCont = new LanguageContainer(_entities, _playerManager, _prototypeManager, _languageSystem); + newCont.UpdateLanguage(currLangEnt); + newCont.SetCurrentSpoken(true); + _window.CurrentlySpeaking.AddChild(newCont); + _languageContainers.Add(currLangEnt, newCont); + } + } + + SortChildLanguages(); + } + + private void OnLanguageUpdated(Entity langEnt) + { + // If Window is ever null and being re-created we should be doing a full rebuild anyway. + if (_window == null) + return; + + if (_languageContainers.TryGetValue(langEnt, out var container)) + { + if (_languageSystem.GetLocalCommunicator() is not { } localComm) + return; + + if (localComm.Comp.Languages is { } langs && !langs.Contains(langEnt) && _window is not null) + { + if(_window.LanguageList.Children.Contains(container)) + _window.LanguageList.RemoveChild(container); + else if(_window.CurrentlySpeaking.Children.Contains(container)) + _window.CurrentlySpeaking.RemoveChild(container); + } + else + { + container.UpdateLanguage(langEnt); + } + } + else + { + var newCont = new LanguageContainer(_entities, _playerManager, _prototypeManager, _languageSystem); + newCont.UpdateLanguage(langEnt); + _window.LanguageList.AddChild(newCont); + _languageContainers.Add(langEnt, newCont); + } + + SortChildLanguages(); + } + + private void SortChildLanguages() + { + if (_window == null) + return; + + var children = _window.LanguageList.Children.OfType().ToList(); + _window.LanguageList.RemoveAllChildren(); + children.Sort((p, q) => + { + if (p.LanguageEnt is null) + return -1; + + if (q.LanguageEnt is null) + return 1; + + return string.Compare(p.LanguageEnt.Value.Comp.Language.Id, + q.LanguageEnt.Value.Comp.Language.Id, + StringComparison.CurrentCulture); + }); + foreach (var child in children) + { + _window.LanguageList.AddChild(child); + } + } + + public override void FrameUpdate(FrameEventArgs args) + { + if (_window is { NeedsFullRebuild: true }) + RebuildWindow(); + } + + private void RebuildWindow() + { + if (_window == null) + return; + + var player = _playerManager.LocalEntity; + if (player == null) + return; + + _window.NeedsFullRebuild = false; + + _languageContainers.Clear(); + _window.CurrentlySpeaking.RemoveAllChildren(); + _window.LanguageList.RemoveAllChildren(); + + var speakingContainer = new LanguageContainer(_entities, _playerManager, _prototypeManager, _languageSystem); + _window.CurrentlySpeaking.AddChild(speakingContainer); + speakingContainer.SetCurrentSpoken(true); + + if (_languageSystem.TryGetLanguageEntities(player.Value, out var languages) + && _languageSystem.GetCurrentLanguageEntity(player.Value) is { } currentLanguageEnt) + { + languages.Remove(currentLanguageEnt); + + speakingContainer.UpdateLanguage(currentLanguageEnt); + _languageContainers.Add(currentLanguageEnt, speakingContainer); + + // Languages in the UI are sorted by their localized name, just to add some semblance of stability. + languages.Sort((entity1, entity2) => + { + var langProto1 = _prototypeManager.Index(entity1.Comp.Language); + var langProto2 = _prototypeManager.Index(entity2.Comp.Language); + + return string.Compare(langProto1.LocalizedName, langProto2.LocalizedName, StringComparison.CurrentCulture); + }); + + foreach (var language in languages) + { + var langCont = new LanguageContainer(_entities, _playerManager, _prototypeManager, _languageSystem); + langCont.UpdateLanguage(language); + _window.LanguageList.AddChild(langCont); + _languageContainers.Add(language, langCont); + } + } + } + + private void NeedsFullRebuild() + { + if (_window != null) + _window.NeedsFullRebuild = true; + } + + private void OnPlayerAttached(EntityUid uid) + { + NeedsFullRebuild(); + } + + private void DeactivateButton() + { + LanguageButton?.Pressed = false; + } + + private void ActivateButton() + { + LanguageButton?.Pressed = true; + } + + public void OnStateEntered(GameplayState state) + { + _window = UIManager.CreateWindow(); + _window.OnClose += DeactivateButton; + _window.OnOpen += ActivateButton; + + CommandBinds.Builder + .Bind(ContentKeyFunctions.OpenLanguageMenu, + InputCmdHandler.FromDelegate(_ => ToggleWindow())) + .Register(); + + NeedsFullRebuild(); + } + + public void OnStateExited(GameplayState state) + { + if (_window != null) + { + _window.Close(); + _window = null; + } + + CommandBinds.Unregister(); + } + + public void OnSystemLoaded(LanguageSystem system) + { + system.OnLanguageEntityUpdate += OnLanguageUpdated; + system.OnLanguageCommunicatorUpdate += OnLanguageCommunicatorUpdated; + _playerManager.LocalPlayerAttached += OnPlayerAttached; + } + + public void OnSystemUnloaded(LanguageSystem system) + { + system.OnLanguageEntityUpdate -= OnLanguageUpdated; + system.OnLanguageCommunicatorUpdate -= OnLanguageCommunicatorUpdated; + _playerManager.LocalPlayerAttached -= OnPlayerAttached; + } +} diff --git a/Content.Client/_DEN/UserInterface/Systems/Language/Windows/LanguageWindow.xaml b/Content.Client/_DEN/UserInterface/Systems/Language/Windows/LanguageWindow.xaml new file mode 100644 index 00000000000..7755817e843 --- /dev/null +++ b/Content.Client/_DEN/UserInterface/Systems/Language/Windows/LanguageWindow.xaml @@ -0,0 +1,19 @@ + + + + + + + + + Rest is filled in by LanguageUIController + + + + diff --git a/Content.Client/_DEN/UserInterface/Systems/Language/Windows/LanguageWindow.xaml.cs b/Content.Client/_DEN/UserInterface/Systems/Language/Windows/LanguageWindow.xaml.cs new file mode 100644 index 00000000000..c41c2755e16 --- /dev/null +++ b/Content.Client/_DEN/UserInterface/Systems/Language/Windows/LanguageWindow.xaml.cs @@ -0,0 +1,16 @@ +using Content.Client.UserInterface.Controls; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._DEN.UserInterface.Systems.Language.Windows; + +[GenerateTypedNameReferences] +public sealed partial class LanguageWindow : FancyWindow +{ + public bool NeedsFullRebuild; + + public LanguageWindow() + { + RobustXamlLoader.Load(this); + } +} diff --git a/Content.Server/Animals/Systems/ParrotMemorySystem.cs b/Content.Server/Animals/Systems/ParrotMemorySystem.cs index 6d8192f5d51..b83b1f25031 100644 --- a/Content.Server/Animals/Systems/ParrotMemorySystem.cs +++ b/Content.Server/Animals/Systems/ParrotMemorySystem.cs @@ -42,10 +42,12 @@ public override void Initialize() SubscribeLocalEvent(ListenerOnMapInit); - SubscribeLocalEvent(OnListen); - SubscribeLocalEvent(OnHeadsetReceive); + //SubscribeLocalEvent(OnListen); // DEN: Languages, see ListenLanguageEvent + //SubscribeLocalEvent(OnHeadsetReceive); // DEN: Languages, see HeadsetRadioReceiveLanguageRelayEvent - SubscribeLocalEvent(OnTryVocalize); + //SubscribeLocalEvent(OnTryVocalize); // DEN: Languages, see TryVocalizeLanguageEvent + + InitializeLanguage(); // DEN: Languages } private void OnErase(ref EraseEvent args) @@ -60,12 +62,14 @@ private void ListenerOnMapInit(Entity entity, ref MapIn Log.Warning($"Entity {ToPrettyString(entity)} has a ParrotListenerComponent but was not given an ActiveListenerComponent"); } + [Obsolete("Use OnLanguageListen instead.")] // DEN: Languages private void OnListen(Entity entity, ref ListenEvent args) { TryLearn(entity.Owner, args.Message, args.Source); } + [Obsolete("Use OnHeadsetReceiveLanguage instead.")] // DEN: Languages private void OnHeadsetReceive(Entity entity, ref HeadsetRadioReceiveRelayEvent args) { var message = args.RelayedEvent.Message; @@ -78,6 +82,7 @@ private void OnHeadsetReceive(Entity entity, ref Headse /// Called when an entity with a ParrotMemoryComponent tries to vocalize. /// This function picks a message from memory and sets the event to handled /// + [Obsolete("Use OnTryVocalizeLanguage instead.", true)] // DEN: Languages private void OnTryVocalize(Entity entity, ref TryVocalizeEvent args) { // return if this was already handled @@ -142,15 +147,17 @@ public void TryLearn(Entity en return; // actually commit this message to memory - Learn((entity, entity.Comp1), message, source); + //Learn((entity, entity.Comp1), message, source); DEN: Languages } + /* DEN: Languages /// /// Actually learn a message and commit it to memory /// /// Entity learning a new word /// Message to learn /// Source EntityUid of the message + [Obsolete("Use LearnLanguage instead.")] // DEN: Languages private void Learn(Entity entity, string message, EntityUid source) { // log a low-priority chat type log to the admin logger @@ -176,6 +183,7 @@ private void Learn(Entity entity, string message, EntityU var replaceIdx = _random.Next(entity.Comp.SpeechMemories.Count); entity.Comp.SpeechMemories[replaceIdx] = newMemory; } + */ /// /// Delete all messages from a specified player on all ParrotMemoryComponents diff --git a/Content.Server/Anomaly/AnomalySystem.Generator.cs b/Content.Server/Anomaly/AnomalySystem.Generator.cs index 09af0419bbd..2c4540f9bda 100644 --- a/Content.Server/Anomaly/AnomalySystem.Generator.cs +++ b/Content.Server/Anomaly/AnomalySystem.Generator.cs @@ -176,7 +176,7 @@ private void OnGeneratingFinished(EntityUid uid, AnomalyGeneratorComponent compo Audio.PlayPvs(component.GeneratingFinishedSound, uid); var message = Loc.GetString("anomaly-generator-announcement"); - _radio.SendRadioMessage(uid, message, _prototype.Index(component.ScienceChannel), uid); + _radio.SendLanguageRadioMessage(uid, message, _prototype.Index(component.ScienceChannel), uid); // DEN: Languages } private void UpdateGenerator() diff --git a/Content.Server/Cargo/Systems/CargoSystem.Funds.cs b/Content.Server/Cargo/Systems/CargoSystem.Funds.cs index bc6571c0cc5..1fe4bc2efc9 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Funds.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Funds.cs @@ -65,7 +65,7 @@ private void OnWithdrawFunds(Entity ent, ref CargoCo ("amount", args.Amount), ("name1", Loc.GetString(ourAccount.Name)), ("code1", Loc.GetString(ourAccount.Code))); - _radio.SendRadioMessage(ent, msg, ourAccount.RadioChannel, ent, escapeMarkup: false); + _radio.SendLanguageRadioMessage(ent, msg, ourAccount.RadioChannel, ent, escapeMarkup: false); // DEN: Languages } } else @@ -82,8 +82,8 @@ private void OnWithdrawFunds(Entity ent, ref CargoCo ("code1", Loc.GetString(ourAccount.Code)), ("name2", Loc.GetString(otherAccount.Name)), ("code2", Loc.GetString(otherAccount.Code))); - _radio.SendRadioMessage(ent, msg, ourAccount.RadioChannel, ent, escapeMarkup: false); - _radio.SendRadioMessage(ent, msg, otherAccount.RadioChannel, ent, escapeMarkup: false); + _radio.SendLanguageRadioMessage(ent, msg, ourAccount.RadioChannel, ent, escapeMarkup: false); // DEN: Languages + _radio.SendLanguageRadioMessage(ent, msg, otherAccount.RadioChannel, ent, escapeMarkup: false); // DEN: Languages } } } diff --git a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs index 0b5f0155937..40fdf0e7f51 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs @@ -242,9 +242,9 @@ private void OnApproveOrderMessage(EntityUid uid, CargoOrderConsoleComponent com ("orderAmount", order.OrderQuantity), ("approver", order.Approver ?? string.Empty), ("cost", cost)); - _radio.SendRadioMessage(uid, message, account.RadioChannel, uid, escapeMarkup: false); + _radio.SendLanguageRadioMessage(uid, message, account.RadioChannel, uid, escapeMarkup: false); // DEN: Languages if (CargoOrderConsoleComponent.BaseAnnouncementChannel != account.RadioChannel) - _radio.SendRadioMessage(uid, message, CargoOrderConsoleComponent.BaseAnnouncementChannel, uid, escapeMarkup: false); + _radio.SendLanguageRadioMessage(uid, message, CargoOrderConsoleComponent.BaseAnnouncementChannel, uid, escapeMarkup: false); // DEN: Languages } ConsolePopup(args.Actor, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(ev.FulfillmentEntity.Value).EntityName))); diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 9ea04ca3bce..d85cefa213e 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -21,7 +21,6 @@ using Content.Shared.Players.RateLimiting; using Content.Shared.Radio; using Content.Shared.Station.Components; -using Content.Shared.Whitelist; using Robust.Server.Player; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; @@ -190,45 +189,62 @@ public override void TrySendInGameICMessage( message = message[1..]; } - bool shouldCapitalize = (desiredType != InGameICChatType.Emote); + bool shouldCapitalize = (desiredType == InGameICChatType.Emote); bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation); // Capitalizing the word I only happens in English, so we check language here bool shouldCapitalizeTheWordI = (!CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Parent.Name == "en") || (CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Name == "en"); - message = SanitizeInGameICMessage(source, message, out var emoteStr, shouldCapitalize, shouldPunctuate, shouldCapitalizeTheWordI); + // DEN: Detailed message system. + bool needsRadio = false; + RadioChannelPrototype? channel = null; + // We want to do this processing before we try to parse it into a complex message. + if (checkRadioPrefix) + { + if (TryProcessRadioMessage(source, message, out var modMessage, out channel)) + { + needsRadio = true; + message = modMessage; + } + } + var complexMessage = ConvertMessageToComplex(message); + complexMessage = SanitizeComplexMessage(source, complexMessage, out var emoteStrs, shouldCapitalize, shouldPunctuate, shouldCapitalizeTheWordI); - // Was there an emote in the message? If so, send it. - if (player != null && emoteStr != message && emoteStr != null) + // DEN: Send emote strings extracted from the complex message. + // Multiple emotes in multiple dialogs works. I hope no one ever actually does this. + if (player != null && emoteStrs.Count != 0) { - SendEntityEmote(source, emoteStr, range, nameOverride, ignoreActionBlocker); + foreach (var emoteStr in emoteStrs) + { + SendEntityEmote(source, emoteStr, range, nameOverride, ignoreActionBlocker); + } } - // This can happen if the entire string is sanitized out. - if (string.IsNullOrEmpty(message)) + // DEN: Complex message will be empty, rather than a null string. + if (complexMessage.Parts.Count == 0) return; - // This message may have a radio prefix, and should then be whispered to the resolved radio channel - if (checkRadioPrefix) + // DEN: Complex message parsing. + if (needsRadio) { - if (TryProcessRadioMessage(source, message, out var modMessage, out var channel)) - { - SendEntityWhisper(source, modMessage, range, channel, nameOverride, hideLog, ignoreActionBlocker); - return; - } + SendEntityComplexSpeech(source, complexMessage, WhisperWrapper, ChatChannel.Whisper, range, channel, nameOverride, hideLog, ignoreActionBlocker); + return; } // Otherwise, send whatever type. switch (desiredType) { case InGameICChatType.Speak: - SendEntitySpeak(source, message, range, nameOverride, hideLog, ignoreActionBlocker); + // DEN: Complex Speech and language + SendEntityComplexSpeech(source, complexMessage, SpeakWrapper, ChatChannel.Local, range, null, nameOverride, hideLog, ignoreActionBlocker); break; case InGameICChatType.Whisper: - SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker); + // DEN: Complex Speech and language + SendEntityComplexSpeech(source, complexMessage, WhisperWrapper, ChatChannel.Whisper, range, null, nameOverride, hideLog, ignoreActionBlocker); break; case InGameICChatType.Emote: - SendEntityEmote(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker); + // DEN: Complex Speech. + SendEntityComplexEmote(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker); break; } } @@ -368,6 +384,7 @@ public override void DispatchStationAnnouncement( #region Private API + [Obsolete("Use SendEntityComplexSpeech instead.", true)] // DEN: Languages private void SendEntitySpeak( EntityUid source, string originalMessage, @@ -440,6 +457,7 @@ private void SendEntitySpeak( } } + [Obsolete("Use SendEntityComplexSpeech instead.", true)] // DEN: Languages private void SendEntityWhisper( EntityUid source, string originalMessage, @@ -830,26 +848,6 @@ public readonly record struct ICChatRecipientData(float Range, bool Observer, bo { } - private string ObfuscateMessageReadability(string message, float chance) - { - var modifiedMessage = new StringBuilder(message); - - for (var i = 0; i < message.Length; i++) - { - if (char.IsWhiteSpace((modifiedMessage[i]))) - { - continue; - } - - if (_random.Prob(1 - chance)) - { - modifiedMessage[i] = '~'; - } - } - - return modifiedMessage.ToString(); - } - public string BuildGibberishString(IReadOnlyList charOptions, int length) { var sb = new StringBuilder(); diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs index 09415d02e3a..79148d06dbb 100644 --- a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs +++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs @@ -173,10 +173,10 @@ private void OnChangeStatus(Entity ent, ref Cri // this is impossible _ => "not-wanted" }; - _radio.SendRadioMessage(ent, + _radio.SendLanguageRadioMessage(ent, Loc.GetString($"criminal-records-console-{statusString}", args), ent.Comp.SecurityChannel, - ent); + ent); // DEN: Languages _adminLogger.Add(LogType.Identity, LogImpact.Low, $"{ToPrettyString(mob.Value):name} changed criminal status for {name} to \"{statusString}\""); diff --git a/Content.Server/Holopad/HolopadSystem.cs b/Content.Server/Holopad/HolopadSystem.cs index c2aaf827dae..9e5b91ad3b8 100644 --- a/Content.Server/Holopad/HolopadSystem.cs +++ b/Content.Server/Holopad/HolopadSystem.cs @@ -27,7 +27,7 @@ namespace Content.Server.Holopad; -public sealed class HolopadSystem : SharedHolopadSystem +public sealed partial class HolopadSystem : SharedHolopadSystem // DEN: Make partial { [Dependency] private readonly TelephoneSystem _telephoneSystem = default!; [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; @@ -63,7 +63,7 @@ public override void Initialize() SubscribeLocalEvent(OnTelephoneStateChange); SubscribeLocalEvent(OnHoloCallCommenced); SubscribeLocalEvent(OnHoloCallEnded); - SubscribeLocalEvent(OnTelephoneMessageSent); + //SubscribeLocalEvent(OnTelephoneMessageSent); // DEN: Languages // Networked events SubscribeNetworkEvent(OnTypingChanged); @@ -83,6 +83,7 @@ public override void Initialize() SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnMobStateChanged); + InitializeLanguage(); // DEN: Languages } #region: Holopad UI bound user interface messages @@ -287,6 +288,7 @@ private void OnHoloCallEnded(Entity entity, ref TelephoneCallE _userInterfaceSystem.CloseUi(entity.Owner, HolopadUiKey.AiRequestWindow, insertedAi); } + [Obsolete("Use OnTelephoneMessageLanguageSent instead.", true)] // DEN: Languages private void OnTelephoneMessageSent(Entity holopad, ref TelephoneMessageSentEvent args) { LinkHolopadToUser(holopad, args.MessageSource); diff --git a/Content.Server/Lathe/LatheSystem.cs b/Content.Server/Lathe/LatheSystem.cs index cce17590e86..bc095efacb5 100644 --- a/Content.Server/Lathe/LatheSystem.cs +++ b/Content.Server/Lathe/LatheSystem.cs @@ -408,7 +408,7 @@ private void OnTechnologyDatabaseModified(Entity ent, foreach (var channel in ent.Comp.Channels) { - _radio.SendRadioMessage(ent.Owner, message, channel, ent.Owner, escapeMarkup: false); + _radio.SendLanguageRadioMessage(ent.Owner, message, channel, ent.Owner, escapeMarkup: false); // DEN: Languages } } diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index 7d16687d5f2..7f217f21f0d 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -8,7 +8,7 @@ namespace Content.Server.Radio.EntitySystems; -public sealed class HeadsetSystem : SharedHeadsetSystem +public sealed partial class HeadsetSystem : SharedHeadsetSystem // DEN: Make partial { [Dependency] private readonly INetManager _netMan = default!; [Dependency] private readonly RadioSystem _radio = default!; @@ -16,10 +16,11 @@ public sealed class HeadsetSystem : SharedHeadsetSystem public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnHeadsetReceive); + //SubscribeLocalEvent(OnHeadsetReceive); // DEN: Languages, see RadioReceiveLanguageEvent SubscribeLocalEvent(OnKeysChanged); - SubscribeLocalEvent(OnSpeak); + InitializeLanguage(); // DEN: Languages + //SubscribeLocalEvent(OnSpeak); // DEN: Languages, see EntitySpokeLanguageEvent } private void OnKeysChanged(EntityUid uid, HeadsetComponent component, EncryptionChannelsChangedEvent args) @@ -42,6 +43,7 @@ private void UpdateRadioChannels(EntityUid uid, HeadsetComponent headset, Encryp EnsureComp(uid).Channels = new(keyHolder.Channels); } + [Obsolete("Use OnSpeakLanguage instead.")] // DEN: Languages private void OnSpeak(EntityUid uid, WearingHeadsetComponent component, EntitySpokeEvent args) { if (args.Channel != null @@ -95,6 +97,7 @@ public void SetEnabled(EntityUid uid, bool value, HeadsetComponent? component = } } + [Obsolete("Use OnHeadsetReceiveLanguage instead.", true)] // DEN: Languages private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref RadioReceiveEvent args) { // TODO: change this when a code refactor is done diff --git a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs index 68067ae6f5e..b1a5783cfe8 100644 --- a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs @@ -19,7 +19,7 @@ namespace Content.Server.Radio.EntitySystems; /// /// This system handles radio speakers and microphones (which together form a hand-held radio). /// -public sealed class RadioDeviceSystem : SharedRadioDeviceSystem +public sealed partial class RadioDeviceSystem : SharedRadioDeviceSystem // DEN: Make partial { [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly PopupSystem _popup = default!; @@ -37,18 +37,20 @@ public override void Initialize() SubscribeLocalEvent(OnMicrophoneInit); SubscribeLocalEvent(OnExamine); SubscribeLocalEvent(OnActivateMicrophone); - SubscribeLocalEvent(OnListen); - SubscribeLocalEvent(OnAttemptListen); + //SubscribeLocalEvent(OnListen); // DEN: Languages + //SubscribeLocalEvent(OnAttemptListen); // DEN: Languages SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnSpeakerInit); SubscribeLocalEvent(OnActivateSpeaker); - SubscribeLocalEvent(OnReceiveRadio); + // SubscribeLocalEvent(OnReceiveRadio); // DEN: Languages SubscribeLocalEvent(OnIntercomEncryptionChannelsChanged); SubscribeLocalEvent(OnToggleIntercomMic); SubscribeLocalEvent(OnToggleIntercomSpeaker); SubscribeLocalEvent(OnSelectIntercomChannel); + + InitializeLanguage(); // DEN: Languages } public override void Update(float frameTime) @@ -149,6 +151,7 @@ private void OnExamine(EntityUid uid, RadioMicrophoneComponent component, Examin } } + [Obsolete("Use OnListenLanguage instead.", true)] // DEN: Languages private void OnListen(EntityUid uid, RadioMicrophoneComponent component, ListenEvent args) { if (HasComp(args.Source)) @@ -159,6 +162,7 @@ private void OnListen(EntityUid uid, RadioMicrophoneComponent component, ListenE _radio.SendRadioMessage(args.Source, args.Message, channel, uid); } + [Obsolete("Use OnAttemptListenLanguage instead.", true)] // DEN: Languages private void OnAttemptListen(EntityUid uid, RadioMicrophoneComponent component, ListenAttemptEvent args) { if (component.PowerRequired && !this.IsPowered(uid, EntityManager) @@ -168,6 +172,7 @@ private void OnAttemptListen(EntityUid uid, RadioMicrophoneComponent component, } } + [Obsolete("Use OnReceiveLanguageRadio instead.", true)] // DEN: Languages private void OnReceiveRadio(EntityUid uid, RadioSpeakerComponent component, ref RadioReceiveEvent args) { if (uid == args.RadioSource) diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 740e6b10303..da3e91e8c2c 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -19,7 +19,7 @@ namespace Content.Server.Radio.EntitySystems; /// /// This system handles intrinsic radios and the general process of converting radio messages into chat messages. /// -public sealed class RadioSystem : EntitySystem +public sealed partial class RadioSystem : EntitySystem // DEN: Make partial { [Dependency] private readonly INetManager _netMan = default!; [Dependency] private readonly IReplayRecordingManager _replay = default!; @@ -36,12 +36,15 @@ public sealed class RadioSystem : EntitySystem public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnIntrinsicReceive); - SubscribeLocalEvent(OnIntrinsicSpeak); + //SubscribeLocalEvent(OnIntrinsicReceive); // DEN: Languages, see RadioReceiveLanguageEvent + //SubscribeLocalEvent(OnIntrinsicSpeak); // DEN: Languages, see EntitySpokeLanguageEvent _exemptQuery = GetEntityQuery(); + + InitializeLanguage(); // DEN: Languages } + [Obsolete("Use OnIntrinsicSpeakLanguage instead.")] // DEN: Languages private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent component, EntitySpokeEvent args) { if (args.Channel != null && component.Channels.Contains(args.Channel.ID)) @@ -51,6 +54,7 @@ private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent } } + [Obsolete("Use OnIntrinsicLanguageReceive instead.")] // DEN: Languages private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent component, ref RadioReceiveEvent args) { if (TryComp(uid, out ActorComponent? actor)) @@ -60,6 +64,7 @@ private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent c /// /// Send radio message to all active radio listeners /// + [Obsolete("Use SendLanguageRadioMessage instead.", true)] // DEN: Languages public void SendRadioMessage(EntityUid messageSource, string message, ProtoId channel, EntityUid radioSource, bool escapeMarkup = true) { SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup); @@ -70,6 +75,7 @@ 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 + [Obsolete("Use SendLanguageRadioMessage instead.", true)] // DEN: Languages public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, bool escapeMarkup = true) { // TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this. diff --git a/Content.Server/Radio/RadioEvent.cs b/Content.Server/Radio/RadioEvent.cs index 49ff63f824e..05f264997f3 100644 --- a/Content.Server/Radio/RadioEvent.cs +++ b/Content.Server/Radio/RadioEvent.cs @@ -3,12 +3,14 @@ namespace Content.Server.Radio; +[Obsolete("Use RadioReceiveLanguageEvent instead.", true)] // DEN: Languages [ByRefEvent] public readonly record struct RadioReceiveEvent(string Message, EntityUid MessageSource, RadioChannelPrototype Channel, EntityUid RadioSource, MsgChatMessage ChatMsg); /// /// Event raised on the parent entity of a headset radio when a radio message is received /// +[Obsolete("Use HeadsetRadioReceiveLanguageRelayEvent instead.", true)] // DEN: Languages [ByRefEvent] public readonly record struct HeadsetRadioReceiveRelayEvent(RadioReceiveEvent RelayedEvent); diff --git a/Content.Server/Research/Systems/ResearchSystem.Console.cs b/Content.Server/Research/Systems/ResearchSystem.Console.cs index baaf06ea714..3ccf37cac7d 100644 --- a/Content.Server/Research/Systems/ResearchSystem.Console.cs +++ b/Content.Server/Research/Systems/ResearchSystem.Console.cs @@ -55,7 +55,7 @@ private void OnConsoleUnlock(EntityUid uid, ResearchConsoleComponent component, ("amount", technologyPrototype.Cost), ("approver", getIdentityEvent.Title ?? string.Empty) ); - _radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false); + _radio.SendLanguageRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false); // DEN: Languages } SyncClientWithServer(uid); diff --git a/Content.Server/Robotics/Systems/RoboticsConsoleSystem.cs b/Content.Server/Robotics/Systems/RoboticsConsoleSystem.cs index 560d8174aa3..e3ea2c51277 100644 --- a/Content.Server/Robotics/Systems/RoboticsConsoleSystem.cs +++ b/Content.Server/Robotics/Systems/RoboticsConsoleSystem.cs @@ -136,7 +136,7 @@ private void OnDestroy(Entity ent, ref RoboticsConsole _deviceNetwork.QueuePacket(ent, args.Address, payload); var message = Loc.GetString(ent.Comp.DestroyMessage, ("name", data.Name)); - _radio.SendRadioMessage(ent, message, ent.Comp.RadioChannel, ent); + _radio.SendLanguageRadioMessage(ent, message, ent.Comp.RadioChannel, ent); // DEN: Languages _adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(args.Actor):user} destroyed borg {data.Name} with address {args.Address}"); ent.Comp.NextDestroy = now + ent.Comp.DestroyCooldown; diff --git a/Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs b/Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs index 587fa9c4f3b..f28cb906cd8 100644 --- a/Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs +++ b/Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs @@ -185,7 +185,7 @@ public bool TryCompleteSalvageJob(Entity ent, ProtoId< while (computerQuery.MoveNext(out var uid, out _)) { var message = Loc.GetString("job-board-radio-announce", ("rank", FormattedMessage.RemoveMarkupPermissive(Loc.GetString(newRank.Title)))); - _radio.SendRadioMessage(uid, message, UnlockChannel, uid, false); + _radio.SendLanguageRadioMessage(uid, message, UnlockChannel, uid, false); // DEN: Languages break; } diff --git a/Content.Server/Salvage/SalvageSystem.cs b/Content.Server/Salvage/SalvageSystem.cs index 53bb0c06b3e..a5cd928eb4e 100644 --- a/Content.Server/Salvage/SalvageSystem.cs +++ b/Content.Server/Salvage/SalvageSystem.cs @@ -65,7 +65,7 @@ private void Report(EntityUid source, string channelName, string messageKey, par { var message = args.Length == 0 ? Loc.GetString(messageKey) : Loc.GetString(messageKey, args); var channel = _prototypeManager.Index(channelName); - _radioSystem.SendRadioMessage(source, message, channel, source); + _radioSystem.SendLanguageRadioMessage(source, message, channel, source); // DEN: Languages } public override void Update(float frameTime) diff --git a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs index 84a2307f589..bb53519fc0d 100644 --- a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs +++ b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs @@ -319,7 +319,7 @@ private void AlertRadio(Entity ent, string type) var message = Loc.GetString("emitter-" + type + "-broadcast", ("location", FormattedMessage.RemoveMarkupOrThrow(_navMap.GetNearestBeaconString(ent.Owner))) ); - _radio.SendRadioMessage(ent.Owner, message, ent.Comp.RadioChannel, ent.Owner); + _radio.SendLanguageRadioMessage(ent.Owner, message, ent.Comp.RadioChannel, ent.Owner); // DEN: Languages } private void OnLockToggled(Entity ent, ref LockToggledEvent args) diff --git a/Content.Server/Speech/Components/ListenWireAction.cs b/Content.Server/Speech/Components/ListenWireAction.cs index f9f1d9e92ea..80bed3aa8de 100644 --- a/Content.Server/Speech/Components/ListenWireAction.cs +++ b/Content.Server/Speech/Components/ListenWireAction.cs @@ -85,7 +85,7 @@ public override void Pulse(EntityUid user, Wire wire) // The reason for the override is to make the voice sound like its coming from electrity rather than the intercom. voiceOverrideComp.NameOverride = Loc.GetString("wire-listen-pulse-identifier"); voiceOverrideComp.Enabled = true; - _radio.SendRadioMessage(wire.Owner, noiseMsg, _protoMan.Index(radioMicroPhoneComp.BroadcastChannel), wire.Owner); + _radio.SendLanguageRadioMessage(wire.Owner, noiseMsg, _protoMan.Index(radioMicroPhoneComp.BroadcastChannel), wire.Owner); // DEN: Languages voiceOverrideComp.Enabled = false; base.Pulse(user, wire); diff --git a/Content.Server/Speech/EntitySystems/BlockListeningSystem.cs b/Content.Server/Speech/EntitySystems/BlockListeningSystem.cs index 6b098775125..5425599b533 100644 --- a/Content.Server/Speech/EntitySystems/BlockListeningSystem.cs +++ b/Content.Server/Speech/EntitySystems/BlockListeningSystem.cs @@ -3,15 +3,18 @@ namespace Content.Server.Speech.EntitySystems; -public sealed class BlockListeningSystem : EntitySystem +public sealed partial class BlockListeningSystem : EntitySystem // DEN: Make Partial { public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnListenAttempt); + // SubscribeLocalEvent(OnListenAttempt); // DEN: Languages, see ListenLanguageAttemptEvent + + InitializeLanguage(); // DEN: Languages } + [Obsolete("See OnListenLanguageAttempt", true)] // DEN: Languages private void OnListenAttempt(EntityUid uid, BlockListeningComponent component, ListenAttemptEvent args) { args.Cancel(); diff --git a/Content.Server/Speech/EntitySystems/ListeningSystem.cs b/Content.Server/Speech/EntitySystems/ListeningSystem.cs index 35cb7f0eb45..82d43cf8710 100644 --- a/Content.Server/Speech/EntitySystems/ListeningSystem.cs +++ b/Content.Server/Speech/EntitySystems/ListeningSystem.cs @@ -8,21 +8,26 @@ namespace Content.Server.Speech.EntitySystems; /// /// This system redirects local chat messages to listening entities (e.g., radio microphones). /// -public sealed class ListeningSystem : EntitySystem +public sealed partial class ListeningSystem : EntitySystem // DEN: Make partial { [Dependency] private readonly SharedTransformSystem _xforms = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnSpeak); + + InitializeLanguage(); // DEN: Languages + + //SubscribeLocalEvent(OnSpeak); // DEN: Languages, see EntitySpokeLanguageEvent } + [Obsolete("Use OnSpeakLanguage instead.", true)] // DEN: Languages private void OnSpeak(EntitySpokeEvent ev) { PingListeners(ev.Source, ev.Message, ev.ObfuscatedMessage); } + [Obsolete("Use PingListenersLanguage instead.", true)] // DEN: Languages public void PingListeners(EntityUid source, string message, string? obfuscatedMessage) { // TODO whispering / audio volume? Microphone sensitivity? diff --git a/Content.Server/Speech/Muting/MutingSystem.cs b/Content.Server/Speech/Muting/MutingSystem.cs index 9e2a0602a43..d1a504dae23 100644 --- a/Content.Server/Speech/Muting/MutingSystem.cs +++ b/Content.Server/Speech/Muting/MutingSystem.cs @@ -9,15 +9,17 @@ namespace Content.Server.Speech.Muting { - public sealed class MutingSystem : EntitySystem + public sealed partial class MutingSystem : EntitySystem // DEN: Make Partial { [Dependency] private readonly PopupSystem _popupSystem = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnSpeakAttempt); + //SubscribeLocalEvent(OnSpeakAttempt); // DEN: Languages, see SpeakLanguageAttemptEvent SubscribeLocalEvent(OnEmote, before: new[] { typeof(VocalSystem), typeof(MumbleAccentSystem) }); SubscribeLocalEvent(OnScreamAction, before: new[] { typeof(VocalSystem) }); + + InitializeLanguage(); // DEN: Languages } private void OnEmote(EntityUid uid, MutedComponent component, ref EmoteEvent args) @@ -44,6 +46,7 @@ private void OnScreamAction(EntityUid uid, MutedComponent component, ScreamActio } + [Obsolete("Use OnSpeakLanguageAttempt instead", true)] // DEN: Languages private void OnSpeakAttempt(EntityUid uid, MutedComponent component, SpeakAttemptEvent args) { // TODO something better than this. diff --git a/Content.Server/Speech/SpeechNoiseSystem.cs b/Content.Server/Speech/SpeechNoiseSystem.cs index cc9d5feb607..17943b35103 100644 --- a/Content.Server/Speech/SpeechNoiseSystem.cs +++ b/Content.Server/Speech/SpeechNoiseSystem.cs @@ -8,7 +8,7 @@ namespace Content.Server.Speech { - public sealed class SpeechSoundSystem : EntitySystem + public sealed partial class SpeechSoundSystem : EntitySystem // DEN: Make partial { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; @@ -19,7 +19,9 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnEntitySpoke); + InitializeLanguage(); // DEN: Languages + + //SubscribeLocalEvent(OnEntitySpoke); // DEN: Languages, see EntitySpokeLanguageEvent } public SoundSpecifier? GetSpeechSound(Entity ent, string message) @@ -56,6 +58,7 @@ public override void Initialize() return contextSound; } + [Obsolete("Use OnEntitySpokeLanguage instead", true)] // DEN: Languages private void OnEntitySpoke(EntityUid uid, SpeechComponent component, EntitySpokeEvent args) { if (component.SpeechSounds == null) diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs index e8c53de9ebf..cb4701f9f4e 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs @@ -8,7 +8,7 @@ namespace Content.Server.SurveillanceCamera; -public sealed class SurveillanceCameraMicrophoneSystem : EntitySystem +public sealed partial class SurveillanceCameraMicrophoneSystem : EntitySystem // DEN: Make Partial { [Dependency] private readonly SharedTransformSystem _xforms = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; @@ -16,9 +16,11 @@ public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(RelayEntityMessage); - SubscribeLocalEvent(CanListen); + //SubscribeLocalEvent(RelayEntityMessage); // DEN: Languages, see ListenLanguageEvent + //SubscribeLocalEvent(CanListen); // DEN: Language, see ListenLanguageAttemptEvent SubscribeLocalEvent(OnExpandRecipients); + + InitializeLanguage(); // DEN: Languages } private void OnExpandRecipients(ExpandICChatRecipientsEvent ev) @@ -59,6 +61,7 @@ private void OnInit(EntityUid uid, SurveillanceCameraMicrophoneComponent compone RemCompDeferred(uid); } + [Obsolete("Use CanListenLanguage instead.", true)] // DEN: Languages public void CanListen(EntityUid uid, SurveillanceCameraMicrophoneComponent microphone, ListenAttemptEvent args) { // TODO maybe just make this a part of ActiveListenerComponent? @@ -66,6 +69,7 @@ public void CanListen(EntityUid uid, SurveillanceCameraMicrophoneComponent micro args.Cancel(); } + [Obsolete("Use RelayEntityLanguageMessage instead.", true)] // DEN: Languages public void RelayEntityMessage(EntityUid uid, SurveillanceCameraMicrophoneComponent component, ListenEvent args) { if (!TryComp(uid, out SurveillanceCameraComponent? camera)) @@ -96,6 +100,7 @@ public void SetEnabled(EntityUid uid, bool value, SurveillanceCameraMicrophoneCo } } +[Obsolete("Use SurveillanceCameraSpeechLanguageSendEvent instead.", true)] // DEN: Languages public sealed class SurveillanceCameraSpeechSendEvent : EntityEventArgs { public EntityUid Speaker { get; } diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs index 581ac197197..f966d56a732 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs @@ -10,7 +10,7 @@ namespace Content.Server.SurveillanceCamera; /// /// This handles speech for surveillance camera monitors. /// -public sealed class SurveillanceCameraSpeakerSystem : EntitySystem +public sealed partial class SurveillanceCameraSpeakerSystem : EntitySystem // DEN: Partial (this actually just completely replaces this system, oops?) { [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly SpeechSoundSystem _speechSound = default!; @@ -20,9 +20,12 @@ public sealed class SurveillanceCameraSpeakerSystem : EntitySystem /// public override void Initialize() { - SubscribeLocalEvent(OnSpeechSent); + //SubscribeLocalEvent(OnSpeechSent); // DEN: Languages, see SurveillanceCameraSpeechLanguageSendEvent (sorry) + + InitializeLanguage(); // DEN: Languages } + [Obsolete("Use OnSpeechLanguageSent instead.", true)] // DEN: Languages private void OnSpeechSent(EntityUid uid, SurveillanceCameraSpeakerComponent component, SurveillanceCameraSpeechSendEvent args) { diff --git a/Content.Server/Telephone/TelephoneSystem.cs b/Content.Server/Telephone/TelephoneSystem.cs index 0e3090c77eb..22f2cfdf22f 100644 --- a/Content.Server/Telephone/TelephoneSystem.cs +++ b/Content.Server/Telephone/TelephoneSystem.cs @@ -24,7 +24,7 @@ namespace Content.Server.Telephone; -public sealed class TelephoneSystem : SharedTelephoneSystem +public sealed partial class TelephoneSystem : SharedTelephoneSystem // DEN: Make Partial { [Dependency] private readonly AppearanceSystem _appearanceSystem = default!; [Dependency] private readonly InteractionSystem _interaction = default!; @@ -46,9 +46,11 @@ public override void Initialize() SubscribeLocalEvent(OnComponentShutdown); SubscribeLocalEvent(OnPowerChanged); - SubscribeLocalEvent(OnAttemptListen); - SubscribeLocalEvent(OnListen); - SubscribeLocalEvent(OnTelephoneMessageReceived); + //SubscribeLocalEvent(OnAttemptListen); // DEN: Languages, see ListenLanguageAttemptEvent + //SubscribeLocalEvent(OnListen); // DEN: Languages, see ListenLanguageEvent + //SubscribeLocalEvent(OnTelephoneMessageReceived); // DEN: Languages, see TelephoneMessageReceivedEvent + + InitializeLanguage(); // DEN: Languages } #region: Events @@ -64,6 +66,7 @@ private void OnPowerChanged(Entity entity, ref PowerChangedE TerminateTelephoneCalls(entity); } + [Obsolete("Use OnAttemptLanguageListen instead.", true)] // DEN: Languages private void OnAttemptListen(Entity entity, ref ListenAttemptEvent args) { if (!IsTelephonePowered(entity) || @@ -75,6 +78,7 @@ private void OnAttemptListen(Entity entity, ref ListenAttemp } } + [Obsolete("Use OnLanguageListen instead.", true)] // DEN: Languages private void OnListen(Entity entity, ref ListenEvent args) { if (args.Source == entity.Owner) @@ -91,6 +95,7 @@ private void OnListen(Entity entity, ref ListenEvent args) SendTelephoneMessage(args.Source, args.Message, entity); } + [Obsolete("Use OnTelephoneMessageLanguageReceived instead.", true)] // DEN: Languages private void OnTelephoneMessageReceived(Entity entity, ref TelephoneMessageReceivedEvent args) { // Prevent message feedback loops @@ -327,6 +332,7 @@ private void HandleEndingTelephoneCalls(Entity entity, Telep SetTelephoneMicrophoneState(entity, false); } + [Obsolete("Use SendTelephoneLanguageMessage instead.", true)] // DEN: Languages private void SendTelephoneMessage(EntityUid messageSource, string message, Entity source, bool escapeMarkup = true) { // This method assumes that you've already checked that this diff --git a/Content.Server/Trigger/Systems/RattleOnTriggerSystem.cs b/Content.Server/Trigger/Systems/RattleOnTriggerSystem.cs index 963ac36b7f4..69bae03428e 100644 --- a/Content.Server/Trigger/Systems/RattleOnTriggerSystem.cs +++ b/Content.Server/Trigger/Systems/RattleOnTriggerSystem.cs @@ -44,6 +44,6 @@ private void OnTrigger(Entity ent, ref TriggerEvent ar var message = Loc.GetString(messageId, ("user", target.Value), ("position", posText)); // Sends a message to the radio channel specified by the implant - _radio.SendRadioMessage(ent.Owner, message, _prototypeManager.Index(ent.Comp.RadioChannel), ent.Owner); + _radio.SendLanguageRadioMessage(ent.Owner, message, _prototypeManager.Index(ent.Comp.RadioChannel), ent.Owner); // DEN: Languages } } diff --git a/Content.Server/VendingMachines/VendingMachineSystem.cs b/Content.Server/VendingMachines/VendingMachineSystem.cs index 1c283783937..521dd907162 100644 --- a/Content.Server/VendingMachines/VendingMachineSystem.cs +++ b/Content.Server/VendingMachines/VendingMachineSystem.cs @@ -16,7 +16,7 @@ namespace Content.Server.VendingMachines { - public sealed class VendingMachineSystem : SharedVendingMachineSystem + public sealed partial class VendingMachineSystem : SharedVendingMachineSystem // DEN: Made Partial { [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly PricingSystem _pricing = default!; @@ -31,11 +31,13 @@ public override void Initialize() SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnVendingPrice); - SubscribeLocalEvent(OnTryVocalize); + // SubscribeLocalEvent(OnTryVocalize); // DEN: Languages, see TryVocalizeLanguageEvent SubscribeLocalEvent(OnSelfDispense); SubscribeLocalEvent(OnPriceCalculation); + + InitializeLanguage(); // DEN: Languages } private void OnVendingPrice(EntityUid uid, VendingMachineComponent component, ref PriceCalculationEvent args) @@ -240,6 +242,7 @@ private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent co args.Price += priceSets.Max(); } + [Obsolete("Use TryVocalizeLanguageEvent instead.", true)] // DEN: Languages private void OnTryVocalize(Entity ent, ref TryVocalizeEvent args) { args.Cancelled |= ent.Comp.Broken; diff --git a/Content.Server/Vocalization/Systems/DatasetVocalizationSystem.cs b/Content.Server/Vocalization/Systems/DatasetVocalizationSystem.cs index fcca5605f42..eac30f8fa0f 100644 --- a/Content.Server/Vocalization/Systems/DatasetVocalizationSystem.cs +++ b/Content.Server/Vocalization/Systems/DatasetVocalizationSystem.cs @@ -6,7 +6,7 @@ namespace Content.Server.Vocalization.Systems; /// -public sealed class DatasetVocalizationSystem : EntitySystem +public sealed partial class DatasetVocalizationSystem : EntitySystem // DEN: Made partial { [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly IRobustRandom _random = default!; @@ -15,9 +15,12 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnTryVocalize); + //SubscribeLocalEvent(OnTryVocalize); // DEN: Languages, see TryVocalizeLanguageEvent + + InitializeLanguage(); // DEN: Languages } + [Obsolete("Obsolete, use OnTryVocalizeLanguage instead.", true)] // DEN: Languages private void OnTryVocalize(Entity ent, ref TryVocalizeEvent args) { if (args.Handled) diff --git a/Content.Server/Vocalization/Systems/RadioVocalizationSystem.cs b/Content.Server/Vocalization/Systems/RadioVocalizationSystem.cs index 00f6b7bbd1a..ca69a8fd430 100644 --- a/Content.Server/Vocalization/Systems/RadioVocalizationSystem.cs +++ b/Content.Server/Vocalization/Systems/RadioVocalizationSystem.cs @@ -23,12 +23,14 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnVocalize); + //SubscribeLocalEvent(OnVocalize); // DEN: Languages, see VocalizeLanguageEvent + InitializeLanguage(); // DEN: Languages } /// /// Called whenever an entity with a VocalizerComponent tries to speak /// + [Obsolete("Obsolete, use OnVocalizeLanguage instead.", true)] // DEN: Languages private void OnVocalize(Entity entity, ref VocalizeEvent args) { if (args.Handled) diff --git a/Content.Server/Vocalization/Systems/VocalizationSystem.cs b/Content.Server/Vocalization/Systems/VocalizationSystem.cs index 8801e057b7e..b997ce1f8f7 100644 --- a/Content.Server/Vocalization/Systems/VocalizationSystem.cs +++ b/Content.Server/Vocalization/Systems/VocalizationSystem.cs @@ -25,7 +25,8 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnRequiresPowerTryVocalize); + //SubscribeLocalEvent(OnRequiresPowerTryVocalize); // DEN: Languages, see TryVocalizeLanguageEvent + InitializeLanguage(); // DEN: Languages } private void OnMapInit(Entity ent, ref MapInitEvent args) @@ -33,6 +34,7 @@ private void OnMapInit(Entity ent, ref MapInitEvent args) ent.Comp.NextVocalizeInterval = _random.Next(ent.Comp.MinVocalizeInterval, ent.Comp.MaxVocalizeInterval); } + [Obsolete("Use OnRequiresPowerTryVocalizeLanguage instead.", true)] // DEN: Languages private void OnRequiresPowerTryVocalize(Entity ent, ref TryVocalizeEvent args) { if (!TryComp(ent, out var receiver)) @@ -45,6 +47,7 @@ private void OnRequiresPowerTryVocalize(Entity /// Try speaking by raising a TryVocalizeEvent /// This event is passed to systems adding a message to it and setting it to handled /// + [Obsolete("Use TrySpeakLanguage instead.", true)] // DEN: Languages private void TrySpeak(Entity entity) { var tryVocalizeEvent = new TryVocalizeEvent(); @@ -70,6 +73,7 @@ private void TrySpeak(Entity entity) /// /// Actually say something. /// + [Obsolete("Use SpeakLanguage instead.", true)] // DEN: Languages private void Speak(Entity entity, string message) { // raise a VocalizeEvent @@ -115,7 +119,7 @@ public override void Update(float frameTime) vocalizer.NextVocalizeInterval = _gameTiming.CurTime + randomSpeakInterval; // try to speak - TrySpeak((uid, vocalizer)); + TrySpeakLanguage((uid, vocalizer)); // DEN: Languages } } } @@ -125,6 +129,7 @@ public override void Update(float frameTime) /// /// Message to send, this is null when the event is just fired and should be set by a system /// Whether the message was handled by a system +[Obsolete("Use TryVocalizeLanguageEvent instead.", true)] // DEN: Languages [ByRefEvent] public record struct TryVocalizeEvent(string? Message = null, bool Handled = false, bool Cancelled = false); @@ -134,5 +139,6 @@ public record struct TryVocalizeEvent(string? Message = null, bool Handled = fal /// /// Message to send /// Whether the message was handled by a system +[Obsolete("Use VocalizeLanguageEvent instead.", true)] // DEN: Languages [ByRefEvent] public record struct VocalizeEvent(string Message, bool Handled = false); diff --git a/Content.Server/_DEN/Animals/Systems/ParrotMemorySystem.Language.cs b/Content.Server/_DEN/Animals/Systems/ParrotMemorySystem.Language.cs new file mode 100644 index 00000000000..c8ee9a7ad2d --- /dev/null +++ b/Content.Server/_DEN/Animals/Systems/ParrotMemorySystem.Language.cs @@ -0,0 +1,157 @@ +using System.Linq; +using Content.Server.Animals.Components; +using Content.Server.Radio; +using Content.Server.Vocalization.Systems; +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Language.EntitySystems; +using Content.Shared._DEN.Speech; +using Content.Shared.Animals.Components; +using Content.Shared.Chat; +using Content.Shared.Database; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Animals.Systems; + +public sealed partial class ParrotMemorySystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedLanguageSystem _language = default!; + + private EntityQuery _audibleQuery; + + private void InitializeLanguage() + { + _audibleQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnLanguageListen); + SubscribeLocalEvent(OnHeadsetReceiveLanguage); + SubscribeLocalEvent(OnTryVocalizeLanguage); + } + + private void OnLanguageListen(Entity entity, ref ListenLanguageEvent args) + { + if (args.Channel != ChatChannel.Local) + return; + TryLearnLanguage(entity.Owner, args.LanguageEnt, args.Message, args.Source); + } + + private void OnHeadsetReceiveLanguage(Entity entity, + ref HeadsetRadioReceiveLanguageRelayEvent args) + { + var message = args.RelayedEvent.Message; + var languageEnt = args.RelayedEvent.LanguageEnt; + var source = args.RelayedEvent.MessageSource; + + TryLearnLanguage(entity.Owner, languageEnt, message, source); + } + + private void OnTryVocalizeLanguage(Entity entity, ref TryVocalizeLanguageEvent args) + { + if (args.Handled) + return; + + if (entity.Comp.SpeechMemories.Count == 0) + return; + + var memory = _random.Pick(entity.Comp.SpeechMemories); + + args.Message = memory.Message; + var language = GetEntity(memory.Language); + if (TryComp(language, out var langComp)) + args.Language = (language, langComp); + + args.Handled = true; + } + + private void TryLearnLanguage(Entity entity, + Entity languageEnt, + ComplexChatMessage incomingMessage, + EntityUid source) + { + if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2)) + return; + + if (!_audibleQuery.HasComponent(languageEnt)) + return; + + if (!_whitelist.CheckBoth(source, entity.Comp2.Blacklist, entity.Comp2.Whitelist)) + return; + + if (source.Equals(entity) || _mobState.IsIncapacitated(entity)) + return; + + if (_gameTiming.CurTime < entity.Comp1.NextLearnInterval) + return; + + var dialogParts = incomingMessage.Parts.Where(part => part.Item1 == ChatPart.Dialog) + .Select(part => part.Item2) + .ToList(); + + if (dialogParts.Count == 0) + return; + + var message = _random.Pick(dialogParts).Trim(); + + if (string.IsNullOrWhiteSpace(message)) + return; + + if (message.Length < entity.Comp1.MinEntryLength || message.Length > entity.Comp1.MaxEntryLength) + return; + + entity.Comp1.NextLearnInterval = _gameTiming.CurTime + entity.Comp1.LearnCooldown; + + if (!_random.Prob(entity.Comp1.LearnChance)) + return; + + var language = _proto.Index(languageEnt.Comp.Language); + + LearnLanguage((entity, entity.Comp1), message, language, source); + } + + private void LearnLanguage(Entity entity, + string message, + LanguagePrototype language, + EntityUid source) + { + var languageName = Loc.GetString(language.Name); + + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Parroting entity {ToPrettyString(entity):entity} learned the phrase \"{message}\" in {languageName} from {ToPrettyString(source):speaker}"); + + NetUserId? sourceNetUserId = null; + if (_mind.TryGetMind(source, out _, out var mind)) + { + sourceNetUserId = mind.UserId; + } + + EntityUid spokenLanguage; + if (_language.SpeaksLanguage(entity, language.ID, out var spokenEnt)) + { + spokenLanguage = spokenEnt.Value; + } + else + { + if (!_language.TryAddLanguage(entity, language.ID, out var addedLangs)) + { + Log.Warning("Failed to teach " + Name(entity) + " language: " + language.Name); + return; + } + // If TryAddLanguage returned true this will have at least one language. + spokenLanguage = addedLangs.First(); + } + + + var newMemory = new SpeechMemory(sourceNetUserId, message, GetNetEntity(spokenLanguage)); + + if (entity.Comp.SpeechMemories.Count < entity.Comp.MaxSpeechMemory) + { + entity.Comp.SpeechMemories.Add(newMemory); + return; + } + + var replaceIdx = _random.Next(entity.Comp.SpeechMemories.Count); + entity.Comp.SpeechMemories[replaceIdx] = newMemory; + } +} diff --git a/Content.Server/_DEN/Chat/SharedChatEvents.Language.cs b/Content.Server/_DEN/Chat/SharedChatEvents.Language.cs new file mode 100644 index 00000000000..00e39915c7b --- /dev/null +++ b/Content.Server/_DEN/Chat/SharedChatEvents.Language.cs @@ -0,0 +1,30 @@ +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Radio; + +namespace Content.Shared.Chat; + +public sealed class EntitySpokeLanguageEvent : EntityEventArgs +{ + public readonly EntityUid Source; + public readonly Entity LanguageEnt; + public readonly ComplexChatMessage Message; + public readonly string Verb; + public readonly ChatChannel ChatChannel; + + /// + /// If the entity was trying to speak into a radio, this was the channel they were trying to access. If a radio + /// message gets sent on this channel, this should be set to null to prevent duplicate messages. + /// + public RadioChannelPrototype? RadioChannel; + + public EntitySpokeLanguageEvent(EntityUid source, Entity languageEnt, ComplexChatMessage message, RadioChannelPrototype? radioChannel, string verb, ChatChannel chatChannel) + { + Source = source; + Message = message; + LanguageEnt = languageEnt; + RadioChannel = radioChannel; + Verb = verb; + ChatChannel = chatChannel; + } +} diff --git a/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs b/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs new file mode 100644 index 00000000000..9fe42798648 --- /dev/null +++ b/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs @@ -0,0 +1,442 @@ +using System.Text; +using Content.Server._DEN.Language.EntitySystems; +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Language.EntitySystems; +using Content.Shared.Chat; +using Content.Shared.Database; +using Content.Shared.IdentityManagement; +using Content.Shared.Radio; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Chat.Systems; + +public sealed partial class ChatSystem +{ + [Dependency] private readonly LanguageSystem _language = default!; + + public static readonly ProtoId SpeakWrapper = "SpeakWrapper"; + public static readonly ProtoId WhisperWrapper = "WhisperWrapper"; + + public void SendEntityComplexSpeech(EntityUid source, + ComplexChatMessage originalMessage, + ProtoId wrapperProto, + ChatChannel chatChannel, + ChatTransmitRange range, + RadioChannelPrototype? radioChannel = null, + string? nameOverride = null, + bool hideLog = false, + bool ignoreActionBlocker = false, + string? verbOverride = null, + Entity? languageOverride = null) + { + // Getting this first makes sure that if the language defaulted to something new it is set for CanSpeak + var retrievedLanguage = languageOverride ?? _language.GetCurrentLanguageEntity(source); + if (retrievedLanguage is not { } languageEnt) + { + Log.Warning("Entity: " + Name(source) + " attempted to speak without a language."); + return; + } + + if (!_actionBlocker.CanSpeakLanguage(source, (languageEnt, languageEnt.Comp), chatChannel) && + !ignoreActionBlocker) + return; + + var language = _prototypeManager.Index(languageEnt.Comp.Language); + + var message = TransformComplexSpeech(source, originalMessage); + + if (message.Parts.Count == 0) + return; + + var speech = GetComplexSpeechVerb(source, message, language, chatChannel); + + string name; + if (nameOverride != null) + { + name = nameOverride; + } + else + { + var nameEv = new TransformSpeakerNameEvent(source, Name(source)); + RaiseLocalEvent(source, nameEv); + name = nameEv.VoiceName; + // Check for a speech verb override + if (nameEv.SpeechVerb != null && _prototypeManager.Resolve(nameEv.SpeechVerb, out var proto)) + speech = proto; + } + + name = FormattedMessage.EscapeText(name); + + var verb = verbOverride ?? Loc.GetString(_random.Pick(speech.SpeechVerbStrings)); + + if (language.WrapperOverrides is { } wrapperOverrides && + wrapperOverrides.TryGetValue(chatChannel, out var wrapperOverride)) + wrapperProto = wrapperOverride; + + var wrapper = _prototypeManager.Index(wrapperProto); + + // TODO: It's still weird that this is hardcoded, but you can expand it anyway so it's not the end of the world. + foreach (var (session, data) in GetRecipients(source, + chatChannel == ChatChannel.Whisper ? WhisperMuffledRange : VoiceRange)) + { + var entRange = MessageRangeCheck(session, data, range); + if (entRange == MessageRangeCheckResult.Disallowed) + continue; + + if (chatChannel == ChatChannel.Whisper && entRange != MessageRangeCheckResult.Full) + continue; + + var visibleName = name; + + var entHideChat = entRange == MessageRangeCheckResult.HideChat; + + // Don't bother checking the event if the player doesn't have an entity. + if (session.AttachedEntity is { Valid: true } playerEntity) + { + SendComplexMessageToEntity(source, + playerEntity, + languageEnt, + message, + wrapper, + chatChannel, + visibleName, + verb, + speech.Bold, + entHideChat, + null, + null); + } + } + + var (unwrappedMessage, wrappedMessage) = BuildComplexMessage(message, + wrapper, + language, + speech.Bold, + !language.DisplayInChat, + true, + name, + verb, + null, + null); + + _replay.RecordServerMessage(new ChatMessage(chatChannel, + unwrappedMessage, + wrappedMessage, + GetNetEntity(source), + null, + MessageRangeHideChatForReplay(range))); + + var ev = new EntitySpokeLanguageEvent(source, languageEnt, message, radioChannel, verb, chatChannel); + RaiseLocalEvent(source, ev, true); + + if (!HasComp(source) || hideLog) + return; + + // Build the original string to check if TransformComplexSpeech changed it. + var (original, _) = BuildComplexMessage(originalMessage, + wrapper, + language, + speech.Bold, + !language.DisplayInChat, + true, + name, + verb, + null, + null); + + var languageName = Loc.GetString(language.Name); + + if (original == unwrappedMessage) + { + if (name != Name(source)) + { + _adminLogger.Add(LogType.Chat, + LogImpact.Low, + $"Say from {source} as {name} in {languageName}: {original}."); + } + else + { + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {source} in {languageName}: {original}."); + } + } + else + { + if (name != Name(source)) + { + _adminLogger.Add(LogType.Chat, + LogImpact.Low, + $"{chatChannel} from {source} as {name} in {languageName}, original: {original}, transformed: {unwrappedMessage}."); + } + else + { + _adminLogger.Add(LogType.Chat, + LogImpact.Low, + $"{chatChannel} from {source} in {languageName}, original: {original}, transformed: {unwrappedMessage}."); + } + } + } + + public void SendComplexMessageToEntity(EntityUid source, + Entity listener, + Entity speakingEnt, + ComplexChatMessage originalMessage, + LanguageWrapperPrototype wrapper, + ChatChannel channel, + string name, + string verb, + bool bold, + bool hideChat, + string? radioChannel, + Color? color) + { + if (!Resolve(listener, ref listener.Comp)) + return; + + var language = _prototypeManager.Index(speakingEnt.Comp.Language); + + var understandEv = new AttemptUnderstandingEvent(source, language); + RaiseLocalEvent(listener, understandEv); + + if (understandEv.HideMessage) + return; + + var message = originalMessage; + + var understanding = _prototypeManager.Index(SharedLanguageSystem.MinimumFluency); + + if (understandEv is { Handled: true, Understanding: not null }) + { + understanding = _prototypeManager.Index(understandEv.Understanding.Value.Comp.Fluency); + } + + message = _language.ModifyMessageWithLanguage(speakingEnt, + source, + listener, + message, + language, + understanding, + name, + verb, + channel, + out name, + out verb); + + if (message.Parts.Count == 0) + return; + + var useLanguageFont = !HasComp(listener); + var hideLanguage = !(language.DisplayInChat && + _prototypeManager.Index(language.UnderstandingForDisplay) <= understanding) || + understandEv.HideLanguage; + + var (unwrappedMessage, wrappedMessage) = BuildComplexMessage(message, + wrapper, + language, + bold, + hideLanguage, + useLanguageFont, + name, + verb, + radioChannel, + color); + + _chatManager.ChatMessageToOne(channel, + unwrappedMessage, + wrappedMessage, + source, + hideChat, + listener.Comp.PlayerSession.Channel); + } + + // Returns the unwrapped message, as well as a wrapped version of the message based on the provided settings. + public (string, string) BuildComplexMessage(ComplexChatMessage message, + LanguageWrapperPrototype wrapper, + LanguagePrototype language, + bool bold, + bool hideLanguage, + bool useLanguageFont, + string name, + string verb, + string? channel, + Color? color) + { + var langStr = ""; + if (!hideLanguage) + { + langStr = Loc.GetString(wrapper.Language, + ("language", language.LocalizedAbbreviation), + ("color", language.FontColor)); + } + + var prefix = Loc.GetString(wrapper.Prefix, + ("language", langStr), + ("spacing", message.NeedsSeparation ? "(" : ""), + ("spacingClose", message.NeedsSeparation ? ")" : ""), + ("entityName", name), + ("channel", channel is null ? "" : $"\\[{channel}\\]")); + var wrappedBuilder = new StringBuilder(); + var unwrappedBuilder = new StringBuilder(); + + var boldType = Loc.GetString(wrapper.BoldType); + + var mainWrapper = wrapper.Message; + // Special casing to get the " not to be in the bubble in pure dialog. + if (message.Parts is [{ Item1: ChatPart.Dialog }]) + { + var (_, part) = message.Parts[0]; + unwrappedBuilder.Append(message.Delimiter + part + message.Delimiter); + wrappedBuilder.Append(Loc.GetString(wrapper.Dialog, + ("fontType", useLanguageFont ? language.FontId : "Default"), + ("fontColor", color ?? language.FontColor), + ("fontSize", language.FontSize), + ("style", bold ? $"[{boldType}]" : ""), + ("styleClose", bold ? $"[/{boldType}]" : ""), + ("message", part))); + + mainWrapper = wrapper.SingularMessage; + } + else + { + foreach (var (kind, part) in message.Parts) + { + if (kind == ChatPart.Dialog) + { + unwrappedBuilder.Append(message.Delimiter + part + message.Delimiter); + wrappedBuilder.Append(message.Delimiter); + wrappedBuilder.Append(Loc.GetString(wrapper.Dialog, + ("fontType", useLanguageFont ? language.FontId : "Default"), + ("fontColor", color ?? language.FontColor), + ("fontSize", language.FontSize), + ("style", bold ? $"[{boldType}]" : ""), + ("styleClose", bold ? $"[/{boldType}]" : ""), + ("message", part))); + wrappedBuilder.Append(message.Delimiter); + } + else + { + unwrappedBuilder.Append(part); + wrappedBuilder.Append(Loc.GetString(wrapper.Emote, ("message", part))); + } + } + } + + var needsVerb = message.IsDetailed || string.IsNullOrEmpty(verb); + + var wrapResult = Loc.GetString(mainWrapper, + ("space", message.NeedsSpacing ? " " : ""), + ("verb", needsVerb ? "" : verb + ", "), + ("prefix", prefix), + ("message", wrappedBuilder.ToString()), + ("color", color ?? language.FontColor)); + return (unwrappedBuilder.ToString(), wrapResult); + } + + private void SendEntityComplexEmote(EntityUid source, + string action, + ChatTransmitRange range, + string? nameOverride, + bool hideLog = false, + bool checkEmote = true, + bool ignoreActionBlocker = false, + NetUserId? author = null) + { + if (!_actionBlocker.CanEmote(source) && !ignoreActionBlocker) + return; + + var isDetailed = action.StartsWith("!"); + if (isDetailed) + action = action[1..]; + + var useSpace = !(action.StartsWith("'") || action.StartsWith(",")) || isDetailed; + + var ent = Identity.Entity(source, EntityManager); + string name; + if (nameOverride != null) + { + name = nameOverride; + } + else + { + // This may cause languages to interfere with a person's name even though they're emoting? + // It's probably fine... + // The other option is that pAIs don't inherit plushie names while emoting. + // It DOES also make the voicemask work for changing your emote name. + // Prebase has this. + var nameEv = new TransformSpeakerNameEvent(source, Name(ent)); + RaiseLocalEvent(source, nameEv); + name = nameEv.VoiceName; + } + name = FormattedMessage.EscapeText(name); + + var wrappedMessage = Loc.GetString("chat-language-entity-me-wrap-message", + ("entity", ent), + ("entityName", name), + ("spacing", isDetailed ? "(" : ""), + ("spacingClose", isDetailed ? ")" : ""), + ("space", useSpace ? " " : ""), + ("message", action)); + + if (checkEmote && + !TryEmoteChatInput(source, action)) + return; + + SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author); + if (!hideLog) + { + if (name != Name(source)) + { + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source} as {name}: {action}"); + } + else + { + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}"); + } + } + } + + private ComplexChatMessage TransformComplexSpeech(EntityUid sender, ComplexChatMessage message) + { + var transformEvt = new TransformLanguageEvent(sender, message); + RaiseLocalEvent(sender, transformEvt, true); + + return transformEvt.Message; + } + + private ComplexChatMessage SanitizeComplexMessage(EntityUid source, + ComplexChatMessage message, + out List emoteStrs, + bool shouldCapitalize = true, + bool punctuate = false, + bool capitalizeTheWordI = true) + { + emoteStrs = []; + var newParts = new List<(ChatPart, string)>(message.Parts.Count); + foreach (var part in message.Parts) + { + if (part.Item1 == ChatPart.Dialog) + { + var sanitized = SanitizeInGameICMessage(source, + part.Item2, + out var emote, + shouldCapitalize, + punctuate, + capitalizeTheWordI); + if (!string.IsNullOrEmpty(sanitized)) + newParts.Add((part.Item1, sanitized)); + if (emote is not null) + emoteStrs.Add(emote); + } + else + { + newParts.Add((part.Item1, part.Item2)); + } + } + + return new ComplexChatMessage(message, newParts); + } +} diff --git a/Content.Server/_DEN/Holopad/HolopadSystem.Language.cs b/Content.Server/_DEN/Holopad/HolopadSystem.Language.cs new file mode 100644 index 00000000000..b2aff5334da --- /dev/null +++ b/Content.Server/_DEN/Holopad/HolopadSystem.Language.cs @@ -0,0 +1,18 @@ +using Content.Server.Telephone; +using Content.Shared.Holopad; + +namespace Content.Server.Holopad; + +public sealed partial class HolopadSystem +{ + private void InitializeLanguage() + { + SubscribeLocalEvent(OnTelephoneMessageLanguageSent); + } + + private void OnTelephoneMessageLanguageSent(Entity holopad, + ref TelephoneMessageLanguageSentEvent args) + { + LinkHolopadToUser(holopad, args.MessageSource); + } +} diff --git a/Content.Server/_DEN/Language/Commands/LanguageCommand.cs b/Content.Server/_DEN/Language/Commands/LanguageCommand.cs new file mode 100644 index 00000000000..1a99d335cc4 --- /dev/null +++ b/Content.Server/_DEN/Language/Commands/LanguageCommand.cs @@ -0,0 +1,145 @@ +using System.Text; +using Content.Server._DEN.Language.EntitySystems; +using Content.Server.Administration; +using Content.Shared._DEN.Language; +using Content.Shared.Administration; +using Robust.Shared; +using Robust.Shared.Configuration; +using Robust.Shared.Console; +using Robust.Shared.Prototypes; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; +using Robust.Shared.Utility; + +namespace Content.Server._DEN.Language.Commands; + +[ToolshedCommand, AdminCommand(AdminFlags.VarEdit)] +public sealed partial class LanguageCommand : ToolshedCommand +{ + private LanguageSystem? _language; + + [CommandImplementation("add")] + public EntityUid Add([PipedArgument] EntityUid target, + ProtoId language, + bool speaks = true, + [CommandArgument(typeof(FluencyProtoIdParser))] string fluency = "Fluent") + { + _language ??= GetSys(); + _language.TryAddLanguage(target, language, speaks, fluency, out var _); + + return target; + } + + [CommandImplementation("remove")] + public EntityUid Remove([PipedArgument] EntityUid target, + ProtoId language, + bool all = false) + { + _language ??= GetSys(); + + if (all) + _language.TryRemoveLanguages(target, language); + else + _language.TryRemoveLanguage(target, language); + + return target; + } + + [CommandImplementation("get")] + public List<(ProtoId, ProtoId, bool)> Get([PipedArgument] EntityUid target, + ProtoId language) + { + _language ??= GetSys(); + + _language.TryGetLanguages(target, language, out var languages); + + return languages; + } + + [CommandImplementation("getall")] + public List<(ProtoId, ProtoId, bool)> GetAll( + [PipedArgument] EntityUid target) + { + _language ??= GetSys(); + + _language.TryGetLanguages(target, out var languages); + + return languages; + } + + [CommandImplementation("speaks")] + public bool Speaks([PipedArgument] EntityUid target, + ProtoId language, + [CommandInverted] bool inverted) + { + _language ??= GetSys(); + + var speaks = _language.SpeaksLanguage(target, language); + + return inverted ? !speaks : speaks; + } + + [CommandImplementation("understands")] + public bool Understands([PipedArgument] EntityUid target, + ProtoId language, + [CommandInverted] bool inverted, + [CommandArgument(typeof(FluencyProtoIdParser))] string minimumFluency = "Unfamiliar") + { + _language ??= GetSys(); + + var understands = _language.UnderstandsLanguage(target, language, minimumFluency); + + return inverted ? !understands : understands; + } +} + +// This is the only way I could figure out to make an optional argument that's a ProtoId +// C# won't convert from a default 'string' to 'ProtoId' in method parameters. +// This is gross and a copy paste of ProtoIdTypeParser because CommandArgument also won't +// accept a TypeParser, only a CustomTypeParser :( +public sealed class FluencyProtoIdParser : CustomTypeParser +{ + [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + + public override bool TryParse(ParserContext ctx, out string result) + { + result = ""; + string? proto; + + // Prototype ids can be specified without quotes, but for backwards compatibility, we also accept strings with + // quotes, as previously it **had** to be a string + if (ctx.PeekRune() == new Rune('"')) + { + if (!Toolshed.TryParse(ctx, out proto)) + return false; + } + else + { + proto = ctx.GetWord(ParserContext.IsToken); + } + + if (proto is null || !_proto.HasIndex(proto)) + { + _proto.TryGetKindFrom(out var kind); + DebugTools.AssertNotNull(kind); + + ctx.Error = new NotAValidPrototype(proto ?? "[null]", kind!); + result = ""; + return false; + } + + result = new(proto); + return true; + } + + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) + { + + var hint = ToolshedCommand.GetArgHint(arg, typeof(ProtoId)); + var maxCount = _config.GetCVar(CVars.ToolshedPrototypesAutocompleteLimit); + var options = CompletionHelper.PrototypeIdsLimited(ctx.Input[ctx.Index..], proto: _proto, maxCount: maxCount); + return CompletionResult.FromHintOptions(options, hint); + } +} diff --git a/Content.Server/_DEN/Language/Commands/SetLanguageCommand.cs b/Content.Server/_DEN/Language/Commands/SetLanguageCommand.cs new file mode 100644 index 00000000000..1cee087984a --- /dev/null +++ b/Content.Server/_DEN/Language/Commands/SetLanguageCommand.cs @@ -0,0 +1,63 @@ +using System.Linq; +using Content.Server._DEN.Language.EntitySystems; +using Content.Shared._DEN.Language; +using Content.Shared.Administration; +using Robust.Shared.Console; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; + +namespace Content.Server._DEN.Language.Commands; + +[AnyCommand] +public sealed class SetLanguageCommand : LocalizedEntityCommands +{ + [Dependency] private readonly LanguageSystem _language = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + + public override string Command => "setlang"; + + public override 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; + + if (_proto.TryIndex(args[0], out var language)) + _language.TrySetLanguage(playerEntity, language); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (shell.Player is not { } player) + return CompletionResult.Empty; + + if (player.Status != SessionStatus.InGame || player.AttachedEntity is not { } ent) + return CompletionResult.Empty; + + if (args.Length == 1) + { + if (!_language.TryGetLanguageEntities(ent, out var languages)) + return CompletionResult.Empty; + + var spoken = languages.FindAll(lang => lang.Comp.Speaks); + return CompletionResult.FromHintOptions( + spoken.Select(lang => lang.Comp.Language.Id), + Loc.GetString("setlang-completion-hint")); + } + return CompletionResult.Empty; + } +} diff --git a/Content.Server/_DEN/Language/EntitySystems/GestaltSystem.cs b/Content.Server/_DEN/Language/EntitySystems/GestaltSystem.cs new file mode 100644 index 00000000000..6264c8cb37e --- /dev/null +++ b/Content.Server/_DEN/Language/EntitySystems/GestaltSystem.cs @@ -0,0 +1,119 @@ +using Content.Server.Chat.Systems; +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Language.EntitySystems; +using Content.Shared._DEN.Speech; +using Content.Shared.Examine; +using Content.Shared.Ghost; +using Content.Shared.Mobs.Systems; +using Content.Shared.Popups; +using Content.Shared.Whitelist; +using Robust.Server.Player; +using Robust.Shared.Random; + +namespace Content.Server._DEN.Language.EntitySystems; + +public sealed partial class GestaltSystem : EntitySystem +{ + [Dependency] private readonly SharedLanguageSystem _language = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + private EntityQuery _gestaltQuery; + + public override void Initialize() + { + _gestaltQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnExpandICChatRecipients); + SubscribeLocalEvent>(OnSpeakLanguageAttempt); + + SubscribeLocalEvent(OnGestaltLanguageStartup); + SubscribeLocalEvent(OnGestaltLanguageExamined); + } + + private void OnGestaltLanguageStartup(Entity ent, ref ComponentStartup args) + { + _language.OnLanguageUpdated(ent.AsType()); + } + + private void OnGestaltLanguageExamined(Entity ent, ref ExaminedEvent args) + { + args.PushMarkup(Loc.GetString("language-gestalt-language-description")); + } + + private void OnSpeakLanguageAttempt(Entity entity, ref LanguageRelayedEvent args) + { + var gestalt = entity.Comp; + + if (!gestalt.RequiresHost) + return; + + var foundHost = false; + if (gestalt.HostWhitelist is { } whitelist) + { + var hostQuery = EntityQueryEnumerator(); + while (hostQuery.MoveNext(out var host, out var _)) + { + if (!_whitelist.IsWhitelistPass(whitelist, host) || !_mobState.IsAlive(host)) + continue; + + foundHost = true; + break; + } + } + else + { + foundHost = true; + } + + if (!foundHost) + { + if (gestalt.MissingHostPopups.Count != 0) + { + _popupSystem.PopupEntity(Loc.GetString(_random.Pick(gestalt.MissingHostPopups)), args.Owner, args.Owner); + } + args.Args.Cancel(); + } + } + + private void OnExpandICChatRecipients(ExpandICChatRecipientsEvent args) + { + if (_language.GetCurrentLanguageEntity(args.Source) is not { } spokenLangEnt) + return; + + if (!_gestaltQuery.TryGetComponent(spokenLangEnt, out var gestalt)) + return; + + var ghostHearing = GetEntityQuery(); + var xforms = GetEntityQuery(); + + var transformSource = xforms.GetComponent(args.Source); + var sourceCoords = transformSource.Coordinates; + + foreach (var player in _playerManager.Sessions) + { + if (player.AttachedEntity is not { Valid: true } playerEntity) + continue; + + var observer = ghostHearing.HasComp(playerEntity); + + // Observer, or fails the whitelist if it exists. + if (!(observer || gestalt.ReceiverWhitelist is not { } receiverWhitelist + || _whitelist.IsWhitelistPass(receiverWhitelist, playerEntity))) + continue; + + var transformEntity = xforms.GetComponent(playerEntity); + + float distance = -1; + if (sourceCoords.TryDistance(EntityManager, transformEntity.Coordinates, out var dist)) + { + distance = dist; + } + args.Recipients.TryAdd(player, new ChatSystem.ICChatRecipientData(distance, observer)); + } + } +} diff --git a/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs b/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs new file mode 100644 index 00000000000..7b5526eb567 --- /dev/null +++ b/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs @@ -0,0 +1,73 @@ +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Language.EntitySystems; +using Content.Shared.Chat; +using Robust.Shared.Prototypes; + +namespace Content.Server._DEN.Language.EntitySystems; + +public sealed partial class LanguageSystem : SharedLanguageSystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>( + OnAttemptUnderstandingRelay); + + SubscribeNetworkEvent(OnHideFontsRequest); + } + + private void OnHideFontsRequest(HideFontsMessage msg, EntitySessionEventArgs args) + { + var senderSession = args.SenderSession; + + if (senderSession.AttachedEntity is not { } senderEnt) + return; + + if (msg.Hide) + { + EnsureComp(senderEnt); + } + else + { + RemComp(senderEnt); + } + } + + private void OnAttemptUnderstandingRelay(Entity ent, + ref LanguageRelayedEvent args) + { + var evt = args.Args; + if (evt.Language.ID != ent.Comp.Language) + return; + + var hasUnderstanding = _proto.Index(ent.Comp.Fluency); + if (evt.Understanding is null || _proto.Index(evt.Understanding.Value.Comp.Fluency) < hasUnderstanding) + { + evt.Understanding = ent; + evt.Handled = true; + } + } + + public ComplexChatMessage ModifyMessageWithLanguage(EntityUid languageEntity, + EntityUid sender, + EntityUid listener, + ComplexChatMessage originalMessage, + LanguagePrototype language, + LanguageFluencyPrototype understanding, + string originalName, + string originalVerb, + ChatChannel chatChannel, + out string name, + out string verb) + { + var ev = new LanguageModifyMessageEvent(sender, listener, originalMessage, language, understanding, originalName, originalVerb, chatChannel); + RaiseLocalEvent(languageEntity, ev); + name = ev.Name; + verb = ev.Verb; + return ev.Message; + } +} diff --git a/Content.Server/_DEN/Language/EntitySystems/SyllableScramblingSystem.cs b/Content.Server/_DEN/Language/EntitySystems/SyllableScramblingSystem.cs new file mode 100644 index 00000000000..88ea10e7afd --- /dev/null +++ b/Content.Server/_DEN/Language/EntitySystems/SyllableScramblingSystem.cs @@ -0,0 +1,218 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.RegularExpressions; +using Content.Shared._DEN.CCVars; +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Language.EntitySystems; +using Content.Shared.Chat; +using Content.Shared.Dataset; +using Robust.Shared.Configuration; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server._DEN.Language.EntitySystems; + +public sealed class SyllableScramblingSystem : SharedSyllableScramblingSystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + // 1000 most common words and their order. This is a dictionary to make looking up specific words faster. + public Dictionary CommonWordFrequency = new(); + + // Cache for individual words + private readonly Dictionary, OrderedDictionary> _wordCache = new(); + // Cache for the 1000 most common words, gets added to but never excluded from. Still gets built as needed. + private readonly Dictionary, Dictionary> _commonWordCache = new(); + // Cache for messages, cares about the understanding of the language. + private readonly Dictionary> _messageCache = new(); + + private static readonly ProtoId CommonWords = "CommonWords"; + + private int _messageCacheMaxSize = 0; + private int _wordCacheMaxSize = 0; + + private static readonly Regex Lowercase = new("[a-z]|I$|[0-9]", RegexOptions.Compiled); + private static readonly Regex Sentence = new(@"(.+?(?:[\.!\?]|$))", RegexOptions.Compiled); + private static readonly Regex Punctuation = new(@"[\,\.\!\?]", RegexOptions.Compiled); + + public override void Initialize() + { + SubscribeLocalEvent(OnLanguageModifyMessage); + + _cfg.OnValueChanged(DenCCVars.LanguageMessageCacheSize, cacheSize => _messageCacheMaxSize = cacheSize, true); + _cfg.OnValueChanged(DenCCVars.LanguageWordCacheSize, cacheSize => _wordCacheMaxSize = cacheSize, true); + + BuildCommonWordSet(); + } + + private void BuildCommonWordSet() + { + var commonWords = _proto.Index(CommonWords); + CommonWordFrequency = new Dictionary(commonWords.Values.Count, StringComparer.OrdinalIgnoreCase); + var i = 0; + foreach (var word in commonWords.Values) + { + CommonWordFrequency.Add(Loc.GetString(word), i++); + } + } + + private void OnLanguageModifyMessage(Entity entity, ref LanguageModifyMessageEvent args) + { + var newMessageParts = new List<(ChatPart, string)>(); + foreach (var (kind, part) in args.Message.Parts) + { + if (kind == ChatPart.Dialog) + { + var modifiedMsg = ScrambleMessage(part, args.Language, entity.Comp, args.Understanding.Understanding); + newMessageParts.Add((kind, modifiedMsg)); + } + else + { + newMessageParts.Add((kind, part)); + } + } + args.Message = new ComplexChatMessage(args.Message, newMessageParts); + } + + private string ScrambleMessage(string message, ProtoId language, SyllableScramblingComponent comp, int understanding = 0) + { + if (understanding >= 100) + return message; + + // Check if we have this cached. This is useful so we don't have to re-scramble the message for multiple listeners. + if (TryGetMessageCachedValue(language.Id + "-" + understanding, message, out var value)) + { + var allCaps = !Lowercase.IsMatch(message); + return allCaps ? value.ToUpper() : value; + } + + var builder = new StringBuilder(); + var wordBuilder = new StringBuilder(); + var random = IoCManager.Resolve(); + + foreach (Match sentence in Sentence.Matches(message)) + { + var firstWord = true; + foreach (var word in sentence.Value.Split(' ')) + { + var allCaps = !Lowercase.IsMatch(word); + var trimmedWord = Punctuation.Replace(word, string.Empty); + var commonality = CommonWordFrequency.GetValueOrDefault(trimmedWord, 1500); + + var prob = 10 * (1 - (commonality / 500)); + if (understanding > 0 && random.Next(100) <= understanding + prob) + { + builder.Append(trimmedWord); + builder.Append(' '); + firstWord = false; + continue; + } + + if (TryGetWordCachedValue(language, trimmedWord, out var cachedWord)) + { + if (firstWord) + { + cachedWord = string.Concat(cachedWord[0].ToString().ToUpper(), cachedWord.AsSpan(1)); + firstWord = false; + } + builder.Append(allCaps ? cachedWord.ToUpper() : cachedWord); + builder.Append(' '); + continue; + } + + wordBuilder.Clear(); + var count = random.Next(comp.MinSyllables, comp.MaxSyllables + 1); + for (var i = 0; i < count; i++) + { + var syllable = random.Pick(comp.Syllables); + if (firstWord) + { + syllable = string.Concat(syllable[0].ToString().ToUpper(), syllable.AsSpan(1)); + firstWord = false; + } + + wordBuilder.Append(syllable); + } + var scrambledWord = wordBuilder.ToString(); + AddWordToCache(language, trimmedWord, scrambledWord.ToLower()); + builder.Append(allCaps ? scrambledWord.ToUpper() : scrambledWord); + builder.Append(' '); + } + + if (Punctuation.IsMatch(sentence.Value[^1].ToString())) + { + builder.Remove(builder.Length - 1, 1); + builder.Append(sentence.Value[^1]); + } + + builder.Append(' '); + } + + var result = builder.ToString().Trim(); + AddMessageToCache(language.Id + "-" + understanding, message, result); + return result; + } + + private bool TryGetMessageCachedValue(string key, string msg, [MaybeNullWhen(false)] out string value) + { + _messageCache.TryAdd(key, new OrderedDictionary(StringComparer.OrdinalIgnoreCase)); + var messageCache = _messageCache[key]; + if (messageCache.Remove(msg, out value)) + { + // Put the entry back at the end of the ordered cache. + messageCache.Add(msg, value); + return true; + } + return false; + } + + private void AddMessageToCache(string key, string msg, string value) + { + _messageCache.TryAdd(key, new OrderedDictionary(StringComparer.OrdinalIgnoreCase)); + var messageCache = _messageCache[key]; + messageCache.Remove(msg); + messageCache.Add(msg, value); + if (messageCache.Count > _messageCacheMaxSize) + messageCache.RemoveAt(0); + } + + private bool TryGetWordCachedValue(ProtoId language, string word, [MaybeNullWhen(false)] out string value) + { + if (CommonWordFrequency.ContainsKey(word)) + { + _commonWordCache.TryAdd(language, new Dictionary(StringComparer.OrdinalIgnoreCase)); + var commonCache = _commonWordCache[language]; + return commonCache.TryGetValue(word, out value); + } + + _wordCache.TryAdd(language, new OrderedDictionary(StringComparer.OrdinalIgnoreCase)); + var wordCache = _wordCache[language]; + if (wordCache.Remove(word, out value)) + { + wordCache.Add(word, value); + return true; + } + + return false; + } + + private void AddWordToCache(ProtoId language, string word, string value) + { + if (CommonWordFrequency.ContainsKey(word)) + { + _commonWordCache.TryAdd(language, new Dictionary(StringComparer.OrdinalIgnoreCase)); + var commonCache = _commonWordCache[language]; + commonCache.TryAdd(word, value); + return; + } + + _wordCache.TryAdd(language, new OrderedDictionary(StringComparer.OrdinalIgnoreCase)); + var wordCache = _wordCache[language]; + wordCache.Remove(word); + wordCache.Add(word, value); + if (wordCache.Count > _wordCacheMaxSize) + wordCache.RemoveAt(0); + } +} diff --git a/Content.Server/_DEN/Language/EntitySystems/UniversalLanguageSpeakerSystem.cs b/Content.Server/_DEN/Language/EntitySystems/UniversalLanguageSpeakerSystem.cs new file mode 100644 index 00000000000..1135401e68d --- /dev/null +++ b/Content.Server/_DEN/Language/EntitySystems/UniversalLanguageSpeakerSystem.cs @@ -0,0 +1,42 @@ +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Language.EntitySystems; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Server._DEN.Language.EntitySystems; + +// If this gets moved to shared it breaks the client UI because it creates a clientside only language. +public sealed class UniversalLanguageSpeakerSystem : EntitySystem +{ + [Dependency] private readonly SharedLanguageSystem _language = default!; + + private static readonly ProtoId Universal = "Universal"; + + public override void Initialize() + { + SubscribeLocalEvent(OnUniversalLanguageStartup); + SubscribeLocalEvent( + OnUniversalAttemptUnderstanding); + } + + private void OnUniversalLanguageStartup(Entity entity, ref ComponentStartup args) + { + if (_language.TryAddLanguage(entity, Universal, true, SharedLanguageSystem.MaximumFluency, out var langs)) + { + if (langs.FirstOrNull() is { } lang && TryComp(lang, out var langComp)) + { + entity.Comp.UniversalLanguage = (lang, langComp); + return; + } + } + Log.Debug("Failed to add universal language to: " + Name(entity)); + } + + private void OnUniversalAttemptUnderstanding(Entity ent, + ref AttemptUnderstandingEvent evt) + { + evt.Understanding = ent.Comp.UniversalLanguage; + evt.Handled = true; + } +} diff --git a/Content.Server/_DEN/Radio/EntitySystems/HeadsetSystem.Language.cs b/Content.Server/_DEN/Radio/EntitySystems/HeadsetSystem.Language.cs new file mode 100644 index 00000000000..093228e666e --- /dev/null +++ b/Content.Server/_DEN/Radio/EntitySystems/HeadsetSystem.Language.cs @@ -0,0 +1,63 @@ +using Content.Server.Chat.Systems; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; +using Content.Shared.Radio.Components; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server.Radio.EntitySystems; + +public sealed partial class HeadsetSystem +{ + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly ChatSystem _chat = default!; + + private EntityQuery _radioLang; + + private void InitializeLanguage() + { + _radioLang = GetEntityQuery(); + + SubscribeLocalEvent(OnHeadsetReceiveLanguage); + SubscribeLocalEvent(OnSpeakLanguage); + } + + private void OnSpeakLanguage(EntityUid uid, WearingHeadsetComponent component, EntitySpokeLanguageEvent args) + { + if (args.RadioChannel != null + && TryComp(component.Headset, out EncryptionKeyHolderComponent? keys) + && keys.Channels.Contains(args.RadioChannel.ID) + && _radioLang.HasComponent(args.LanguageEnt)) + { + _radio.SendLanguageRadioMessage(uid, args.LanguageEnt, args.Message, args.RadioChannel, component.Headset); + } + } + + private void OnHeadsetReceiveLanguage(EntityUid uid, HeadsetComponent component, ref RadioReceiveLanguageEvent args) + { + var parent = Transform(uid).ParentUid; + + if (parent.IsValid()) + { + var relayEvent = new HeadsetRadioReceiveLanguageRelayEvent(args); + RaiseLocalEvent(parent, ref relayEvent); + } + + if (TryComp(parent, out ActorComponent? _)) + { + _chat.SendComplexMessageToEntity( + args.RadioSource, + parent, + args.LanguageEnt, + args.Message, + _prototype.Index(RadioSystem.RadioWrapper), + ChatChannel.Radio, + args.Name, + args.Verb, + args.Speech.Bold, + false, + args.Channel.LocalizedName, + args.Channel.Color); + } + } +} diff --git a/Content.Server/_DEN/Radio/EntitySystems/RadioDeviceSystem.Language.cs b/Content.Server/_DEN/Radio/EntitySystems/RadioDeviceSystem.Language.cs new file mode 100644 index 00000000000..0d6277e44d4 --- /dev/null +++ b/Content.Server/_DEN/Radio/EntitySystems/RadioDeviceSystem.Language.cs @@ -0,0 +1,70 @@ +using Content.Server.Chat.Systems; +using Content.Server.Power.EntitySystems; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Speech; +using Content.Shared.Chat; +using Content.Shared.Radio.Components; + +namespace Content.Server.Radio.EntitySystems; + +public sealed partial class RadioDeviceSystem +{ + private EntityQuery _radioLang; + + private void InitializeLanguage() + { + _radioLang = GetEntityQuery(); + + SubscribeLocalEvent(OnListenLanguage); + SubscribeLocalEvent(OnAttemptListenLanguage); + + SubscribeLocalEvent(OnReceiveLanguageRadio); + } + + private void OnListenLanguage(EntityUid uid, RadioMicrophoneComponent component, ListenLanguageEvent args) + { + if (HasComp(args.Source)) + return; + + var channel = _protoMan.Index(component.BroadcastChannel); + if (_recentlySent.Add((args.Message.OriginalMessage, args.Source, channel))) + _radio.SendLanguageRadioMessage(args.Source, args.LanguageEnt, args.Message, channel, uid); + } + + private void OnAttemptListenLanguage(EntityUid uid, + RadioMicrophoneComponent component, + ListenLanguageAttemptEvent args) + { + if (component.PowerRequired && !this.IsPowered(uid, EntityManager) + || component.UnobstructedRequired && !_interaction.InRangeUnobstructed(args.Source, uid, 0) + || !_radioLang.HasComp(args.LanguageEnt)) + { + args.Cancel(); + } + } + + private void OnReceiveLanguageRadio(EntityUid uid, + RadioSpeakerComponent component, + ref RadioReceiveLanguageEvent args) + { + if (uid == args.RadioSource) + return; + + var nameEv = new TransformSpeakerNameEvent(args.MessageSource, Name(args.MessageSource)); + RaiseLocalEvent(args.MessageSource, nameEv); + + var name = Loc.GetString("speech-name-relay", + ("speaker", Name(uid)), + ("originalName", nameEv.VoiceName)); + + _chat.SendEntityComplexSpeech(uid, + args.Message, + ChatSystem.WhisperWrapper, + ChatChannel.Whisper, + ChatTransmitRange.GhostRangeLimit, + null, + name, + verbOverride: args.Verb, + languageOverride: args.LanguageEnt); + } +} diff --git a/Content.Server/_DEN/Radio/EntitySystems/RadioSystem.Language.cs b/Content.Server/_DEN/Radio/EntitySystems/RadioSystem.Language.cs new file mode 100644 index 00000000000..ca300fe86f6 --- /dev/null +++ b/Content.Server/_DEN/Radio/EntitySystems/RadioSystem.Language.cs @@ -0,0 +1,179 @@ +using Content.Server._DEN.Language.EntitySystems; +using Content.Server.Chat.Systems; +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; +using Content.Shared.Database; +using Content.Shared.Radio; +using Content.Shared.Radio.Components; +using Content.Shared.Speech; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Radio.EntitySystems; + +public sealed partial class RadioSystem +{ + [Dependency] private readonly LanguageSystem _language = default!; + + private EntityQuery _radioLang; + + public static readonly ProtoId RadioWrapper = "RadioWrapper"; + + private void InitializeLanguage() + { + _radioLang = GetEntityQuery(); + + SubscribeLocalEvent(OnIntrinsicLanguageReceive); + SubscribeLocalEvent(OnIntrinsicSpeakLanguage); + } + + private void OnIntrinsicLanguageReceive(EntityUid uid, + IntrinsicRadioReceiverComponent component, + ref RadioReceiveLanguageEvent args) + { + if (TryComp(uid, out ActorComponent? actor)) + { + _chat.SendComplexMessageToEntity( + args.RadioSource, + uid, + args.LanguageEnt, + args.Message, + _prototype.Index(RadioWrapper), + ChatChannel.Radio, + args.Name, + args.Verb, + args.Speech.Bold, + false, + args.Channel.LocalizedName, + args.Channel.Color + ); + } + } + + private void OnIntrinsicSpeakLanguage(EntityUid uid, + IntrinsicRadioTransmitterComponent component, + EntitySpokeLanguageEvent args) + { + if (args.RadioChannel != null && component.Channels.Contains(args.RadioChannel.ID) && _radioLang.HasComp(args.LanguageEnt)) + { + SendLanguageRadioMessage(uid, args.LanguageEnt, args.Message, args.RadioChannel, uid); + args.RadioChannel = null; + } + } + + public void SendLanguageRadioMessage(EntityUid uid, + string message, + ProtoId channel, + EntityUid radioSource, + bool escapeMarkup = true) + { + SendLanguageRadioMessage(uid, message, _prototype.Index(channel), radioSource, escapeMarkup); + } + + public void SendLanguageRadioMessage(EntityUid uid, + string message, + RadioChannelPrototype channel, + EntityUid radioSource, + bool escapeMarkup = true) + { + var complex = new ComplexChatMessage(message, "\"", false, true, false, escapeMarkup); + var languageEnt = _language.GetCurrentLanguageEntity(uid, true); + if (languageEnt is null) + { + Log.Warning("Default language entity is null! Unable to send message."); + return; + } + SendLanguageRadioMessage(uid, languageEnt.Value, complex, channel, radioSource); + } + + public void SendLanguageRadioMessage(EntityUid messageSource, + Entity languageEnt, + ComplexChatMessage message, + RadioChannelPrototype channel, + EntityUid radioSource) + { + if (!_messages.Add(message.OriginalMessage)) + return; + + var evt = new TransformSpeakerNameEvent(messageSource, MetaData(messageSource).EntityName); + RaiseLocalEvent(messageSource, evt); + + var name = evt.VoiceName; + name = FormattedMessage.EscapeText(name); + + var language = _prototype.Index(languageEnt.Comp.Language); + + SpeechVerbPrototype speech; + if (evt.SpeechVerb != null && _prototype.Resolve(evt.SpeechVerb, out var evntProto)) + speech = evntProto; + else + speech = _chat.GetComplexSpeechVerb(messageSource, message, language, ChatChannel.Radio); + + var verb = Loc.GetString(_random.Pick(speech.SpeechVerbStrings)); + + var ev = new RadioReceiveLanguageEvent(message, languageEnt, speech, name, verb, messageSource, channel, radioSource); + + var sendAttemptEv = new RadioSendAttemptEvent(channel, radioSource); + RaiseLocalEvent(ref sendAttemptEv); + RaiseLocalEvent(radioSource, ref sendAttemptEv); + var canSend = !sendAttemptEv.Cancelled; + + var sourceMapId = Transform(radioSource).MapID; + var hasActiveServer = HasActiveServer(sourceMapId, channel.ID); + var sourceServerExempt = _exemptQuery.HasComp(radioSource); + + var radioQuery = EntityQueryEnumerator(); + + while (canSend && radioQuery.MoveNext(out var receiver, out var radio, out var transform)) + { + if (!radio.ReceiveAllChannels) + { + if (!radio.Channels.Contains(channel.ID) || (TryComp(receiver, out var intercom) && + !intercom.SupportedChannels.Contains(channel.ID))) + continue; + } + + if (!channel.LongRange && transform.MapID != sourceMapId && !radio.GlobalReceive) + continue; + + var needServer = !channel.LongRange && !sourceServerExempt; + if (needServer && !hasActiveServer) + continue; + + var attemptEv = new RadioReceiveAttemptEvent(channel, radioSource, receiver); + RaiseLocalEvent(ref attemptEv); + RaiseLocalEvent(receiver, ref attemptEv); + if (attemptEv.Cancelled) + continue; + + RaiseLocalEvent(receiver, ref ev); + } + + var (unwrappedMessage, wrappedMessage) = _chat.BuildComplexMessage(message, + _prototype.Index(RadioWrapper), + language, + speech.Bold, + language.DisplayInChat, + true, + name, + verb, + channel.LocalizedName, + channel.Color); + + if (name != Name(messageSource)) + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(messageSource):user} as {name} {channel.LocalizedName}: {unwrappedMessage}"); + else + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(messageSource):user} on {channel.LocalizedName}: {unwrappedMessage}"); + + _replay.RecordServerMessage(new ChatMessage( + ChatChannel.Radio, + unwrappedMessage, + wrappedMessage, + NetEntity.Invalid, + null)); + _messages.Remove(message.OriginalMessage); + } +} diff --git a/Content.Server/_DEN/Radio/RadioEvent.Language.cs b/Content.Server/_DEN/Radio/RadioEvent.Language.cs new file mode 100644 index 00000000000..d57cf0a972f --- /dev/null +++ b/Content.Server/_DEN/Radio/RadioEvent.Language.cs @@ -0,0 +1,13 @@ +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; +using Content.Shared.Radio; +using Content.Shared.Speech; + +namespace Content.Server.Radio; + +[ByRefEvent] +public readonly record struct RadioReceiveLanguageEvent(ComplexChatMessage Message, Entity LanguageEnt, SpeechVerbPrototype Speech, string Name, string Verb, EntityUid MessageSource, RadioChannelPrototype Channel, EntityUid RadioSource); + +[ByRefEvent] +public readonly record struct HeadsetRadioReceiveLanguageRelayEvent(RadioReceiveLanguageEvent RelayedEvent); diff --git a/Content.Server/_DEN/Speech/EntitySystems/BlockListeningSystem.Language.cs b/Content.Server/_DEN/Speech/EntitySystems/BlockListeningSystem.Language.cs new file mode 100644 index 00000000000..a4798ee098f --- /dev/null +++ b/Content.Server/_DEN/Speech/EntitySystems/BlockListeningSystem.Language.cs @@ -0,0 +1,19 @@ +using Content.Server.Speech.Components; +using Content.Shared._DEN.Speech; + +namespace Content.Server.Speech.EntitySystems; + +public sealed partial class BlockListeningSystem +{ + private void InitializeLanguage() + { + SubscribeLocalEvent(OnListenLanguageAttempt); + } + + private void OnListenLanguageAttempt(EntityUid uid, + BlockListeningComponent component, + ListenLanguageAttemptEvent args) + { + args.Cancel(); + } +} diff --git a/Content.Server/_DEN/Speech/EntitySystems/ListeningSystem.Language.cs b/Content.Server/_DEN/Speech/EntitySystems/ListeningSystem.Language.cs new file mode 100644 index 00000000000..6c6108c04dc --- /dev/null +++ b/Content.Server/_DEN/Speech/EntitySystems/ListeningSystem.Language.cs @@ -0,0 +1,64 @@ +using Content.Server.Chat.Systems; +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Speech; +using Content.Shared.Chat; +using Content.Shared.Speech.Components; + +namespace Content.Server.Speech.EntitySystems; + +public sealed partial class ListeningSystem +{ + [Dependency] private readonly SharedChatSystem _chat = default!; + + private void InitializeLanguage() + { + SubscribeLocalEvent(OnSpeakLanguage); + } + + private void OnSpeakLanguage(EntitySpokeLanguageEvent ev) + { + PingLanguageListeners(ev.Source, ev.LanguageEnt, ev.Message, ev.Verb, ev.ChatChannel); + } + + public void PingLanguageListeners(EntityUid source, Entity languageEnt, ComplexChatMessage message, string verb, ChatChannel channel) + { + // TODO whispering / audio volume? Microphone sensitivity? + // for now, whispering just arbitrarily reduces the listener's max range. + + var xformQuery = GetEntityQuery(); + var sourceXform = xformQuery.GetComponent(source); + var sourcePos = _xforms.GetWorldPosition(sourceXform, xformQuery); + + var attemptEv = new ListenLanguageAttemptEvent(source, languageEnt); + var ev = new ListenLanguageEvent(message, source, languageEnt, verb, channel); + // TODO: Hardcoded obfuscation bad. + var obfuscatedEv = channel == ChatChannel.Whisper + ? new ListenLanguageEvent(_chat.ObfuscateComplexChatMessage(message, 0.2f), source, languageEnt, verb, channel) + : null; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var listenerUid, out var listener, out var xform)) + { + if (xform.MapID != sourceXform.MapID) + continue; + + var distance = (sourcePos - _xforms.GetWorldPosition(xform, xformQuery)).LengthSquared(); + if (distance > listener.Range * listener.Range) + continue; + + RaiseLocalEvent(listenerUid, attemptEv); + if (attemptEv.Cancelled) + { + attemptEv.Uncancel(); + continue; + } + + if (obfuscatedEv != null && distance > ChatSystem.WhisperClearRange) + RaiseLocalEvent(listenerUid, obfuscatedEv); + else + RaiseLocalEvent(listenerUid, ev); + } + + } +} diff --git a/Content.Server/_DEN/Speech/Muting/MutingSystem.Language.cs b/Content.Server/_DEN/Speech/Muting/MutingSystem.Language.cs new file mode 100644 index 00000000000..5f5d820ce61 --- /dev/null +++ b/Content.Server/_DEN/Speech/Muting/MutingSystem.Language.cs @@ -0,0 +1,35 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Speech; +using Content.Shared.Abilities.Mime; +using Content.Shared.Puppet; +using Content.Shared.Speech.Muting; + +namespace Content.Server.Speech.Muting; + +public sealed partial class MutingSystem +{ + private EntityQuery _audibleQuery; + + public void InitializeLanguage() + { + _audibleQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnSpeakLanguageAttempt); + } + + private void OnSpeakLanguageAttempt(Entity ent, ref SpeakLanguageAttemptEvent args) + { + // Non-audible languages are not impacted by being unable to make sound. + if (!_audibleQuery.HasComp(args.LanguageEnt)) + return; + + if (HasComp(ent)) + _popupSystem.PopupEntity(Loc.GetString("mime-cant-speak"), ent, ent); + else if (HasComp(ent)) + _popupSystem.PopupEntity(Loc.GetString("ventriloquist-puppet-cant-speak"), ent, ent); + else + _popupSystem.PopupEntity(Loc.GetString("speech-muted"), ent, ent); + + args.Cancel(); + } +} diff --git a/Content.Server/_DEN/Speech/SpeechSoundSystem.Language.cs b/Content.Server/_DEN/Speech/SpeechSoundSystem.Language.cs new file mode 100644 index 00000000000..076e796a5fb --- /dev/null +++ b/Content.Server/_DEN/Speech/SpeechSoundSystem.Language.cs @@ -0,0 +1,43 @@ +using System.Linq; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; +using Content.Shared.Speech; + +namespace Content.Server.Speech; + +public sealed partial class SpeechSoundSystem +{ + private EntityQuery _audibleQuery; + + private void InitializeLanguage() + { + _audibleQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnEntitySpokeLanguage); + } + + private void OnEntitySpokeLanguage(EntityUid uid, SpeechComponent component, EntitySpokeLanguageEvent args) + { + if (component.SpeechSounds == null) + return; + + if (!_audibleQuery.HasComponent(args.LanguageEnt)) + return; + + var currentTime = _gameTiming.CurTime; + var cooldown = TimeSpan.FromSeconds(component.SoundCooldownTime); + + if (currentTime - component.LastTimeSoundPlayed < cooldown) + return; + + var lastDialog = args.Message.Parts.LastOrDefault(part => part.Item1 == ChatPart.Dialog).Item2; + + // The "Speech" didn't actually contain any dialog. + if (lastDialog == null) + return; + + var sound = GetSpeechSound((uid, component), lastDialog); + component.LastTimeSoundPlayed = currentTime; + _audio.PlayPvs(sound, uid); + } +} diff --git a/Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.Language.cs b/Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.Language.cs new file mode 100644 index 00000000000..d59fd5ddfbf --- /dev/null +++ b/Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.Language.cs @@ -0,0 +1,58 @@ +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Speech; +using Content.Shared.Chat; +using Content.Shared.SurveillanceCamera.Components; + +namespace Content.Server.SurveillanceCamera; + +public sealed partial class SurveillanceCameraMicrophoneSystem +{ + private readonly EntityQuery _audibleQuery = default!; + private readonly EntityQuery _losQuery = default!; + + private void InitializeLanguage() + { + SubscribeLocalEvent(RelayEntityLanguageMessage); + SubscribeLocalEvent(CanListenLanguage); + } + + private void CanListenLanguage(EntityUid uid, + SurveillanceCameraMicrophoneComponent microphone, + ListenLanguageAttemptEvent args) + { + if (_whitelistSystem.IsWhitelistPass(microphone.Blacklist, args.Source) + || !_audibleQuery.HasComponent(args.LanguageEnt) + || !_losQuery.HasComponent(args.LanguageEnt)) + args.Cancel(); + } + + private void RelayEntityLanguageMessage(EntityUid uid, + SurveillanceCameraMicrophoneComponent component, + ListenLanguageEvent args) + { + if (!TryComp(uid, out SurveillanceCameraComponent? camera)) + return; + + var ev = new SurveillanceCameraSpeechLanguageSendEvent(args.Source, args.LanguageEnt, args.Message, args.Verb); + } +} + +public sealed class SurveillanceCameraSpeechLanguageSendEvent : EntityEventArgs +{ + public EntityUid Speaker { get; } + public Entity LanguageEnt { get; } + public ComplexChatMessage Message { get; } + public string Verb { get; } + + public SurveillanceCameraSpeechLanguageSendEvent(EntityUid speaker, + Entity languageEnt, + ComplexChatMessage message, + string verb) + { + Speaker = speaker; + Message = message; + LanguageEnt = languageEnt; + Verb = verb; + } +} diff --git a/Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.Language.cs b/Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.Language.cs new file mode 100644 index 00000000000..fbde41662d2 --- /dev/null +++ b/Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.Language.cs @@ -0,0 +1,56 @@ +using System.Linq; +using Content.Server.Chat.Systems; +using Content.Shared.Chat; +using Content.Shared.Speech; + +namespace Content.Server.SurveillanceCamera; + +public sealed partial class SurveillanceCameraSpeakerSystem +{ + private void InitializeLanguage() + { + SubscribeLocalEvent( + OnSpeechLanguageSent); + } + + private void OnSpeechLanguageSent(EntityUid uid, + SurveillanceCameraSpeakerComponent component, + SurveillanceCameraSpeechLanguageSendEvent args) + { + if (!component.SpeechEnabled) + return; + + var time = _gameTiming.CurTime; + var cd = TimeSpan.FromSeconds(component.SpeechSoundCooldown); // The docs say not to do this but I'm not here to fix the component. + + // I agree with the comment in the original file. + // Maybe SpeechNoiseSystem just needs a "Play noise for message" function. + if (time - component.LastSoundPlayed < cd + && TryComp(args.Speaker, out var speech)) + { + var sound = _speechSound.GetSpeechSound((args.Speaker, speech), + args.Message.Parts.LastOrDefault(part => part.Item1 == ChatPart.Dialog).Item2); + + _audioSystem.PlayPvs(sound, uid); + + component.LastSoundPlayed = time; + } + + var nameEv = new TransformSpeakerNameEvent(args.Speaker, Name(args.Speaker)); + RaiseLocalEvent(args.Speaker, nameEv); + + var name = Loc.GetString("speech-name-relay", + ("speaker", Name(uid)), + ("originalName", nameEv.VoiceName)); + + _chatSystem.SendEntityComplexSpeech(uid, + args.Message, + ChatSystem.SpeakWrapper, + ChatChannel.Whisper, + ChatTransmitRange.GhostRangeLimit, + null, + name, + verbOverride: args.Verb, + languageOverride: args.LanguageEnt); + } +} diff --git a/Content.Server/_DEN/Telephone/TelephoneEvents.cs b/Content.Server/_DEN/Telephone/TelephoneEvents.cs new file mode 100644 index 00000000000..6ddf2da7ea2 --- /dev/null +++ b/Content.Server/_DEN/Telephone/TelephoneEvents.cs @@ -0,0 +1,27 @@ +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; +using Content.Shared.Telephone; + +namespace Content.Server.Telephone; + +/// +/// Raised when a chat message is sent by a telephone to another +/// +[ByRefEvent] +public readonly record struct TelephoneMessageLanguageSentEvent( + ComplexChatMessage Message, + Entity LanguageEnt, + EntityUid MessageSource); + +/// +/// Raised when a chat message is received by a telephone from another +/// +[ByRefEvent] +public readonly record struct TelephoneMessageLanguageReceivedEvent( + ComplexChatMessage Message, + Entity LanguageEnt, + string Verb, + string Name, + EntityUid MessageSource, + Entity TelephoneSource); diff --git a/Content.Server/_DEN/Telephone/TelephoneSystem.Language.cs b/Content.Server/_DEN/Telephone/TelephoneSystem.Language.cs new file mode 100644 index 00000000000..f694c2c4d9c --- /dev/null +++ b/Content.Server/_DEN/Telephone/TelephoneSystem.Language.cs @@ -0,0 +1,150 @@ +using Content.Server.Chat.Systems; +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Language.EntitySystems; +using Content.Shared._DEN.Speech; +using Content.Shared.Cargo; +using Content.Shared.Chat; +using Content.Shared.Database; +using Content.Shared.Mind.Components; +using Content.Shared.Speech; +using Content.Shared.Telephone; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Telephone; + +public sealed partial class TelephoneSystem +{ + public static readonly ProtoId TelephoneWrapper = "TelephoneWrapper"; + + private EntityQuery _audibleQuery; + private EntityQuery _losQuery; + + private void InitializeLanguage() + { + _audibleQuery = GetEntityQuery(); + _losQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnAttemptLanguageListen); + SubscribeLocalEvent(OnLanguageListen); + SubscribeLocalEvent(OnTelephoneMessageLanguageReceived); + } + + private void OnAttemptLanguageListen(Entity entity, ref ListenLanguageAttemptEvent args) + { + if (!IsTelephonePowered(entity) || + !IsTelephoneEngaged(entity) || + entity.Comp.Muted || + !_interaction.InRangeUnobstructed(args.Source, entity.Owner, 0)) + { + args.Cancel(); + } + } + + private void OnLanguageListen(Entity entity, ref ListenLanguageEvent args) + { + if (args.Source == entity.Owner) + return; + + // Everything else in the chat code checks for ActorComponent... + if (!HasComp(args.Source)) + return; + + if (!_recentChatMessages.Add((args.Source, args.Message.OriginalMessage, entity))) + return; + + // Only transmit spoken languages, or, in the case of holopads, sign and spoken. + if (_audibleQuery.HasComp(args.LanguageEnt) || entity.Comp.TransmitsVisuals && _losQuery.HasComp(args.LanguageEnt)) + SendTelephoneLanguageMessage(args.Source, args.LanguageEnt, args.Message, entity); + } + + private void OnTelephoneMessageLanguageReceived(Entity entity, + ref TelephoneMessageLanguageReceivedEvent args) + { + // Prevent message feedback loops + if (entity == args.TelephoneSource) + return; + + if (!IsTelephonePowered(entity) || + !IsSourceConnectedToReceiver(args.TelephoneSource, entity)) + return; + + var nameEv = new TransformSpeakerNameEvent(args.MessageSource, Name(args.MessageSource)); + RaiseLocalEvent(args.MessageSource, nameEv); + + // Determine if speech should be relayed via the telephone itself or a designated speaker + var speaker = entity.Comp.Speaker?.Owner ?? entity.Owner; + + var name = Loc.GetString("chat-telephone-name-relay", + ("originalName", nameEv.VoiceName), + ("speaker", Name(speaker))); + + var range = args.TelephoneSource.Comp.LinkedTelephones.Count > 1 + ? ChatTransmitRange.HideChat + : ChatTransmitRange.GhostRangeLimit; + var whisper = entity.Comp.SpeakerVolume == TelephoneVolume.Whisper; + + _chat.SendEntityComplexSpeech(speaker, args.Message, TelephoneWrapper, whisper ? ChatChannel.Whisper : ChatChannel.Local, range, null, name, languageOverride: args.LanguageEnt); + } + + private void SendTelephoneLanguageMessage(EntityUid messageSource, Entity languageEnt, ComplexChatMessage message, Entity source) + { + // This method assumes that you've already checked that this + // telephone is able to transmit messages and that it can + // send messages to any telephones linked to it + var language = _prototype.Index(languageEnt.Comp.Language); + + var ev = new TransformSpeakerNameEvent(messageSource, MetaData(messageSource).EntityName); + RaiseLocalEvent(messageSource, ev); + + var name = ev.VoiceName; + name = FormattedMessage.EscapeText(name); + + SpeechVerbPrototype speech; + if (ev.SpeechVerb != null && _prototype.Resolve(ev.SpeechVerb, out var evntProto)) + speech = evntProto; + else + speech = _chat.GetComplexSpeechVerb(messageSource, message, language, ChatChannel.Radio); + + var verb = Loc.GetString(_random.Pick(speech.SpeechVerbStrings)); + + var evSentMessage = new TelephoneMessageLanguageSentEvent(message, languageEnt, messageSource); + RaiseLocalEvent(source, ref evSentMessage); + source.Comp.StateStartTime = _timing.CurTime; + + var evReceivedMessage = new TelephoneMessageLanguageReceivedEvent(message, languageEnt, verb, name, messageSource, source); + + foreach (var receiver in source.Comp.LinkedTelephones) + { + RaiseLocalEvent(receiver, ref evReceivedMessage); + receiver.Comp.StateStartTime = _timing.CurTime; + } + + var (unwrappedMessage, wrappedMessage) = _chat.BuildComplexMessage(message, + _prototype.Index(TelephoneWrapper), + language, + speech.Bold, + language.DisplayInChat, + true, + name, + verb, + null, + null); + + var chat = new ChatMessage( + ChatChannel.Radio, + unwrappedMessage, + wrappedMessage, + NetEntity.Invalid, + null); + + if (name != Name(messageSource)) + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Telephone message from {ToPrettyString(messageSource):user} as {name} on {source} in {language.LocalizedName}: {unwrappedMessage}"); + else + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Telephone message from {ToPrettyString(messageSource):user} on {source} in {language.LocalizedName}: {unwrappedMessage}"); + + _replay.RecordServerMessage(chat); + } +} diff --git a/Content.Server/_DEN/VendingMachines/VendingMachineSystem.Language.cs b/Content.Server/_DEN/VendingMachines/VendingMachineSystem.Language.cs new file mode 100644 index 00000000000..78b6909f3c0 --- /dev/null +++ b/Content.Server/_DEN/VendingMachines/VendingMachineSystem.Language.cs @@ -0,0 +1,17 @@ +using Content.Server.Vocalization.Systems; +using Content.Shared.VendingMachines; + +namespace Content.Server.VendingMachines; + +public sealed partial class VendingMachineSystem +{ + private void InitializeLanguage() + { + SubscribeLocalEvent(OnTryVocalizeLanguage); + } + + private void OnTryVocalizeLanguage(Entity ent, ref TryVocalizeLanguageEvent args) + { + args.Cancelled |= ent.Comp.Broken; + } +} diff --git a/Content.Server/_DEN/Vocalization/Systems/DatasetVocalizationSystem.Language.cs b/Content.Server/_DEN/Vocalization/Systems/DatasetVocalizationSystem.Language.cs new file mode 100644 index 00000000000..a745a494b87 --- /dev/null +++ b/Content.Server/_DEN/Vocalization/Systems/DatasetVocalizationSystem.Language.cs @@ -0,0 +1,23 @@ +using Content.Server.Vocalization.Components; +using Content.Shared.Random.Helpers; + +namespace Content.Server.Vocalization.Systems; + +public sealed partial class DatasetVocalizationSystem +{ + private void InitializeLanguage() + { + SubscribeLocalEvent(OnTryVocalizeLanguage); + } + + private void OnTryVocalizeLanguage(Entity ent, ref TryVocalizeLanguageEvent args) + { + if (args.Handled) + return; + + var dataset = _protoMan.Index(ent.Comp.Dataset); + + args.Message = _random.Pick(dataset); + args.Handled = true; + } +} diff --git a/Content.Server/_DEN/Vocalization/Systems/RadioVocalizationSystem.Language.cs b/Content.Server/_DEN/Vocalization/Systems/RadioVocalizationSystem.Language.cs new file mode 100644 index 00000000000..5354008a713 --- /dev/null +++ b/Content.Server/_DEN/Vocalization/Systems/RadioVocalizationSystem.Language.cs @@ -0,0 +1,50 @@ +using Content.Server.Chat.Systems; +using Content.Server.Vocalization.Components; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; +using Robust.Shared.Random; + +namespace Content.Server.Vocalization.Systems; + +public sealed partial class RadioVocalizationSystem +{ + private void InitializeLanguage() + { + SubscribeLocalEvent(OnVocalizeLanguage); + } + + private void OnVocalizeLanguage(Entity entity, ref VocalizeLanguageEvent args) + { + if (args.Handled) + return; + + args.Handled = TrySpeakLanguageRadio(entity.Owner, args.Language, args.Message); + } + + private bool TrySpeakLanguageRadio(Entity entity, + Entity language, + string message) + { + if (!Resolve(entity, ref entity.Comp)) + return false; + + if (!_random.Prob(entity.Comp.RadioAttemptChance)) + return false; + + if (!TryPickRandomRadioChannel(entity, out var channel)) + return false; + + var radioChannel = _proto.Index(channel); + var cmplxMessage = _chat.ConvertMessageToComplex(message); + + _chat.SendEntityComplexSpeech( + entity, + cmplxMessage, + ChatSystem.WhisperWrapper, + ChatChannel.Whisper, + ChatTransmitRange.Normal, + radioChannel); + + return true; + } +} diff --git a/Content.Server/_DEN/Vocalization/Systems/VocalizationSystem.Language.cs b/Content.Server/_DEN/Vocalization/Systems/VocalizationSystem.Language.cs new file mode 100644 index 00000000000..f067cae2921 --- /dev/null +++ b/Content.Server/_DEN/Vocalization/Systems/VocalizationSystem.Language.cs @@ -0,0 +1,83 @@ +using Content.Server.Chat.Systems; +using Content.Server.Power.Components; +using Content.Server.Vocalization.Components; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Language.EntitySystems; +using Content.Shared.Chat; + +namespace Content.Server.Vocalization.Systems; + +public sealed partial class VocalizationSystem +{ + [Dependency] private readonly SharedLanguageSystem _languageSystem = default!; + + private void InitializeLanguage() + { + SubscribeLocalEvent( + OnRequiresPowerTryVocalizeLanguage); + } + + private void OnRequiresPowerTryVocalizeLanguage(Entity ent, + ref TryVocalizeLanguageEvent args) + { + if (!TryComp(ent, out var receiver)) + return; + + args.Cancelled |= !receiver.Powered; + } + + private void TrySpeakLanguage(Entity entity) + { + var tryVocalizeLanguageEvent = new TryVocalizeLanguageEvent(); + RaiseLocalEvent(entity, ref tryVocalizeLanguageEvent); + + if (tryVocalizeLanguageEvent.Cancelled) + return; + + if (!tryVocalizeLanguageEvent.Handled) + return; + + if (tryVocalizeLanguageEvent.Message is not { } message) + return; + + var language = tryVocalizeLanguageEvent.Language ?? _languageSystem.GetCurrentLanguageEntity(entity); + if (language is null) + return; + + SpeakLanguage(entity, language.Value, message); + } + + private void SpeakLanguage(Entity entity, Entity language, string message) + { + var vocalizeLanguageEvent = new VocalizeLanguageEvent(message, language); + RaiseLocalEvent(entity, ref vocalizeLanguageEvent); + + if (vocalizeLanguageEvent.Handled) + return; + + // I skip the CanSpeak check because TrySendInGameICMessage does chat check anyway and it's not necessarily + // trivial with languages, doing it twice is pointless. + var cmplxMessage = _chat.ConvertMessageToComplex(message); + _chat.SendEntityComplexSpeech(entity, cmplxMessage, ChatSystem.SpeakWrapper, ChatChannel.Local, entity.Comp.HideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, languageOverride: language); + } +} + +/// +/// Fired when the entity wants to try vocalizing, but doesn't have a message yet. +/// +/// Message to send, this is null when the event is just fired and should be set by a system +/// Language to use, this is null when the event is just fired and may be set by a system +/// Whether the message was handled by a system +/// Prevents the vocalization attempt +[ByRefEvent] +public record struct TryVocalizeLanguageEvent(string? Message = null, Entity? Language = null, bool Handled = false, bool Cancelled = false); + +/// +/// Fired when the entity wants to vocalize and has a message. Allows for interception by other systems if the +/// vocalization needs to be done some other way +/// +/// Message to send +/// Language to use +/// Whether the message was handled by a system +[ByRefEvent] +public record struct VocalizeLanguageEvent(string Message, Entity Language, bool Handled = false); diff --git a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs index 485dc895804..0c86786bd73 100644 --- a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs +++ b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs @@ -19,7 +19,7 @@ namespace Content.Shared.ActionBlocker /// Utility methods to check if a specific entity is allowed to perform an action. /// [UsedImplicitly] - public sealed class ActionBlockerSystem : EntitySystem + public sealed partial class ActionBlockerSystem : EntitySystem // DEN: Made partial { [Dependency] private readonly SharedContainerSystem _container = default!; @@ -146,6 +146,7 @@ public bool CanThrow(EntityUid user, EntityUid itemUid) return !itemEv.Cancelled; } + [Obsolete("Use CanSpeakLanguage instead.", true)] // DEN: Languages public bool CanSpeak(EntityUid uid) { // This one is used as broadcast diff --git a/Content.Shared/Administration/AdminFrozenSystem.cs b/Content.Shared/Administration/AdminFrozenSystem.cs index ee0afb543a8..2c902f66cd0 100644 --- a/Content.Shared/Administration/AdminFrozenSystem.cs +++ b/Content.Shared/Administration/AdminFrozenSystem.cs @@ -13,7 +13,7 @@ namespace Content.Shared.Administration; // TODO deduplicate with BlockMovementComponent -public sealed class AdminFrozenSystem : EntitySystem +public sealed partial class AdminFrozenSystem : EntitySystem // DEN: Make partial { [Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] private readonly PullingSystem _pulling = default!; @@ -31,9 +31,11 @@ public override void Initialize() SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnEmoteAttempt); - SubscribeLocalEvent(OnSpeakAttempt); + //SubscribeLocalEvent(OnSpeakAttempt); // DEN: Languages, see SpeakLanguageAttemptEvent SubscribeLocalEvent(OnInGameOocMessageAttempt); SubscribeLocalEvent(OnInGameOocMessageAttemptBroadcast); + + InitializeLanguage(); // DEN: Languages } /// @@ -51,6 +53,7 @@ private void OnInteractAttempt(Entity ent, ref Interaction args.Cancelled = true; } + [Obsolete("Use OnSpeakLanguageAttempt instead.", true)] // DEN: Languages private void OnSpeakAttempt(EntityUid uid, AdminFrozenComponent component, SpeakAttemptEvent args) { if (!component.Muted) diff --git a/Content.Shared/Animals/Components/ParrotMemoryComponent.cs b/Content.Shared/Animals/Components/ParrotMemoryComponent.cs index e1c5ba8ed91..7befe6d7027 100644 --- a/Content.Shared/Animals/Components/ParrotMemoryComponent.cs +++ b/Content.Shared/Animals/Components/ParrotMemoryComponent.cs @@ -1,6 +1,8 @@ +using Content.Shared._DEN.Language; using Content.Shared.Animals.Systems; using Robust.Shared.GameStates; using Robust.Shared.Network; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; @@ -58,4 +60,4 @@ public sealed partial class ParrotMemoryComponent : Component } [Serializable, NetSerializable] -public record struct SpeechMemory(NetUserId? NetUserId, string Message); +public record struct SpeechMemory(NetUserId? NetUserId, string Message, NetEntity Language); // DEN: Languages diff --git a/Content.Shared/Bed/Sleep/SleepingSystem.cs b/Content.Shared/Bed/Sleep/SleepingSystem.cs index 661c8399a1c..bf47ef71b59 100644 --- a/Content.Shared/Bed/Sleep/SleepingSystem.cs +++ b/Content.Shared/Bed/Sleep/SleepingSystem.cs @@ -62,7 +62,7 @@ public override void Initialize() SubscribeLocalEvent(OnCompInit); SubscribeLocalEvent(OnComponentRemoved); SubscribeLocalEvent(OnRejuvenate); - SubscribeLocalEvent(OnSpeakAttempt); + //SubscribeLocalEvent(OnSpeakAttempt); // DEN: Languages, see SpeakLanguageAttemptEvent SubscribeLocalEvent(OnSeeAttempt); SubscribeLocalEvent(OnPointAttempt); SubscribeLocalEvent(OnSlip); @@ -78,6 +78,8 @@ public override void Initialize() SubscribeLocalEvent(OnEmoteAttempt); SubscribeLocalEvent(OnChangeForceSay, after: new []{typeof(PainNumbnessSystem)}); + + InitializeLanguage(); // DEN: Languages } private void OnUnbuckleAttempt(Entity ent, ref UnbuckleAttemptEvent args) @@ -161,6 +163,7 @@ private void OnComponentRemoved(Entity ent, ref ComponentRemo _blindableSystem.UpdateIsBlind(ent.Owner); } + [Obsolete("Use OnSpeakLanguageAttempt instead.", true)] // DEN: Languages private void OnSpeakAttempt(Entity ent, ref SpeakAttemptEvent args) { // TODO reduce duplication of this behavior with MobStateSystem somehow diff --git a/Content.Shared/Chat/SharedChatEvents.cs b/Content.Shared/Chat/SharedChatEvents.cs index 5701081b56f..c269f34012c 100644 --- a/Content.Shared/Chat/SharedChatEvents.cs +++ b/Content.Shared/Chat/SharedChatEvents.cs @@ -1,3 +1,4 @@ +using Content.Shared._DEN.Language; using Content.Shared.Inventory; using Content.Shared.Radio; using Content.Shared.Speech; @@ -9,7 +10,7 @@ namespace Content.Shared.Chat; /// This event should be sent everytime an entity talks (Radio, local chat, etc...). /// The event is sent to both the entity itself, and all clothing (For stuff like voice masks). /// -public sealed class TransformSpeakerNameEvent : EntityEventArgs, IInventoryRelayEvent +public sealed class TransformSpeakerNameEvent : EntityEventArgs, IInventoryRelayEvent, ISpokenLanguageRelayEvent // DEN: Languages { public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; public EntityUid Sender; @@ -55,6 +56,7 @@ public CheckIgnoreSpeechBlockerEvent(EntityUid sender, bool ignoreBlocker) /// /// Raised on an entity when it speaks, either through 'say' or 'whisper'. /// +[Obsolete("Use EntitySpokeLanguageEvent instead.", true)] // DEN: Languages public sealed class EntitySpokeEvent : EntityEventArgs { public readonly EntityUid Source; diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs index 4043725679d..aa89444445a 100644 --- a/Content.Shared/Chat/SharedChatSystem.cs +++ b/Content.Shared/Chat/SharedChatSystem.cs @@ -1,4 +1,5 @@ using System.Collections.Frozen; +using System.Text; using System.Text.RegularExpressions; using Content.Shared.ActionBlocker; using Content.Shared.Chat.Prototypes; @@ -215,6 +216,27 @@ private static string OopsConcat(string a, string b) return a + b; } + // DEN: Move this to shared. + protected string ObfuscateMessageReadability(string message, float chance) + { + var modifiedMessage = new StringBuilder(message); + + for (var i = 0; i < message.Length; i++) + { + if (char.IsWhiteSpace((modifiedMessage[i]))) + { + continue; + } + + if (_random.Prob(1 - chance)) + { + modifiedMessage[i] = '~'; + } + } + + return modifiedMessage.ToString(); + } + public string SanitizeMessageCapitalizeTheWordI(string message, string theWordI = "i") { if (string.IsNullOrEmpty(message)) diff --git a/Content.Shared/Chat/TypingIndicator/SharedTypingIndicatorSystem.cs b/Content.Shared/Chat/TypingIndicator/SharedTypingIndicatorSystem.cs index d67ab1d0fc5..0e77fe32825 100644 --- a/Content.Shared/Chat/TypingIndicator/SharedTypingIndicatorSystem.cs +++ b/Content.Shared/Chat/TypingIndicator/SharedTypingIndicatorSystem.cs @@ -31,7 +31,7 @@ public override void Initialize() SubscribeLocalEvent(OnGotUnequipped); SubscribeLocalEvent>(BeforeShow); - SubscribeAllEvent(OnTypingChanged); + SubscribeAllEvent(OnTypingLanguageChanged); // DEN: Languages } private void OnPlayerAttached(PlayerAttachedEvent ev) @@ -63,6 +63,7 @@ private void BeforeShow(Entity entity, ref Inv args.Args.TryUpdateTimeAndIndicator(entity.Comp.TypingIndicatorPrototype, entity.Comp.GotEquippedTime); } + [Obsolete("Use OnTypingLanguageChanged instead.", true)] // DEN: Languages, I dunno what to name this function. private void OnTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs args) { var uid = args.SenderSession.AttachedEntity; diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs index bfb9a41b0b0..7e102a978bb 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs @@ -24,6 +24,8 @@ private void InitializeRelay() SubscribeLocalEvent(RefRelayEvent); SubscribeLocalEvent(RefRelayEvent); SubscribeLocalEvent(RefRelayEvent); + + InitializeLanguage(); // DEN: Languages } private void RelayEvent(Entity entity, ref T args) where T : EntityEventArgs diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs index 24b76e15c56..af7fd9e15cc 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs @@ -17,6 +17,8 @@ public void InitializeRelay() SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); + + InitializeLanguage(); // DEN: Languages } /// diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index 63f63103227..dd7109635cc 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -133,5 +133,7 @@ public static BoundKeyFunction[] GetHotbarBoundKeys() => public static readonly BoundKeyFunction MappingRemoveDecal = "MappingRemoveDecal"; public static readonly BoundKeyFunction MappingCancelEraseDecal = "MappingCancelEraseDecal"; public static readonly BoundKeyFunction MappingOpenContextMenu = "MappingOpenContextMenu"; + + public static readonly BoundKeyFunction OpenLanguageMenu = "OpenLanguageMenu"; // DEN: Languages } } diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index 8101c19eab5..da5fbec5bee 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -102,6 +102,7 @@ public void InitializeRelay() SubscribeLocalEvent>(OnGetEquipmentVerbs); SubscribeLocalEvent>(OnGetInnateVerbs); + InitializeLanguage(); // DEN: Languages } protected void RefRelayInventoryEvent(EntityUid uid, InventoryComponent component, ref T args) where T : IInventoryRelayEvent diff --git a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs index b0a6eb8c808..fc97f6c5dd1 100644 --- a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs +++ b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs @@ -33,7 +33,7 @@ private void SubscribeEvents() SubscribeLocalEvent(CheckAct); SubscribeLocalEvent(CheckConcious); SubscribeLocalEvent(CheckAct); - SubscribeLocalEvent(OnSpeakAttempt); + //SubscribeLocalEvent(OnSpeakAttempt); // DEN: Languages, see SpeakLanguageAttemptEvent SubscribeLocalEvent(OnEquipAttempt); SubscribeLocalEvent(CheckAct); SubscribeLocalEvent(OnUnequipAttempt); @@ -49,6 +49,8 @@ private void SubscribeEvents() SubscribeLocalEvent(OnDamageModify); SubscribeLocalEvent(OnUnbuckleAttempt); + + InitializeLanguage(); // DEN: Languages } private void OnUnbuckleAttempt(Entity ent, ref UnbuckleAttemptEvent args) @@ -157,6 +159,7 @@ private void OnGettingStripped(EntityUid target, MobStateComponent component, Be args.Multiplier /= 2; } + [Obsolete("Use OnSpeakLanguageAttempt instead.", true)] // DEN: Languages private void OnSpeakAttempt(EntityUid uid, MobStateComponent component, SpeakAttemptEvent args) { if (HasComp(uid)) diff --git a/Content.Shared/Speech/ListenEvent.cs b/Content.Shared/Speech/ListenEvent.cs index 8854bd99f42..fe487511853 100644 --- a/Content.Shared/Speech/ListenEvent.cs +++ b/Content.Shared/Speech/ListenEvent.cs @@ -1,5 +1,6 @@ namespace Content.Shared.Speech; +[Obsolete("Use ListenLanguageEvent instead.", true)] // DEN: Languages public sealed class ListenEvent : EntityEventArgs { public readonly string Message; @@ -12,6 +13,7 @@ public ListenEvent(string message, EntityUid source) } } +[Obsolete("Use ListenLanguageAttemptEvent instead.", true)] // DEN: Languages public sealed class ListenAttemptEvent : CancellableEntityEventArgs { public readonly EntityUid Source; diff --git a/Content.Shared/Speech/SpeakAttemptEvent.cs b/Content.Shared/Speech/SpeakAttemptEvent.cs index c76ef4df256..2e5f1736c59 100644 --- a/Content.Shared/Speech/SpeakAttemptEvent.cs +++ b/Content.Shared/Speech/SpeakAttemptEvent.cs @@ -1,5 +1,8 @@ -namespace Content.Shared.Speech +using Content.Shared._DEN.Language; + +namespace Content.Shared.Speech { + [Obsolete("Use SpeakLanguageAttemptEvent instead", true)] // DEN: languages public sealed class SpeakAttemptEvent : CancellableEntityEventArgs { public SpeakAttemptEvent(EntityUid uid) diff --git a/Content.Shared/Speech/SpeechSystem.cs b/Content.Shared/Speech/SpeechSystem.cs index 77e3b7ef027..d98ccdf70ad 100644 --- a/Content.Shared/Speech/SpeechSystem.cs +++ b/Content.Shared/Speech/SpeechSystem.cs @@ -1,12 +1,13 @@ namespace Content.Shared.Speech { - public sealed class SpeechSystem : EntitySystem + public sealed partial class SpeechSystem : EntitySystem // DEN: Make partial { public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnSpeakAttempt); + //SubscribeLocalEvent(OnSpeakAttempt); // DEN: Languages, see SpeakLanguageAttemptEvent + InitializeLanguage(); // DEN: Languages } public void SetSpeech(EntityUid uid, bool value, SpeechComponent? component = null) @@ -24,6 +25,7 @@ public void SetSpeech(EntityUid uid, bool value, SpeechComponent? component = nu Dirty(uid, component); } + [Obsolete("Use OnSpeakLanguageAttempt instead.", true)] // DEN: Languages private void OnSpeakAttempt(SpeakAttemptEvent args) { if (!TryComp(args.Uid, out SpeechComponent? speech) || !speech.Enabled) diff --git a/Content.Shared/Telephone/TelephoneComponent.cs b/Content.Shared/Telephone/TelephoneComponent.cs index 89748d78a45..34679816cd7 100644 --- a/Content.Shared/Telephone/TelephoneComponent.cs +++ b/Content.Shared/Telephone/TelephoneComponent.cs @@ -166,12 +166,14 @@ public record struct TelephoneCallEndedEvent(); /// /// Raised when a chat message is sent by a telephone to another /// +[Obsolete("Use TelephoneMessageLanguageSentEvent instead.", true)] // DEN: Languages [ByRefEvent] public readonly record struct TelephoneMessageSentEvent(string Message, MsgChatMessage ChatMsg, EntityUid MessageSource); /// /// Raised when a chat message is received by a telephone from another /// +[Obsolete("Use TelephoneMessageLanguageReceivedEvent instead.", true)] // DEN: Languages [ByRefEvent] public readonly record struct TelephoneMessageReceivedEvent(string Message, MsgChatMessage ChatMsg, EntityUid MessageSource, Entity TelephoneSource); diff --git a/Content.Shared/Trigger/Systems/TriggerSystem.Voice.cs b/Content.Shared/Trigger/Systems/TriggerSystem.Voice.cs index a312b6aa282..58edd6eb50a 100644 --- a/Content.Shared/Trigger/Systems/TriggerSystem.Voice.cs +++ b/Content.Shared/Trigger/Systems/TriggerSystem.Voice.cs @@ -1,9 +1,11 @@ +using Content.Shared._DEN.Language; using Content.Shared.Trigger.Components.Triggers; using Content.Shared.Speech; using Content.Shared.Speech.Components; using Content.Shared.Database; using Content.Shared.Examine; using Content.Shared.Verbs; +using Robust.Shared.Prototypes; namespace Content.Shared.Trigger.Systems; @@ -13,8 +15,10 @@ private void InitializeVoice() { SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnVoiceExamine); - SubscribeLocalEvent(OnListen); + //SubscribeLocalEvent(OnListen); // DEN: Languages, see ListenLanguageEvent SubscribeLocalEvent>(OnVoiceGetAltVerbs); + + InitializeLanguage(); // DEN: Languages } private void OnMapInit(Entity ent, ref MapInitEvent args) @@ -40,9 +44,17 @@ private void OnVoiceExamine(EntityUid uid, TriggerOnVoiceComponent component, Ex else if (component.InspectInitializedLoc != null && !string.IsNullOrWhiteSpace(component.KeyPhrase)) { args.PushText(Loc.GetString(component.InspectInitializedLoc.Value, ("keyphrase", component.KeyPhrase))); + // DEN: Display what language was recorded. + if (component.KeyLanguage is { } keyLangId) + { + var keyLang = _proto.Index(keyLangId); + args.PushText(Loc.GetString("language-trigger-on-voice-examine", ("language", keyLang.Abbreviation))); + } } } + /* // DEN: Gotta comment this all out because it doesn't compile anymore. + [Obsolete("Use OnListenLanguage instead.", true)] // DEN: Languages private void OnListen(Entity ent, ref ListenEvent args) { var component = ent.Comp; @@ -77,6 +89,7 @@ private void OnListen(Entity ent, ref ListenEvent args) RaiseLocalEvent(ent, ref voice); } } + */ private void OnVoiceGetAltVerbs(Entity ent, ref GetVerbsEvent args) { @@ -168,9 +181,10 @@ public void StopRecording(Entity ent, EntityUid? user) /// /// Stop recording and set the current keyphrase message. /// - public void FinishRecording(Entity ent, EntityUid source, string message) + public void FinishRecording(Entity ent, EntityUid source, string message, ProtoId language) // DEN: Languages { ent.Comp.KeyPhrase = message; + ent.Comp.KeyLanguage = language; // DEN: Languages ent.Comp.IsRecording = false; Dirty(ent); @@ -186,6 +200,7 @@ public void FinishRecording(Entity ent, EntityUid sourc public void ClearRecording(Entity ent) { ent.Comp.KeyPhrase = null; + ent.Comp.KeyLanguage = null; // DEN: Languages ent.Comp.IsRecording = false; Dirty(ent); RemComp(ent); @@ -200,6 +215,7 @@ public void SetToDefault(Entity ent, EntityUid? user = return; ent.Comp.KeyPhrase = Loc.GetString(ent.Comp.DefaultKeyPhrase); + ent.Comp.KeyLanguage = ent.Comp.DefaultKeyLanguage; // DEN: Languages ent.Comp.IsRecording = false; Dirty(ent); UpdateListening(ent); diff --git a/Content.Shared/_DEN/ActionBlocker/ActionBlockerSystem.Language.cs b/Content.Shared/_DEN/ActionBlocker/ActionBlockerSystem.Language.cs new file mode 100644 index 00000000000..cd2311d0b5a --- /dev/null +++ b/Content.Shared/_DEN/ActionBlocker/ActionBlockerSystem.Language.cs @@ -0,0 +1,16 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Speech; +using Content.Shared.Chat; + +namespace Content.Shared.ActionBlocker; + +public sealed partial class ActionBlockerSystem +{ + public bool CanSpeakLanguage(EntityUid uid, Entity language, ChatChannel? channel = null) + { + var ev = new SpeakLanguageAttemptEvent(uid, language, channel); + RaiseLocalEvent(uid, ev, true); + + return !ev.Cancelled; + } +} diff --git a/Content.Shared/_DEN/Administration/AdminFrozenSystem.Language.cs b/Content.Shared/_DEN/Administration/AdminFrozenSystem.Language.cs new file mode 100644 index 00000000000..78f8900956c --- /dev/null +++ b/Content.Shared/_DEN/Administration/AdminFrozenSystem.Language.cs @@ -0,0 +1,19 @@ +using Content.Shared._DEN.Speech; + +namespace Content.Shared.Administration; + +public sealed partial class AdminFrozenSystem +{ + public void InitializeLanguage() + { + SubscribeLocalEvent(OnSpeakLanguageAttempt); + } + + public void OnSpeakLanguageAttempt(Entity ent, ref SpeakLanguageAttemptEvent args) + { + if (!ent.Comp.Muted) + return; + + args.Cancel(); + } +} diff --git a/Content.Shared/_DEN/Bed/Sleep/SleepingSystem.Language.cs b/Content.Shared/_DEN/Bed/Sleep/SleepingSystem.Language.cs new file mode 100644 index 00000000000..d806a0bd972 --- /dev/null +++ b/Content.Shared/_DEN/Bed/Sleep/SleepingSystem.Language.cs @@ -0,0 +1,31 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Speech; +using Content.Shared.Damage.ForceSay; + +namespace Content.Shared.Bed.Sleep; + +public sealed partial class SleepingSystem +{ + private EntityQuery _unconsciousLanguageQuery; + + public void InitializeLanguage() + { + _unconsciousLanguageQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnSpeakLanguageAttempt); + } + + private void OnSpeakLanguageAttempt(Entity ent, ref SpeakLanguageAttemptEvent args) + { + if (_unconsciousLanguageQuery.HasComp(args.LanguageEnt)) + return; + + if (HasComp(ent)) + { + RemCompDeferred(ent); + return; + } + + args.Cancel(); + } +} diff --git a/Content.Shared/_DEN/CCVars/DenCCVars.cs b/Content.Shared/_DEN/CCVars/DenCCVars.cs new file mode 100644 index 00000000000..dafe63459aa --- /dev/null +++ b/Content.Shared/_DEN/CCVars/DenCCVars.cs @@ -0,0 +1,37 @@ +using Robust.Shared.Configuration; + +namespace Content.Shared._DEN.CCVars; + +[CVarDefs] +public sealed class DenCCVars +{ + /// + /// The maximum number of message translations to cache at a time. + /// The total size will cap out at this times the number of languages times the number of + /// different 'understanding' variants in use. + /// + public static readonly CVarDef LanguageMessageCacheSize = + CVarDef.Create("languages.message_cache_size", 20, CVar.ARCHIVE); + + /// + /// The number of words to keep in the word cache at a time. + /// + public static readonly CVarDef LanguageWordCacheSize = + CVarDef.Create("languages.word_cache_size", 50, CVar.ARCHIVE); + + /// + /// Whether or not to give an entity that tries speaking without LanguageCommunicatorComponent a language. + /// + public static readonly CVarDef FallbackDefaultLanguage = + CVarDef.Create("languages.fallback_default_language", false, CVar.ARCHIVE); + + /// + /// The default spoken language. If fallback_default_language is set, entities without LanguageCommunicatorComponent + /// will use this. Systems that directly send messages will also use this language. + /// + public static readonly CVarDef DefaultLanguage = + CVarDef.Create("languages.default_language", "Basic", CVar.ARCHIVE); + + public static readonly CVarDef HideLanguageFonts = + CVarDef.Create("languages.hide_fonts", false, CVar.CLIENTONLY | CVar.ARCHIVE); +} diff --git a/Content.Shared/_DEN/Chat/SharedChatSystem.Language.cs b/Content.Shared/_DEN/Chat/SharedChatSystem.Language.cs new file mode 100644 index 00000000000..6132368120d --- /dev/null +++ b/Content.Shared/_DEN/Chat/SharedChatSystem.Language.cs @@ -0,0 +1,138 @@ +using System.Linq; +using System.Text; +using Content.Shared._DEN.Language; +using Content.Shared.Speech; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared.Chat; + +public abstract partial class SharedChatSystem +{ + + // TODO: Kill the other spot where this is getting called from and more this into WhisperMuffle (if we even keep using it) + public ComplexChatMessage ObfuscateComplexChatMessage(ComplexChatMessage message, float amount) + { + var newParts = new List<(ChatPart, string)>(); + foreach (var (kind, text) in message.Parts) + { + if (kind == ChatPart.Dialog) + { + var newText = ObfuscateMessageReadability(text, amount); + newParts.Add((kind, newText)); + } + else + { + newParts.Add((kind, text)); + } + } + + return new ComplexChatMessage(message, newParts); + } + + public SpeechVerbPrototype GetComplexSpeechVerb(EntityUid source, ComplexChatMessage message, LanguagePrototype language, ChatChannel channel) + { + var lastDialog = message.Parts.LastOrDefault(p => p.Item1 == ChatPart.Dialog).Item2; + + SpeechVerbPrototype? current = null; + Dictionary>? currentSuffixVerbs = null; + if (language.SpeechVerbs is { } speechVerbs) + { + if (speechVerbs.TryGetValue(channel, out var channelVerbs)) + { + current = _prototypeManager.Index(channelVerbs.DefaultVerb); + currentSuffixVerbs = channelVerbs.SuffixSpeechVerbs; + } + } + + if (currentSuffixVerbs is not null) + { + foreach (var (str, id) in currentSuffixVerbs) + { + var proto = _prototypeManager.Index(id); + if (lastDialog.EndsWith(Loc.GetString(str)) && proto.Priority >= (current?.Priority ?? 0)) + { + current = proto; + } + } + } + + // if no applicable suffix verb return the normal one used by the entity + return current ?? GetSpeechVerb(source, lastDialog); + } + + public ComplexChatMessage ConvertMessageToComplex(string message) + { + var isDetailed = false; + var needsSpacing = true; + var needsSeparation = false; + if (message.StartsWith('!')) + { + isDetailed = true; + message = message[1..].Trim(); + if (message.StartsWith('"')) + { + needsSeparation = true; + } + else if (message.StartsWith(',') || message.StartsWith('\'')) + { + needsSpacing = false; + } + } + + return new ComplexChatMessage(message, "\"", isDetailed, needsSpacing, needsSeparation); + } +} + +public enum ChatPart +{ + Dialog, + Emote, +} + +public readonly record struct ComplexChatMessage() +{ + public readonly string OriginalMessage = string.Empty; + public readonly IReadOnlyList<(ChatPart, string)> Parts = []; + public readonly string Delimiter = string.Empty; + public readonly bool IsDetailed; + public readonly bool NeedsSpacing; + public readonly bool NeedsSeparation; + + public ComplexChatMessage(ComplexChatMessage primary, IReadOnlyList<(ChatPart, string)> parts) : this() + { + OriginalMessage = primary.OriginalMessage; + Delimiter = primary.Delimiter; + IsDetailed = primary.IsDetailed; + NeedsSpacing = primary.NeedsSpacing; + NeedsSeparation = primary.NeedsSeparation; + Parts = parts; + } + + public ComplexChatMessage(string message, string delimiter, bool isDetailed, bool needsSpacing, bool needsSeparation, bool escapeMarkup = true) : this() + { + OriginalMessage = message; + Delimiter = delimiter; + IsDetailed = isDetailed; + NeedsSpacing = needsSpacing; + NeedsSeparation = needsSeparation; + if (escapeMarkup) + message = FormattedMessage.EscapeText(message); + if (!isDetailed) + { + Parts = [(ChatPart.Dialog, message)]; + return; + } + + var outside = false; + List<(ChatPart, string)> parts = []; + foreach (var msgChunk in message.Split(delimiter)) + { + if (!string.IsNullOrEmpty(msgChunk)) + parts.Add((outside ? ChatPart.Dialog : ChatPart.Emote, msgChunk)); + outside = !outside; + } + + Parts = parts; + } +} diff --git a/Content.Shared/_DEN/Chat/TypingIndicator/SharedTypingIndicatorSystem.Language.cs b/Content.Shared/_DEN/Chat/TypingIndicator/SharedTypingIndicatorSystem.Language.cs new file mode 100644 index 00000000000..6067552946a --- /dev/null +++ b/Content.Shared/_DEN/Chat/TypingIndicator/SharedTypingIndicatorSystem.Language.cs @@ -0,0 +1,34 @@ +using Content.Shared._DEN.Language.EntitySystems; + +namespace Content.Shared.Chat.TypingIndicator; + +public abstract partial class SharedTypingIndicatorSystem +{ + [Dependency] private readonly SharedLanguageSystem _language = default!; + + private void OnTypingLanguageChanged(TypingChangedEvent ev, EntitySessionEventArgs args) + { + var uid = args.SenderSession.AttachedEntity; + if (!Exists(uid)) + { + Log.Warning($"Client {args.SenderSession} sent TypingChangedEvent without an attached entity."); + return; + } + + var languageEnt = _language.GetCurrentLanguageEntity(uid.Value); + // Don't send typing if they have no valid language. + // See DenCCVars.FallbackDefaultLanguage if you need some weird entity to have an indicator. + if (languageEnt == null) + return; + + // check if this entity can speak or emote + if (!_actionBlocker.CanEmote(uid.Value) && !_actionBlocker.CanSpeakLanguage(uid.Value, languageEnt.Value)) + { + // nah, make sure that typing indicator is disabled + SetTypingIndicatorState(uid.Value, TypingIndicatorState.None); + return; + } + + SetTypingIndicatorState(uid.Value, ev.State); + } +} diff --git a/Content.Shared/_DEN/Hands/EntitySystems/SharedHandsSystem.Relay.Language.cs b/Content.Shared/_DEN/Hands/EntitySystems/SharedHandsSystem.Relay.Language.cs new file mode 100644 index 00000000000..8ab9dffff0a --- /dev/null +++ b/Content.Shared/_DEN/Hands/EntitySystems/SharedHandsSystem.Relay.Language.cs @@ -0,0 +1,13 @@ +using Content.Shared._DEN.Language; +using Content.Shared.Hands.Components; + +namespace Content.Shared.Hands.EntitySystems; + +public abstract partial class SharedHandsSystem +{ + private void InitializeLanguage() + { + SubscribeLocalEvent(RefRelayEvent); + SubscribeLocalEvent(RefRelayEvent); + } +} diff --git a/Content.Shared/_DEN/Implants/SharedSubdermalImplantSystem.Relays.Language.cs b/Content.Shared/_DEN/Implants/SharedSubdermalImplantSystem.Relays.Language.cs new file mode 100644 index 00000000000..79a58282442 --- /dev/null +++ b/Content.Shared/_DEN/Implants/SharedSubdermalImplantSystem.Relays.Language.cs @@ -0,0 +1,13 @@ +using Content.Shared._DEN.Language; +using Content.Shared.Implants.Components; + +namespace Content.Shared.Implants; + +public abstract partial class SharedSubdermalImplantSystem +{ + private void InitializeLanguage() + { + SubscribeLocalEvent(RelayToImplantEvent); + SubscribeLocalEvent(RelayToImplantEvent); + } +} diff --git a/Content.Shared/_DEN/Inventory/InventorySystem.Relay.Languages.cs b/Content.Shared/_DEN/Inventory/InventorySystem.Relay.Languages.cs new file mode 100644 index 00000000000..88f161fa230 --- /dev/null +++ b/Content.Shared/_DEN/Inventory/InventorySystem.Relay.Languages.cs @@ -0,0 +1,12 @@ +using Content.Shared._DEN.Language; + +namespace Content.Shared.Inventory; + +public partial class InventorySystem +{ + private void InitializeLanguage() + { + SubscribeLocalEvent(RefRelayInventoryEvent); + SubscribeLocalEvent(RefRelayInventoryEvent); + } +} diff --git a/Content.Shared/_DEN/Language/Components/AudibleComponent.cs b/Content.Shared/_DEN/Language/Components/AudibleComponent.cs new file mode 100644 index 00000000000..4ace8471e56 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/AudibleComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared._DEN.Language.Components; + +/// +/// Marks a language as being audible. Things that listen for speech such as triggers and parrots care about this. +/// +[RegisterComponent] +public sealed partial class AudibleComponent : Component; diff --git a/Content.Shared/_DEN/Language/Components/ChatChannelWhitelistComponent.cs b/Content.Shared/_DEN/Language/Components/ChatChannelWhitelistComponent.cs new file mode 100644 index 00000000000..5b2466d24cf --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/ChatChannelWhitelistComponent.cs @@ -0,0 +1,28 @@ +using Content.Shared.Chat; + +namespace Content.Shared._DEN.Language.Components; + +/// +/// Makes a language only speakable in certain channels, or prevents it from being spoken in certain channels. +/// +[RegisterComponent] +public sealed partial class ChatChannelWhitelistComponent : Component +{ + /// + /// The set of chat channels that are valid for this language. Channels are assumed valid if this is not present. + /// + [DataField] + public List? Whitelist; + + /// + /// The set of channels that are not valid for this language. Channels are assumed valid if this is not present. + /// + [DataField] + public List? Blacklist; + + /// + /// Messages to pop up to the user when they try speaking in the wrong channel. + /// + [DataField] + public List FailureMessages = []; +} diff --git a/Content.Shared/_DEN/Language/Components/ChildLanguageComponent.cs b/Content.Shared/_DEN/Language/Components/ChildLanguageComponent.cs new file mode 100644 index 00000000000..f965daf94e9 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/ChildLanguageComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared._DEN.Language.EntitySystems; +using Robust.Shared.GameStates; + +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedLanguageSystem))] +public sealed partial class ChildLanguageComponent : Component +{ + [AutoNetworkedField] + [ViewVariables(VVAccess.ReadOnly)] + public EntityUid ParentLanguage; +} diff --git a/Content.Shared/_DEN/Language/Components/GestaltComponent.cs b/Content.Shared/_DEN/Language/Components/GestaltComponent.cs new file mode 100644 index 00000000000..f3a80eb189a --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/GestaltComponent.cs @@ -0,0 +1,29 @@ +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; + +namespace Content.Shared._DEN.Language.Components; + +/// +/// Gestalt languages transmit to all available players and rely on the languages understanding to filter +/// out players who do not speak the language. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class GestaltComponent : Component +{ + [DataField] + public bool RequiresHost; + + [DataField] + public EntityWhitelist? ReceiverWhitelist; + + /// + /// Entity whitelist that a host must pass to count. Hosts must also always have a GestaltHostComponent just so + /// that GestaltSystem doesn't have to search every entity in the game for a host. If a host can be alive, it + /// must be alive. + /// + [DataField] + public EntityWhitelist? HostWhitelist; + + [DataField] + public List MissingHostPopups = []; +} diff --git a/Content.Shared/_DEN/Language/Components/GestaltHostComponent.cs b/Content.Shared/_DEN/Language/Components/GestaltHostComponent.cs new file mode 100644 index 00000000000..9c9533a9350 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/GestaltHostComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent] +public sealed partial class GestaltHostComponent : Component; diff --git a/Content.Shared/_DEN/Language/Components/LanguageCommunicatorComponent.cs b/Content.Shared/_DEN/Language/Components/LanguageCommunicatorComponent.cs new file mode 100644 index 00000000000..1e3ed6f635e --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/LanguageCommunicatorComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.Containers; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[Access(typeof(EntitySystems.SharedLanguageSystem))] +public sealed partial class LanguageCommunicatorComponent : Component +{ + public const string ContainerId = "languages"; + + [ViewVariables] + public Container? Languages; + + [AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)] + public EntityUid? CurrentLanguage; + + [AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)] + public ProtoId? LastSpokenLanguage; + + [AlwaysPushInheritance] + [DataField("languages")] + public Dictionary, (bool, ProtoId)> BaseLanguages = new(); +} diff --git a/Content.Shared/_DEN/Language/Components/LanguageComponent.cs b/Content.Shared/_DEN/Language/Components/LanguageComponent.cs new file mode 100644 index 00000000000..ad9098cc749 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/LanguageComponent.cs @@ -0,0 +1,32 @@ +using Content.Shared._DEN.Language.EntitySystems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[Access(typeof(SharedLanguageSystem))] +public sealed partial class LanguageComponent : Component +{ + [AutoNetworkedField] + [ViewVariables(VVAccess.ReadOnly)] + public ProtoId Language; + + [AutoNetworkedField] + [ViewVariables(VVAccess.ReadOnly)] + public ProtoId Fluency; + + // Maybe should be tied to fluency, but it could be useful for this to be asymmetric later. + [AutoNetworkedField] + [ViewVariables(VVAccess.ReadOnly)] + public bool Speaks; + + // The entity currently holding this language. + [AutoNetworkedField] + [ViewVariables(VVAccess.ReadOnly)] + public EntityUid? Holder; + + [AutoNetworkedField] + [ViewVariables(VVAccess.ReadOnly)] + public List Children = new(); +} diff --git a/Content.Shared/_DEN/Language/Components/LanguageFontSuppressionComponent.cs b/Content.Shared/_DEN/Language/Components/LanguageFontSuppressionComponent.cs new file mode 100644 index 00000000000..1ee3e881797 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/LanguageFontSuppressionComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared._DEN.Language.Components; + +/// +/// Marks a user as desiring not to see the language font on languages they understand. +/// +[RegisterComponent] +public sealed partial class LanguageFontSuppressionComponent : Component; diff --git a/Content.Shared/_DEN/Language/Components/LineOfSightLanguageComponent.cs b/Content.Shared/_DEN/Language/Components/LineOfSightLanguageComponent.cs new file mode 100644 index 00000000000..f197b614a0a --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/LineOfSightLanguageComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared._DEN.Language.Components; + +/// +/// Indicates that line of sight is required in order to understand a language, for, IE, sign. +/// +[RegisterComponent] +public sealed partial class LineOfSightLanguageComponent : Component; diff --git a/Content.Shared/_DEN/Language/Components/MinimumFluencyComponent.cs b/Content.Shared/_DEN/Language/Components/MinimumFluencyComponent.cs new file mode 100644 index 00000000000..d05712e8644 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/MinimumFluencyComponent.cs @@ -0,0 +1,14 @@ +using Content.Shared.Chat; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent] +public sealed partial class MinimumFluencyComponent : Component +{ + [DataField("minimum", required: true)] + public ProtoId MinimumFluency; + + [DataField] + public Dictionary>? Replacements; +} diff --git a/Content.Shared/_DEN/Language/Components/RadioTransmittableComponent.cs b/Content.Shared/_DEN/Language/Components/RadioTransmittableComponent.cs new file mode 100644 index 00000000000..68b268ba9e4 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/RadioTransmittableComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent] +public sealed partial class RadioTransmittableComponent : Component; diff --git a/Content.Shared/_DEN/Language/Components/ReplaceSpeakerNameComponent.cs b/Content.Shared/_DEN/Language/Components/ReplaceSpeakerNameComponent.cs new file mode 100644 index 00000000000..15e63329ae1 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/ReplaceSpeakerNameComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent] +public sealed partial class ReplaceSpeakerNameComponent : Component +{ + [DataField] + public string? ReplaceName; +} diff --git a/Content.Shared/_DEN/Language/Components/SpeechTransformableComponent.cs b/Content.Shared/_DEN/Language/Components/SpeechTransformableComponent.cs new file mode 100644 index 00000000000..3b2071aa881 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/SpeechTransformableComponent.cs @@ -0,0 +1,5 @@ +namespace Content.Shared._DEN.Language.Components; + +// Marker component that a language is verbally spoken. +[RegisterComponent] +public sealed partial class SpeechTransformableComponent : Component; diff --git a/Content.Shared/_DEN/Language/Components/SyllableScramblingComponent.cs b/Content.Shared/_DEN/Language/Components/SyllableScramblingComponent.cs new file mode 100644 index 00000000000..2705331c437 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/SyllableScramblingComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared._DEN.Language.EntitySystems; + +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent] +[Access(typeof(SharedSyllableScramblingSystem))] +public sealed partial class SyllableScramblingComponent : Component +{ + [DataField] + public int MinSyllables { get; private set; } = 1; + + [DataField] + public int MaxSyllables { get; private set; } = 3; + + [DataField(required: true)] + public List Syllables { get; private set; } = new(); +} diff --git a/Content.Shared/_DEN/Language/Components/TranslatedLanguageComponent.cs b/Content.Shared/_DEN/Language/Components/TranslatedLanguageComponent.cs new file mode 100644 index 00000000000..9db46ec3720 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/TranslatedLanguageComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class TranslatedLanguageComponent : Component; diff --git a/Content.Shared/_DEN/Language/Components/TranslatorComponent.cs b/Content.Shared/_DEN/Language/Components/TranslatorComponent.cs new file mode 100644 index 00000000000..dbda5dc5e4c --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/TranslatorComponent.cs @@ -0,0 +1,38 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent] +public sealed partial class TranslatorComponent : Component +{ + public static readonly float DefaultWattage = 0.4f; // ~30 minutes on a medium power cell. + + /// + /// The language this translator needs the user to be able to speak. + /// + [DataField("requires")] + [ViewVariables(VVAccess.ReadWrite)] + public ProtoId? RequiredLanguage; + + /// + /// Languages granted by this translator, as well as their fluency and if they can be spoken. + /// + [DataField("grants")] + [ViewVariables(VVAccess.ReadWrite)] + public Dictionary, (bool, ProtoId)> GrantedLanguageProtos = new(); + + /// + /// The amount of power that this translator drains, assuming it uses any. Defaults to `DefaultWattage` if it + /// does use power and this isn't set. + /// + [DataField] + public float? Wattage; + + /// + /// Actual language entities currently from this translator. + /// + [ViewVariables(VVAccess.ReadOnly)] + public List> GrantedLanguages = new(); + + public EntityUid? CurrentlyGrantingTo; +} diff --git a/Content.Shared/_DEN/Language/Components/UnconsciousLanguageComponent.cs b/Content.Shared/_DEN/Language/Components/UnconsciousLanguageComponent.cs new file mode 100644 index 00000000000..46d3504a390 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/UnconsciousLanguageComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared._DEN.Language.Components; + +/// +/// Indicates that a language can be spoken while unconscious, namely, while asleep. +/// +[RegisterComponent] +public sealed partial class UnconsciousLanguageComponent : Component; diff --git a/Content.Shared/_DEN/Language/Components/UniversalLanguageSpeakerComponent.cs b/Content.Shared/_DEN/Language/Components/UniversalLanguageSpeakerComponent.cs new file mode 100644 index 00000000000..af1a7474667 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/UniversalLanguageSpeakerComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent] +public sealed partial class UniversalLanguageSpeakerComponent : Component +{ + [ViewVariables(VVAccess.ReadOnly)] + public Entity UniversalLanguage; +} diff --git a/Content.Shared/_DEN/Language/Components/VisualNameComponent.cs b/Content.Shared/_DEN/Language/Components/VisualNameComponent.cs new file mode 100644 index 00000000000..80aeb3ae3ca --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/VisualNameComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent] +public sealed partial class VisualNameComponent : Component; diff --git a/Content.Shared/_DEN/Language/Components/WhisperMuffleComponent.cs b/Content.Shared/_DEN/Language/Components/WhisperMuffleComponent.cs new file mode 100644 index 00000000000..433dcf64da9 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/WhisperMuffleComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent] +public sealed partial class WhisperMuffleComponent : Component +{ + /// + /// Whether to muffle or simply completely hide the message. + /// + [DataField] + public bool Muffle; + + [DataField] + public float MuffleAmount = 0.2f; +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/ChatChannelWhitelistSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/ChatChannelWhitelistSystem.cs new file mode 100644 index 00000000000..e0c8e202765 --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/ChatChannelWhitelistSystem.cs @@ -0,0 +1,44 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Speech; +using Content.Shared.Popups; +using Robust.Shared.Random; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public sealed partial class ChatChannelWhitelistSystem : EntitySystem +{ + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + SubscribeLocalEvent>( + OnSpeakLanguageAttempt); + } + + private void OnSpeakLanguageAttempt(Entity ent, + ref LanguageRelayedEvent args) + { + var evt = args.Args; + + // If a channel isn't passed to the event then assume speaking is still possible until proven otherwise. + // The message sending system will always provide a channel, so this is fine and we don't want to interfere + // with cases where, for example, the person is typing but hasn't selected a channel yet. + if (evt.Channel is null) + return; + + if (ent.Comp.Whitelist is { } whitelist && whitelist.Contains(evt.Channel.Value)) + return; + + if (!(ent.Comp.Blacklist is { } blacklist && blacklist.Contains(evt.Channel.Value))) + return; + + if (ent.Comp.FailureMessages.Count > 0) + { + Log.Debug("Doing popup: " + Name(args.Owner)); + _popup.PopupEntity(Loc.GetString(_random.Pick(ent.Comp.FailureMessages)), args.Owner, args.Owner); + } + + evt.Cancel(); + } +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/ChildLanguageSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/ChildLanguageSystem.cs new file mode 100644 index 00000000000..61fea2086b8 --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/ChildLanguageSystem.cs @@ -0,0 +1,32 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared.Examine; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public sealed class ChildLanguageSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedLanguageSystem _language = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnChildLanguageExamined); + + SubscribeLocalEvent(OnChildLanguageStartup); + } + + private void OnChildLanguageStartup(Entity childLang, ref ComponentStartup args) + { + _language.OnLanguageUpdated(childLang.AsType()); + } + + private void OnChildLanguageExamined(Entity lang, ref ExaminedEvent args) + { + if (!TryComp(lang.Comp.ParentLanguage, out var parentLanguage)) + return; + + var parentLang = _proto.Index(parentLanguage.Language); + args.PushMarkup(Loc.GetString("language-child-language-examine", ("parent", parentLang.LocalizedName))); + } +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/LanguageRelaySystem.cs b/Content.Shared/_DEN/Language/EntitySystems/LanguageRelaySystem.cs new file mode 100644 index 00000000000..7df8cdbb3d3 --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/LanguageRelaySystem.cs @@ -0,0 +1,53 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Speech; +using Content.Shared.Chat; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public sealed partial class LanguageRelaySystem : EntitySystem +{ + public override void Initialize() + { + SubscribeLocalEvent(RelayKnownLanguagesEvent); + SubscribeLocalEvent(RelaySpokenLanguageEvent); + SubscribeLocalEvent(RelaySpokenLanguageEvent); + SubscribeLocalEvent(RelaySpokenLanguageEvent); + } + + private void RelayKnownLanguagesEvent(EntityUid uid, LanguageCommunicatorComponent comp, T args) + where T : IKnownLanguagesRelayEvent + { + RelayKnownEvent((uid, comp), ref args); + } + + private void RelaySpokenLanguageEvent(EntityUid uid, LanguageCommunicatorComponent comp, T args) + where T : ISpokenLanguageRelayEvent + { + RelaySpokenEvent((uid, comp), ref args); + } + + private void RelaySpokenEvent(Entity ent, ref T args) + where T : ISpokenLanguageRelayEvent + { + var ev = new LanguageRelayedEvent(ent, args); + if (ent.Comp.CurrentLanguage != null) + { + RaiseLocalEvent(ent.Comp.CurrentLanguage.Value, ev); + } + args = ev.Args; + } + + private void RelayKnownEvent(Entity ent, ref T args) where T : IKnownLanguagesRelayEvent + { + var ev = new LanguageRelayedEvent(ent, args); + if (ent.Comp.Languages != null) + { + foreach (var langEnt in ent.Comp.Languages.ContainedEntities) + { + RaiseLocalEvent(langEnt, ev); + } + } + + args = ev.Args; + } +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/LineOfSightLanguageSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/LineOfSightLanguageSystem.cs new file mode 100644 index 00000000000..f7c534640da --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/LineOfSightLanguageSystem.cs @@ -0,0 +1,37 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; +using Content.Shared.Ghost; +using Content.Shared.Interaction; +using Content.Shared.Physics; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public sealed partial class LineOfSightLanguageSystem : EntitySystem +{ + [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; + + private readonly CollisionGroup _sightMask = CollisionGroup.Opaque; + + private EntityQuery _ghostHearings; + + public override void Initialize() + { + _ghostHearings = GetEntityQuery(); + + SubscribeLocalEvent( + OnModifyMessage); + } + + private void OnModifyMessage(Entity entity, + ref LanguageModifyMessageEvent evt) + { + var isWhisper = evt.Channel == ChatChannel.Whisper; + if (!(_ghostHearings.HasComp(evt.Listener) && !isWhisper) && !_interactionSystem.InRangeUnobstructed(evt.Sender, + evt.Listener, + isWhisper ? SharedChatSystem.WhisperMuffledRange : SharedChatSystem.VoiceRange, + _sightMask)) + { + evt.Message = new ComplexChatMessage(evt.Message, []); + } + } +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/MinimumFluencySystem.cs b/Content.Shared/_DEN/Language/EntitySystems/MinimumFluencySystem.cs new file mode 100644 index 00000000000..ff4adeb70b6 --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/MinimumFluencySystem.cs @@ -0,0 +1,35 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public sealed partial class MinimumFluencySystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnLanguageModifyMessage); + } + + private void OnLanguageModifyMessage(Entity ent, ref LanguageModifyMessageEvent args) + { + var minFluency = _proto.Index(ent.Comp.MinimumFluency); + if (args.Understanding >= minFluency) + return; + + if (ent.Comp.Replacements is { } replacements) + { + var (kind, list) = _random.Pick(replacements); + var chosen = _random.Pick(list); + args.Message = new ComplexChatMessage(args.Message, [(kind, Loc.GetString(chosen))]); + } + else + { + args.Message = new ComplexChatMessage(args.Message, []); + } + } +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/ReplaceSpeakerNameSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/ReplaceSpeakerNameSystem.cs new file mode 100644 index 00000000000..afeb65bb309 --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/ReplaceSpeakerNameSystem.cs @@ -0,0 +1,16 @@ +using Content.Shared._DEN.Language.Components; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public sealed partial class ReplaceSpeakerNameSystem : EntitySystem +{ + public override void Initialize() + { + SubscribeLocalEvent(OnLanguageModifyMessage); + } + + private void OnLanguageModifyMessage(Entity ent, ref LanguageModifyMessageEvent args) + { + args.Name = ent.Comp.ReplaceName ?? ""; + } +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs new file mode 100644 index 00000000000..7961b69f81c --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs @@ -0,0 +1,436 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared._DEN.Language.Components; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public abstract partial class SharedLanguageSystem +{ + private static readonly ProtoId DefaultLanguageFluency = "Fluent"; + + /// + /// Sets the currently spoken language by the entity to the passed language if it speaks it. + /// + /// The entity to set the language on. + /// The language to set the entity to. + /// Whether the language was set. + [PublicAPI] + public bool TrySetLanguage(EntityUid target, ProtoId languageProto) + { + if (!SpeaksLanguage(target, languageProto, out var languageEntity)) + return false; + + var communicator = EnsureComp(target); + + if (communicator.CurrentLanguage == languageEntity.Value) + return true; + + communicator.CurrentLanguage = languageEntity; + communicator.LastSpokenLanguage = languageProto; + Dirty((target, communicator)); + return true; + } + + /// + /// Tries to set the spoken language to the specified languageEntity. + /// + /// Entity to set the language on. + /// The language entity to try to set as the spoken language. + /// + [PublicAPI] + public bool TrySetLanguage(EntityUid target, Entity languageEntity) + { + var communicator = EnsureComp(target); + + if (communicator.Languages is not { } languages) + return false; + + if (!languages.Contains(languageEntity)) + return false; + + if (!languageEntity.Comp.Speaks) + return false; + + if (communicator.CurrentLanguage == languageEntity) + return true; + + communicator.CurrentLanguage = languageEntity; + communicator.LastSpokenLanguage = languageEntity.Comp.Language; + Dirty((target, communicator)); + return true; + } + + #region Add Methods + /// + /// Adds a language to the target entity. The entity will be able to speak and fully understand the language. + /// This may add multiple languages if the language has related languages. + /// + /// The entity to add the language to. + /// The ID of the language to add. + /// The list of added languages. + /// Whether the operation succeeded. Note that languages may have still been added if a related language failed. + [PublicAPI] + public bool TryAddLanguage(EntityUid target, + ProtoId language, + out List> languageEntities) + { + return TryAddLanguage(target, language,true, DefaultLanguageFluency, out languageEntities); + } + + /// + /// Adds a language to the target entity. + /// This may add multiple languages if the language has related languages. + /// + /// The entity to add the language to. + /// The ID of the language to add. + /// Whether the target should be able to speak the language. + /// The amount of fluency the target should have with the language. + /// The list of added languages. + /// Whether the operation succeeded. Note that languages may have still been added if a related language failed. + [PublicAPI] + public bool TryAddLanguage(EntityUid target, + ProtoId languageProto, + bool speaks, + ProtoId fluencyProto, + out List> languageEntities) + { + languageEntities = []; + + return InsertLanguageAndChildren(target, languageProto, fluencyProto, speaks, out languageEntities); + } + #endregion + + #region Remove Methods + /// + /// Removes the most fluent instance of a language from an entity. This will leave less fluent instances + /// such as related languages. + /// + /// The entity to remove the language from. + /// The language to remove. + /// If a language was successfully removed. + [PublicAPI] + public bool TryRemoveLanguage(EntityUid target, ProtoId languageProto) + { + if (!TryComp(target, out var communicator)) + return false; + + if (!TryGetLanguageEntity(target, languageProto, out var languageEntity) || Deleted(languageEntity.Value)) + return false; + + if (communicator.CurrentLanguage is not null && communicator.CurrentLanguage.Value.Equals(languageEntity.Value)) + { + communicator.CurrentLanguage = null; + Dirty((target, communicator)); + } + + PredictedQueueDel(languageEntity); + return true; + } + + /// + /// Removes ALL instances of a language from an entity, regardless of source. This may cause odd behavior with + /// translators or other sources of language. + /// + /// The entity to remove the language from. + /// The language to remove. + /// If all the languages were successfully removed. + [PublicAPI] + public bool TryRemoveLanguages(EntityUid target, ProtoId languageProto) + { + if (!TryComp(target, out var communicator)) + return false; + + if (!TryGetLanguageEntities(target, languageProto, out var languageEntities)) + return false; + + var errored = false; + foreach (var languageEntity in languageEntities) + { + if (Deleted(languageEntity)) + { + errored = true; + continue; + } + + if (communicator.CurrentLanguage is not null && communicator.CurrentLanguage.Value.Equals(languageEntity)) + { + communicator.CurrentLanguage = null; + Dirty((target, communicator)); + } + + PredictedQueueDel(languageEntity); + } + + return !errored; + } + #endregion + + #region Get Methods + /// + /// Retrieves the currently spoken language of the entity. If the entity isn't currently set to one, but it + /// does speak one, then it will be set to the first language it speaks. + /// + /// The entity to retrieve the current language of. + /// Forces the creation of a default language regardless of fallback being on. This + /// is for use by systems/entities that need to send radio messages. + /// The language entity for the currently spoken language, or null if there are none. + [PublicAPI] + public Entity? GetCurrentLanguageEntity(EntityUid target, bool forceDefault = false) + { + LanguageCommunicatorComponent? communicator; + if (!TryComp(target, out communicator)) + { + if (forceDefault || _fallbackDefaultLanguage) + { + InsertLanguageAndChildren(target, _defaultLanguage, DefaultLanguageFluency, true, out _); + communicator = EnsureComp(target); // Should already exist here. + } + else + { + return null; + } + } + + if (communicator.CurrentLanguage is null || Deleted(communicator.CurrentLanguage)) + { + if (!TryGetLanguageEntities(target, out var languageEntities)) + return null; + + var spokenLanguages = languageEntities.FindAll(lang => lang.Comp.Speaks); + if (communicator.LastSpokenLanguage is { } lastSpoken) + { + communicator.CurrentLanguage = spokenLanguages.FirstOrNull(lang => lang.Comp.Language == lastSpoken); + } + + communicator.CurrentLanguage ??= spokenLanguages.FirstOrNull(); + if (communicator.CurrentLanguage is not null) + Dirty(target, communicator); + } + + if (communicator.CurrentLanguage is not null) + { + if (TryComp(communicator.CurrentLanguage, out var languageComp)) + { + return (communicator.CurrentLanguage.Value, languageComp); + } + // This can happen when a client reconnects mid-round. + // The problem is only client side, so it breaks the UI for a second, everything resolves itself eventually. + Log.Warning("Currently spoken 'language' is not a language for: " + Name(target)); + } + return null; + } + + /// + /// Retrieves the currently spoken language of the entity. If the entity isn't currently set to one, but it + /// does speak one, then it will be set to the first language it speaks. + /// If the entity does not have a LanguageCommunicatorComponent then falls back on the values of + /// languages.use_default_language and languages.default_language + /// + /// The entity to retrieve the current language of. + /// The language entity for the currently spoken language, or null if there are none. + [PublicAPI] + public ProtoId? GetCurrentLanguage(EntityUid target) + { + var languageEnt = GetCurrentLanguageEntity(target); + + return languageEnt?.Comp?.Language; + } + + /// + /// Returns the first (most fluent) language entity for the given language on the target entity. + /// + /// The target entity. + /// The language to find a language entity for. + /// The found language entity. + /// Whether a language entity was found. + [PublicAPI] + public bool TryGetLanguageEntity(EntityUid target, + ProtoId languageProto, + [NotNullWhen(true)] out Entity? languageEntity) + { + languageEntity = null; + + if (!TryGetLanguageEntities(target, languageProto, out var languageEntities)) + return false; + + languageEntity = languageEntities.First(); + return true; + } + + /// + /// Retrieves all the language entities from a target. + /// + /// The target entity + /// All the language entities on the target. + /// Whether the entities were successfully retrieved. + [PublicAPI] + public bool TryGetLanguageEntities(EntityUid target, + out List> languageEntities) + { + languageEntities = []; + + if (!TryComp(target, out var communicator)) + return false; + + if (communicator.Languages is not { } languages) + return false; + + languageEntities.AddRange( + from languageEnt in languages.ContainedEntities + where _languageQuery.HasComp(languageEnt) + select _languageQuery.Get(languageEnt)); + + return languageEntities.Count > 0; + } + + /// + /// Retrieves a list of all the language entities that represent a particular language for an entity. + /// + /// The target entity to get language entities from. + /// The language prototype to compare against. + /// The language entities matching the language, sorted by fluency. + /// Whether any language entities were retrieved. + [PublicAPI] + public bool TryGetLanguageEntities(EntityUid target, + ProtoId language, + out List> languageEntities) + { + languageEntities = []; + + if (!TryGetLanguageEntities(target, out languageEntities)) + return false; + + languageEntities = languageEntities.Where(e => e.Comp.Language == language).ToList(); + + languageEntities.Sort((lhs, rhs) => rhs.Comp.Fluency.CompareTo(lhs.Comp.Fluency)); + + return languageEntities.Count > 0; + } + + /// + /// Retrieves a list of all the languages an entity has matching the passed prototype + /// as well as their fluency values and speaking state. + /// + /// The target entity to get the languages from. + /// The language to retrieve. + /// The languages found. + /// Whether any languages were retrieved. + [PublicAPI] + public bool TryGetLanguages(EntityUid target, + ProtoId languageProto, + out List<(ProtoId, ProtoId, bool)> languages) + { + languages = []; + + if (!TryGetLanguageEntities(target, languageProto, out var languageEntities)) + return false; + + languages.AddRange(languageEntities.Select(ent => (ent.Comp.Language, ent.Comp.Fluency, ent.Comp.Speaks)) + .Select(item => ((ProtoId, ProtoId, bool)) item)); + + return true; + } + + /// + /// Retrieves a list of all the languages an entity has, as well as their fluency values and speaking state. + /// + /// The target entity to get the languages from. + /// The languages found. + /// Whether any languages were retrieved. + [PublicAPI] + public bool TryGetLanguages(EntityUid target, + out List<(ProtoId, ProtoId, bool)> languages) + { + languages = []; + + if (!TryGetLanguageEntities(target, out var languageEntities)) + return false; + + languages.AddRange( + languageEntities.Select(ent => (ent.Comp.Language, ent.Comp.Fluency, ent.Comp.Speaks)) + .Select(item => ((ProtoId, ProtoId, bool)) item)); + + return true; + } + + /// + /// Checks whether the provided entity can speak the passed language. + /// + /// The entity to check against. + /// The language to check for. + /// Whether the entity speaks the language. + [PublicAPI] + public bool SpeaksLanguage(EntityUid target, ProtoId languageProto) + { + if (!TryGetLanguageEntities(target, languageProto, out var languages)) + return false; + + return languages.Exists(lang => lang.Comp.Speaks); + } + + /// + /// Checks whether the provided entity can speak the passed language. + /// + /// The entity to check against. + /// The language to check for. + /// The language entity responsible for this ability. + /// Whether the entity speaks the language. + [PublicAPI] + public bool SpeaksLanguage(EntityUid target, ProtoId languageProto, [NotNullWhen(true)] out Entity? languageEnt) + { + languageEnt = null; + + if (!TryGetLanguageEntities(target, languageProto, out var languages)) + return false; + + languageEnt = languages.FirstOrNull(lang => lang.Comp.Speaks); + + return languageEnt != null; + } + + /// + /// Checks if the provided entity understands the matching language at least as well as the provided fluency. + /// + /// The entity to check against. + /// The language to check for. + /// The minimum fluency the entity must have. + /// Whether the entity understands the language at the minimum fluency. + [PublicAPI] + public bool UnderstandsLanguage(EntityUid target, + ProtoId languageProto, + ProtoId minimumFluency) + { + if (!TryGetLanguageEntities(target, languageProto, out var languages)) + return false; + + return languages.Exists(lang => _proto.Index(lang.Comp.Fluency) >= _proto.Index(minimumFluency)); + } + + /// + /// Checks if the provided entity understands the matching language at least as well as the provided fluency. + /// These are sorted by fluency, so the returned entity will always be the most fluent. + /// + /// The entity to check against. + /// The language to check for. + /// The minimum fluency the entity must have. + /// The language entity responsible for this ability. + /// Whether the entity understands the language at the minimum fluency. + [PublicAPI] + public bool UnderstandsLanguage(EntityUid target, + ProtoId languageProto, + ProtoId minimumFluency, + [NotNullWhen(true)] out Entity? languageEnt) + { + languageEnt = null; + + if (!TryGetLanguageEntities(target, languageProto, out var languages)) + return false; + + languageEnt = languages.FirstOrNull(lang => _proto.Index(lang.Comp.Fluency) >= _proto.Index(minimumFluency)); + return languageEnt != null; + } + #endregion +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs new file mode 100644 index 00000000000..61d83b588c1 --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs @@ -0,0 +1,192 @@ +using Content.Shared._DEN.CCVars; +using Content.Shared._DEN.Language.Components; +using Robust.Shared.Configuration; +using Robust.Shared.Containers; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public abstract partial class SharedLanguageSystem : EntitySystem +{ + [Dependency] protected readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly INetManager _netMan = default!; + [Dependency] protected readonly IGameTiming _timing = default!; + + public static readonly ProtoId MaximumFluency = "Fluent"; + public static readonly ProtoId MinimumFluency = "Unfamiliar"; + + private static ProtoId _defaultLanguage = "Basic"; + private bool _fallbackDefaultLanguage; + + private EntityQuery _languageQuery; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnLanguageCommunicatorInit); + SubscribeLocalEvent(OnLanguageCommunicatorShutdown); + SubscribeLocalEvent( + OnLanguageCommunicatorEntityInserted); + SubscribeLocalEvent( + OnLanguageCommunicatorEntityRemoved); + + SubscribeLocalEvent(OnLanguageShutdown); + + SubscribeAllEvent(OnRequestSetSpokenLanguage); + + _cfg.OnValueChanged(DenCCVars.FallbackDefaultLanguage, fallback => _fallbackDefaultLanguage = fallback, true); + _cfg.OnValueChanged(DenCCVars.DefaultLanguage, lang => _defaultLanguage = lang, true); + + _languageQuery = GetEntityQuery(); + } + + private void OnRequestSetSpokenLanguage(RequestSetSpokenLanguageEvent evt, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity is not { } user) + return; + + var languageEnt = GetEntity(evt.LanguageEntity); + + if (!TryComp(languageEnt, out var langComp)) + return; + + TrySetLanguage(user, (languageEnt, langComp)); + } + + private void OnLanguageCommunicatorInit(Entity ent, ref ComponentInit evt) + { + ent.Comp.Languages = _container.EnsureContainer(ent, LanguageCommunicatorComponent.ContainerId); + + foreach (var (language, (speaks, fluency)) in ent.Comp.BaseLanguages) + { + TryAddLanguage(ent, language, speaks, fluency, out _); + } + } + + private void OnLanguageCommunicatorShutdown(Entity ent, ref ComponentShutdown evt) + { + if (ent.Comp.Languages is { } container) + _container.ShutdownContainer(container); + } + + private void OnLanguageCommunicatorEntityInserted(Entity ent, + ref EntInsertedIntoContainerMessage args) + { + if (_languageQuery.TryComp(args.Entity, out var langComp)) + { + var addEvt = new LanguageAddedToCommunicatorEvent((args.Entity, langComp)); + RaiseLocalEvent(ent.Owner, addEvt); + } + } + + private void OnLanguageCommunicatorEntityRemoved(Entity ent, + ref EntRemovedFromContainerMessage args) + { + if (_languageQuery.TryComp(args.Entity, out var langComp)) + { + OnLanguageRemoved(ent, (args.Entity, langComp)); + + var remEvt = new LanguageRemovedFromCommunicatorEvent((args.Entity, langComp)); + RaiseLocalEvent(ent.Owner, remEvt); + } + } + + private void OnLanguageShutdown(Entity ent, ref ComponentShutdown evt) + { + if (TryComp(ent.Comp.Holder, out var commComp) && + commComp.CurrentLanguage == ent) + commComp.CurrentLanguage = null; + + foreach (var child in ent.Comp.Children) + { + PredictedQueueDel(child); + } + } + + private bool InsertLanguageAndChildren(EntityUid target, + ProtoId languageProto, + ProtoId fluencyProto, + bool speaks, + out List> addedEntities) + { + addedEntities = new(); + if (!_proto.TryIndex(languageProto, out var language) || !_proto.TryIndex(fluencyProto, out var fluency)) + return false; + + var communicator = EnsureComp(target); + if (communicator.Languages is not { } languages) + return false; + + // The client can't predict spawning entities + if (_netMan.IsClient) + return true; + + var entity = SpawnLanguageEntity(languageProto, fluencyProto, speaks); + entity.Comp.Holder = target; + if (!_container.Insert(entity.AsType(), languages)) + return false; + + addedEntities.Add(entity); + if (fluency < _proto.Index(MaximumFluency)) + { + return true; + } + + var failedChild = false; + foreach (var (relatedLang, relatedFluency) in language.RelatedLanguages) + { + var childEnt = SpawnLanguageEntity(relatedLang, relatedFluency, false); + childEnt.Comp.Holder = target; + + var childComp = EnsureComp(childEnt); + childComp.ParentLanguage = entity; + Dirty((childEnt, childComp)); + + if (!_container.Insert(childEnt.AsType(), languages)) + { + failedChild = true; + continue; + } + + entity.Comp.Children.Add(childEnt); + addedEntities.Add(childEnt); + } + + Dirty(entity); + + return !failedChild; + } + + private Entity SpawnLanguageEntity(ProtoId languageProto, + ProtoId fluencyProto, + bool speaks) + { + var language = _proto.Index(languageProto); + + var languageEnt = Spawn(); + var languageComp = EnsureComp(languageEnt); + languageComp.Fluency = fluencyProto; + languageComp.Language = languageProto; + languageComp.Speaks = speaks; + if (language.LanguageComponents is not null) + EntityManager.AddComponents(languageEnt, language.LanguageComponents); + + return (languageEnt, languageComp); + } + + protected virtual void OnLanguageRemoved(Entity holder, Entity language) + { + // Used on the client to update the language UI. + // LanguageAdded doesn't exist because the inserted event occurs before the components get added on the client :( + } + + public virtual void OnLanguageUpdated(Entity lang) + { + // Used on the client to update the language UI. + } +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/SharedSyllableScramblingSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/SharedSyllableScramblingSystem.cs new file mode 100644 index 00000000000..37e931568ca --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/SharedSyllableScramblingSystem.cs @@ -0,0 +1,3 @@ +namespace Content.Shared._DEN.Language.EntitySystems; + +public abstract partial class SharedSyllableScramblingSystem : EntitySystem; diff --git a/Content.Shared/_DEN/Language/EntitySystems/SpeechTransformableSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/SpeechTransformableSystem.cs new file mode 100644 index 00000000000..5d42c9e8487 --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/SpeechTransformableSystem.cs @@ -0,0 +1,35 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public sealed partial class SpeechTransformableSystem : EntitySystem +{ + public override void Initialize() + { + SubscribeLocalEvent>(OnVerbalLanguageTransform); + } + + private void OnVerbalLanguageTransform(Entity entity, ref LanguageRelayedEvent args) + { + var evt = args.Args; + var processedMessages = new List<(ChatPart, string)>(); + foreach (var (kind, part) in evt.Message.Parts) + { + if (kind == ChatPart.Dialog) + { + var ev = new TransformSpeechEvent(evt.Sender, part); + RaiseLocalEvent(evt.Sender, ev, true); + if (string.IsNullOrEmpty(ev.Message)) + continue; + processedMessages.Add((kind, ev.Message)); + } + else + { + processedMessages.Add((kind, part)); + } + } + + evt.Message = new ComplexChatMessage(evt.Message, processedMessages); + } +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/TranslatorSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/TranslatorSystem.cs new file mode 100644 index 00000000000..7443f37a5c1 --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/TranslatorSystem.cs @@ -0,0 +1,196 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared.Examine; +using Content.Shared.Hands; +using Content.Shared.Implants; +using Content.Shared.Inventory; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Item.ItemToggle.Components; +using Content.Shared.Power; +using Robust.Shared.Containers; +using Robust.Shared.Timing; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public sealed partial class TranslatorSystem : EntitySystem +{ + // TODO: If you move a translator from pockets with the UI on it flickers duplicate languages. + // This would need some hacky timer stuff to fix. Or for movement between pockets and hands to not also include + // an invisible "drop on the floor" step. <-- Upstream wants to fix this, if they do it'll fix this bug. + + [Dependency] private readonly SharedLanguageSystem _language = default!; + [Dependency] private readonly ItemToggleSystem _itemToggle = default!; + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnTranslatorParentChanged); + SubscribeLocalEvent(OnTranslatorInserted); + SubscribeLocalEvent(OnItemToggle); + SubscribeLocalEvent(OnRefreshChargeRate); + SubscribeLocalEvent>(OnLanguageAddedImplant); + SubscribeLocalEvent>( + OnLanguageAddedInventory); + SubscribeLocalEvent>(OnLanguageAddedHeld); + SubscribeLocalEvent>( + OnLanguageRemovedImplant); + SubscribeLocalEvent>( + OnLanguageRemovedInventory); + SubscribeLocalEvent>(OnLanguageRemovedHeld); + + SubscribeLocalEvent(OnTranslatedLanguageStartup); + SubscribeLocalEvent(OnTranslatedLanguageExamined); + } + + private void OnTranslatedLanguageStartup(Entity ent, ref ComponentStartup args) + { + _language.OnLanguageUpdated(ent.AsType()); + } + + private void OnTranslatedLanguageExamined(Entity ent, ref ExaminedEvent args) + { + args.PushMarkup(Loc.GetString("language-sourced-from-translator")); + } + + private void OnLanguageRemovedHeld(Entity ent, + ref HeldRelayedEvent args) + { + // HeldRelayedEvent doesn't tell us who's holding us :( + if (_containerSystem.TryGetContainingContainer(ent.AsType(), out var container)) + OnLanguageRemoved(ent, container.Owner, args.Args); + } + + private void OnLanguageRemovedInventory(Entity ent, + ref InventoryRelayedEvent evt) + { + OnLanguageRemoved(ent, evt.Owner, evt.Args); + } + + private void OnLanguageRemovedImplant(Entity ent, ref ImplantRelayEvent args) + { + OnLanguageRemoved(ent, args.ImplantedEntity, args.Event); + } + + private void OnLanguageRemoved(Entity ent, + EntityUid holder, + LanguageRemovedFromCommunicatorEvent args) + { + if (ent.Comp.RequiredLanguage is { } requireLang) + { + if (!_language.SpeaksLanguage(holder, requireLang)) + RemoveTranslation(ent); + } + } + + private void OnLanguageAddedHeld(Entity ent, + ref HeldRelayedEvent args) + { + // HeldRelayedEvent doesn't tell us who's holding us :( + if (_containerSystem.TryGetContainingContainer(ent.AsType(), out var container)) + OnLanguageAdded(ent, container.Owner, args.Args); + } + + private void OnLanguageAddedImplant(Entity ent, + ref ImplantRelayEvent args) + { + OnLanguageAdded(ent, args.ImplantedEntity, args.Event); + } + + private void OnLanguageAddedInventory(Entity ent, + ref InventoryRelayedEvent args) + { + OnLanguageAdded(ent, args.Owner, args.Args); + } + + private void OnLanguageAdded(Entity ent, EntityUid holder, LanguageAddedToCommunicatorEvent args) + { + TryAddTranslation(ent, holder); + } + + private void TryAddTranslation(Entity ent, EntityUid target) + { + if (!HasComp(target)) + return; + + if (!_itemToggle.IsActivated(ent.AsType())) + return; + + if (ent.Comp.CurrentlyGrantingTo is not null) + return; + + if (!(ent.Comp.RequiredLanguage is { } requiredLanguage && + _language.SpeaksLanguage(target, requiredLanguage))) + return; + + ent.Comp.CurrentlyGrantingTo = target; + List> languages = []; + foreach (var (lang, (speaks, fluency)) in ent.Comp.GrantedLanguageProtos) + { + _language.TryAddLanguage(target, lang, speaks, fluency, out var newLangs); + languages.AddRange(newLangs); + } + ent.Comp.GrantedLanguages.AddRange(languages); + + foreach (var language in languages) + { + EnsureComp(language); + } + } + + private void RemoveTranslation(Entity ent) + { + foreach (var language in ent.Comp.GrantedLanguages) + { + PredictedQueueDel(language); + } + ent.Comp.GrantedLanguages.Clear(); + ent.Comp.CurrentlyGrantingTo = null; + } + + private void OnTranslatorInserted(Entity ent, ref EntGotInsertedIntoContainerMessage message) + { + TryAddTranslation(ent, message.Container.Owner); + } + + private void OnItemToggle(Entity ent, ref ItemToggledEvent args) + { + if (args.Activated) + { + if (_containerSystem.TryGetContainingContainer(ent.AsType(), out var container)) + { + TryAddTranslation(ent, container.Owner); + } + } + else + { + RemoveTranslation(ent); + } + } + + private void OnTranslatorParentChanged(Entity ent, ref EntParentChangedMessage message) + { + // Moving a translator from your pocket to your hand drops it on the floor invisibly. This stops the + // translator from pointlessly removing the languages and breaking a ton of things if that was the case. + Timer.Spawn(0, + () => + { + EntityUid? newHolder = null; + if (_containerSystem.TryGetContainingContainer(ent.AsType(), out var container)) + { + newHolder = container.Owner; + if (newHolder == ent.Comp.CurrentlyGrantingTo) + return; + } + + RemoveTranslation(ent); + + if (newHolder is not null) + TryAddTranslation(ent, newHolder.Value); + }); + } + + private void OnRefreshChargeRate(Entity ent, ref RefreshChargeRateEvent args) + { + if (_itemToggle.IsActivated(ent.AsType())) + args.NewChargeRate -= ent.Comp.Wattage ?? TranslatorComponent.DefaultWattage; + } +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/VisualNameSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/VisualNameSystem.cs new file mode 100644 index 00000000000..621fce28cb1 --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/VisualNameSystem.cs @@ -0,0 +1,23 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; +using Content.Shared.IdentityManagement; +using Robust.Shared.Utility; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public sealed partial class VisualNameSystem : EntitySystem +{ + public override void Initialize() + { + SubscribeLocalEvent>( + OnTransformSpeakerName); + } + + private void OnTransformSpeakerName(Entity ent, + ref LanguageRelayedEvent args) + { + var evt = args.Args; + var ident = Identity.Entity(evt.Sender, EntityManager); + evt.VoiceName = FormattedMessage.EscapeText(Name(ident)); + } +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/WhisperMuffleSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/WhisperMuffleSystem.cs new file mode 100644 index 00000000000..bc4e1de244c --- /dev/null +++ b/Content.Shared/_DEN/Language/EntitySystems/WhisperMuffleSystem.cs @@ -0,0 +1,63 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; +using Content.Shared.Examine; +using Content.Shared.Ghost; + +namespace Content.Shared._DEN.Language.EntitySystems; + +public sealed partial class WhisperMuffleSystem : EntitySystem +{ + [Dependency] private readonly ExamineSystemShared _examine = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedChatSystem _chat = default!; + + private EntityQuery _xforms; + private EntityQuery _ghostHearings; + + public override void Initialize() + { + _xforms = GetEntityQuery(); + _ghostHearings = GetEntityQuery(); + + SubscribeLocalEvent( + OnModifyMessage); + } + + private void OnModifyMessage(Entity ent, ref LanguageModifyMessageEvent args) + { + if (args.Channel != ChatChannel.Whisper || _ghostHearings.HasComp(args.Listener)) + return; + + var sourceCoords = _xforms.GetComponent(args.Sender).Coordinates; + var listenXform = _xforms.GetComponent(args.Listener); + if (!sourceCoords.TryDistance(EntityManager, listenXform.Coordinates, out var distance)) + return; + + if (distance <= SharedChatSystem.WhisperClearRange) + return; + + if (_examine.InRangeUnOccluded(args.Sender, args.Listener, SharedChatSystem.WhisperMuffledRange)) + { + if (ent.Comp.Muffle) + { + args.Message = _chat.ObfuscateComplexChatMessage(args.Message, ent.Comp.MuffleAmount); + } + else + { + args.Message = new ComplexChatMessage(args.Message, []); + } + } + else + { + args.Name = "Someone"; + if (ent.Comp.Muffle) + { + args.Message = _chat.ObfuscateComplexChatMessage(args.Message, ent.Comp.MuffleAmount); + } + else + { + args.Message = new ComplexChatMessage(args.Message, []); + } + } + } +} diff --git a/Content.Shared/_DEN/Language/HideFontsMessage.cs b/Content.Shared/_DEN/Language/HideFontsMessage.cs new file mode 100644 index 00000000000..786dfbcc410 --- /dev/null +++ b/Content.Shared/_DEN/Language/HideFontsMessage.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared._DEN.Language; + +[Serializable, NetSerializable] +public sealed class HideFontsMessage : EntityEventArgs +{ + public bool Hide { get; } + + public HideFontsMessage(bool hide) + { + Hide = hide; + } +} diff --git a/Content.Shared/_DEN/Language/LanguageEvents.cs b/Content.Shared/_DEN/Language/LanguageEvents.cs new file mode 100644 index 00000000000..6afc616c753 --- /dev/null +++ b/Content.Shared/_DEN/Language/LanguageEvents.cs @@ -0,0 +1,83 @@ +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; +using Content.Shared.Inventory; +using Robust.Shared.Serialization; + +namespace Content.Shared._DEN.Language; + +public interface IKnownLanguagesRelayEvent; + +public interface ISpokenLanguageRelayEvent; + +public sealed class LanguageRelayedEvent(EntityUid owner, TEvent args) : EntityEventArgs +{ + public TEvent Args = args; + public EntityUid Owner = owner; +} + +/// +/// Called on an entity when it is attempting to understand a particular language. +/// +/// The entity sending the message +/// The language being spoken +public sealed class AttemptUnderstandingEvent(EntityUid sender, LanguagePrototype language) + : HandledEntityEventArgs, IKnownLanguagesRelayEvent +{ + public EntityUid Sender = sender; + public LanguagePrototype Language = language; + public Entity? Understanding; + public bool HideLanguage = false; + public bool HideMessage = false; +} + +public sealed class LanguageModifyMessageEvent( + EntityUid sender, + EntityUid listener, + ComplexChatMessage message, + LanguagePrototype language, + LanguageFluencyPrototype understanding, + string name, + string verb, + ChatChannel chatChannel) + : EntityEventArgs, ISpokenLanguageRelayEvent +{ + public EntityUid Sender = sender; + public EntityUid Listener = listener; + public ComplexChatMessage Message = message; + public LanguagePrototype Language = language; + public LanguageFluencyPrototype Understanding = understanding; + public string Name = name; + public string Verb = verb; + public ChatChannel Channel = chatChannel; +} + +public sealed class LanguageAddedToCommunicatorEvent(Entity language) : EntityEventArgs, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.All; + + public Entity Language = language; +} + +public sealed class LanguageRemovedFromCommunicatorEvent(Entity language) : EntityEventArgs, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.All; + + public Entity Language = language; +} + +public sealed class TransformLanguageEvent(EntityUid sender, ComplexChatMessage message) : EntityEventArgs, ISpokenLanguageRelayEvent +{ + public EntityUid Sender = sender; + public ComplexChatMessage Message = message; +} + +[Serializable, NetSerializable] +public sealed class RequestSetSpokenLanguageEvent : EntityEventArgs +{ + public readonly NetEntity LanguageEntity; + + public RequestSetSpokenLanguageEvent(NetEntity languageEntity) + { + LanguageEntity = languageEntity; + } +} diff --git a/Content.Shared/_DEN/Language/Prototypes/LanguageFluencyPrototype.cs b/Content.Shared/_DEN/Language/Prototypes/LanguageFluencyPrototype.cs new file mode 100644 index 00000000000..bfca0680275 --- /dev/null +++ b/Content.Shared/_DEN/Language/Prototypes/LanguageFluencyPrototype.cs @@ -0,0 +1,42 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Language; + +[Prototype] +public sealed partial class LanguageFluencyPrototype : IPrototype +{ + [IdDataField] public string ID { get; private set; } = default!; + + [DataField(required: true)] + public LocId Name = default!; + + [DataField(required: true)] + public int Understanding; + + public Color Color => Color.InterpolateBetween(Color.Green, Color.Red, (float)(Understanding / 100.0)); + + public int CompareTo(LanguageFluencyPrototype? other) + { + return other is null ? 0 : Understanding.CompareTo(other.Understanding); + } + + public static bool operator <(LanguageFluencyPrototype lhs, LanguageFluencyPrototype rhs) + { + return lhs.Understanding < rhs.Understanding; + } + + public static bool operator >(LanguageFluencyPrototype lhs, LanguageFluencyPrototype rhs) + { + return lhs.Understanding > rhs.Understanding; + } + + public static bool operator <=(LanguageFluencyPrototype lhs, LanguageFluencyPrototype rhs) + { + return lhs.Understanding <= rhs.Understanding; + } + + public static bool operator >=(LanguageFluencyPrototype lhs, LanguageFluencyPrototype rhs) + { + return lhs.Understanding >= rhs.Understanding; + } +} diff --git a/Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs b/Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs new file mode 100644 index 00000000000..1c273a4df6f --- /dev/null +++ b/Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs @@ -0,0 +1,98 @@ +using Content.Shared.Chat; +using Content.Shared.Speech; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; + +namespace Content.Shared._DEN.Language; + +[Prototype] +public sealed partial class LanguagePrototype : IPrototype, IInheritingPrototype +{ + [IdDataField] public string ID { get; private set; } = default!; + + [DataField(required: true)] + public LocId Name = default!; + + [ViewVariables(VVAccess.ReadOnly)] + public LocId Abbreviation => Name + "-abbreviation"; + + [ViewVariables(VVAccess.ReadOnly)] + public LocId Description => Name + "-description"; + + public string LocalizedName => Loc.GetString(Name); + public string LocalizedAbbreviation => Loc.GetString(Abbreviation); + public string LocalizedDescription => Loc.GetString(Description); + + /// + /// Speech verb overrides per channel, with optional suffix verbs. + /// + [DataField] + public Dictionary? SpeechVerbs; + + /// + /// The font to use for this language. + /// + [DataField] + public string FontId = "Default"; + + /// + /// The font size to use for this language. + /// + [DataField] + public int FontSize = 12; + + /// + /// The font color to use for this language. + /// + [DataField] + public Color FontColor = Color.White; + + /// + /// Whether to display this language in chat. + /// + [DataField] + public bool DisplayInChat = false; + + /// + /// How familiar with this language someone must be to recognize the language name in chat. + /// This does nothing unless DisplayInChat is true. + /// + [DataField] + public ProtoId UnderstandingForDisplay = "Unfamiliar"; + + [DataField] + public Dictionary>? WrapperOverrides; + + /// + /// Languages that are related to this language. If a speaker is completely Fluent in this language, then + /// they will also be able to understand the related languages in the specified amount. + /// + [DataField] + public Dictionary, ProtoId> RelatedLanguages = new(); + + /// + /// Other components to add to the language entity. These are used to add language specific effects + /// such as being spoken, signed, telepathic, or other such behavior. + /// + [DataField] + [AlwaysPushInheritance] + public ComponentRegistry? LanguageComponents; + + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] + public string[]? Parents { get; private set; } + + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; private set; } +} + +[Serializable, NetSerializable, DataDefinition] +public sealed partial class LanguageSpeechVerbs +{ + [DataField] + public ProtoId? DefaultVerb; + + [DataField] + public Dictionary>? SuffixSpeechVerbs; +} diff --git a/Content.Shared/_DEN/Language/Prototypes/LanguageWrapperPrototype.cs b/Content.Shared/_DEN/Language/Prototypes/LanguageWrapperPrototype.cs new file mode 100644 index 00000000000..15282144937 --- /dev/null +++ b/Content.Shared/_DEN/Language/Prototypes/LanguageWrapperPrototype.cs @@ -0,0 +1,30 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Language; + +[Prototype] +public sealed partial class LanguageWrapperPrototype : IPrototype +{ + [IdDataField] public string ID { get; private set; } = default!; + + [DataField(required: true)] + public LocId Dialog; + + [DataField(required: true)] + public LocId Emote; + + [DataField(required: true)] + public LocId Language; + + [DataField(required: true)] + public LocId Prefix; + + [DataField(required: true)] + public LocId Message; + + [DataField(required: true)] + public LocId SingularMessage; + + [DataField(required: true)] + public LocId BoldType; +} diff --git a/Content.Shared/_DEN/Mobs/Systems/MobStateSystem.Language.cs b/Content.Shared/_DEN/Mobs/Systems/MobStateSystem.Language.cs new file mode 100644 index 00000000000..c36e49c5253 --- /dev/null +++ b/Content.Shared/_DEN/Mobs/Systems/MobStateSystem.Language.cs @@ -0,0 +1,26 @@ +using Content.Shared._DEN.Speech; +using Content.Shared.Damage.ForceSay; +using Content.Shared.Mobs.Components; + +namespace Content.Shared.Mobs.Systems; + +public partial class MobStateSystem +{ + private void InitializeLanguage() + { + SubscribeLocalEvent(OnSpeakLanguageAttempt); + } + + private void OnSpeakLanguageAttempt(Entity uid, ref SpeakLanguageAttemptEvent evt) + { + // TODO: Decide if UnconsciousLanguages should be able to be spoken while critical. + + if (HasComp(uid)) + { + RemCompDeferred(uid); + return; + } + + CheckAct(uid, uid.Comp, evt); + } +} diff --git a/Content.Shared/_DEN/Speech/ListenEvent.Language.cs b/Content.Shared/_DEN/Speech/ListenEvent.Language.cs new file mode 100644 index 00000000000..174c652268f --- /dev/null +++ b/Content.Shared/_DEN/Speech/ListenEvent.Language.cs @@ -0,0 +1,35 @@ +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; + +namespace Content.Shared._DEN.Speech; + +public sealed class ListenLanguageEvent : EntityEventArgs +{ + public readonly ComplexChatMessage Message; + public readonly Entity LanguageEnt; + public readonly EntityUid Source; + public readonly string Verb; + public readonly ChatChannel Channel; + + public ListenLanguageEvent(ComplexChatMessage msg, EntityUid source, Entity languageEnt, string verb, ChatChannel channel) + { + Message = msg; + Source = source; + LanguageEnt = languageEnt; + Verb = verb; + Channel = channel; + } +} + +public sealed class ListenLanguageAttemptEvent : CancellableEntityEventArgs +{ + public readonly EntityUid Source; + public readonly Entity LanguageEnt; + + public ListenLanguageAttemptEvent(EntityUid source, Entity languageEnt) + { + Source = source; + LanguageEnt = languageEnt; + } +} diff --git a/Content.Shared/_DEN/Speech/SpeakLanguageAttemptEvent.cs b/Content.Shared/_DEN/Speech/SpeakLanguageAttemptEvent.cs new file mode 100644 index 00000000000..f56de495024 --- /dev/null +++ b/Content.Shared/_DEN/Speech/SpeakLanguageAttemptEvent.cs @@ -0,0 +1,13 @@ +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Chat; + +namespace Content.Shared._DEN.Speech; + +public sealed class SpeakLanguageAttemptEvent(EntityUid uid, Entity languageEnt, ChatChannel? channel) + : CancellableEntityEventArgs, ISpokenLanguageRelayEvent +{ + public EntityUid Uid = uid; + public Entity LanguageEnt = languageEnt; + public ChatChannel? Channel = channel; +} diff --git a/Content.Shared/_DEN/Speech/SpeechSystem.Language.cs b/Content.Shared/_DEN/Speech/SpeechSystem.Language.cs new file mode 100644 index 00000000000..7396f1edb2e --- /dev/null +++ b/Content.Shared/_DEN/Speech/SpeechSystem.Language.cs @@ -0,0 +1,17 @@ +using Content.Shared._DEN.Speech; + +namespace Content.Shared.Speech; + +public sealed partial class SpeechSystem +{ + private void InitializeLanguage() + { + SubscribeLocalEvent(OnSpeakLanguageAttempt); + } + + private void OnSpeakLanguageAttempt(SpeakLanguageAttemptEvent evt) + { + if (!TryComp(evt.Uid, out SpeechComponent? speech) || !speech.Enabled) + evt.Cancel(); + } +} diff --git a/Content.Shared/_DEN/Telephone/TelephoneComponent.Language.cs b/Content.Shared/_DEN/Telephone/TelephoneComponent.Language.cs new file mode 100644 index 00000000000..4e48d18e47b --- /dev/null +++ b/Content.Shared/_DEN/Telephone/TelephoneComponent.Language.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Telephone; + +public sealed partial class TelephoneComponent +{ + // Controls whether visual languages can be spoken over the 'telephone' + // All the instances of this currently in the game do this, but if it ever changes, it's here. + [DataField] + public bool TransmitsVisuals = true; +} diff --git a/Content.Shared/_DEN/Trigger/Components/Triggers/TriggerOnVoiceComponent.Language.cs b/Content.Shared/_DEN/Trigger/Components/Triggers/TriggerOnVoiceComponent.Language.cs new file mode 100644 index 00000000000..5654ecca1ac --- /dev/null +++ b/Content.Shared/_DEN/Trigger/Components/Triggers/TriggerOnVoiceComponent.Language.cs @@ -0,0 +1,13 @@ +using Content.Shared._DEN.Language; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Trigger.Components.Triggers; + +public sealed partial class TriggerOnVoiceComponent +{ + [DataField, AutoNetworkedField] + public ProtoId? KeyLanguage; + + [DataField, AutoNetworkedField] + public ProtoId? DefaultKeyLanguage; +} diff --git a/Content.Shared/_DEN/Trigger/Systems/TriggerSystem.Voice.Language.cs b/Content.Shared/_DEN/Trigger/Systems/TriggerSystem.Voice.Language.cs new file mode 100644 index 00000000000..8d04b560342 --- /dev/null +++ b/Content.Shared/_DEN/Trigger/Systems/TriggerSystem.Voice.Language.cs @@ -0,0 +1,67 @@ +using System.Linq; +using Content.Shared._DEN.Language.Components; +using Content.Shared._DEN.Speech; +using Content.Shared.Chat; +using Content.Shared.Database; +using Content.Shared.Trigger.Components.Triggers; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Trigger.Systems; + +public sealed partial class TriggerSystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + + private EntityQuery _audibleQuery; + + private void InitializeLanguage() + { + _audibleQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnListenLanguage); + } + + private void OnListenLanguage(Entity ent, ref ListenLanguageEvent args) + { + var languageEnt = args.LanguageEnt; + + if (!_audibleQuery.HasComponent(languageEnt)) + return; + + var language = _proto.Index(languageEnt.Comp.Language); + var component = ent.Comp; + var message = string.Join(' ', args.Message.Parts.Where(part => part.Item1 == ChatPart.Dialog).Select(part => part.Item2)).Trim(); + + if (component.IsRecording) + { + var ev = new ListenLanguageAttemptEvent(args.Source, languageEnt); + RaiseLocalEvent(ent, ev); + + if (ev.Cancelled) + return; + + if (message.Length >= component.MinLength && message.Length <= component.MaxLength) + FinishRecording(ent, args.Source, message, language.ID); + else if (message.Length > component.MaxLength) + _popup.PopupEntity(Loc.GetString("trigger-on-voice-record-failed-too-long"), ent); + else if (message.Length < component.MinLength) + _popup.PopupEntity(Loc.GetString("trigger-on-voice-record-failed-too-short"), ent); + + return; + } + + if (!string.IsNullOrWhiteSpace(component.KeyPhrase) && + message.IndexOf(component.KeyPhrase, StringComparison.InvariantCultureIgnoreCase) is var index and >= 0 && + component.KeyLanguage is not null && + component.KeyLanguage.Value == language.ID) + { + _adminLogger.Add(LogType.Trigger, LogImpact.Medium, + $"A voice-trigger on {ToPrettyString(ent):entity} was triggered by {ToPrettyString(args.Source):speaker} speaking the key-phrase {component.KeyPhrase}."); + Trigger(ent, args.Source, ent.Comp.KeyOut); + + var messageWithoutPhrase = message.Remove(index, component.KeyPhrase.Length).Trim(); + var voice = new VoiceTriggeredEvent(args.Source, message, messageWithoutPhrase); + RaiseLocalEvent(ent, ref voice); + } + } +} diff --git a/Resources/Locale/en-US/_DEN/chat/language/chat-language.ftl b/Resources/Locale/en-US/_DEN/chat/language/chat-language.ftl new file mode 100644 index 00000000000..8c3dd0290e7 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/chat/language/chat-language.ftl @@ -0,0 +1,38 @@ +chat-language-entity-me-wrap-message = [italic]{$spacing}{ PROPER($entity) -> + *[false] The {$entityName}{$spacingClose}{$space}{$message}[/italic] + [true] {CAPITALIZE($entityName)}{$spacingClose}{$space}{$message}[/italic] + } + +chat-language-entity-speak-wrap-dialog = [font="{$fontType}" size={$fontSize}][color={$fontColor}]{$style}{$message}{$styleClose}[/color][/font] +chat-language-entity-speak-wrap-emote = {$message} +chat-language-entity-speak-wrap-language = [font size=11][color={$color}][bold]({$language}) [/bold][/color][/font] +chat-language-entity-speak-wrap-prefix = [BubbleHeader]{$language}[bold]{$spacing}[Name]{$entityName}[/Name]{$spacingClose}[/bold][/BubbleHeader] +chat-language-entity-speak-wrap-message = {$prefix}{$space}{$verb}[BubbleContent]{$message}[/BubbleContent] +chat-language-entity-speak-wrap-message-singular = {$prefix}{$space}{$verb}"[BubbleContent]{$message}[/BubbleContent]" +chat-language-entity-speak-bold = bold + +chat-language-entity-whisper-wrap-dialog = [font="{$fontType}"][color={$fontColor}][italic]{$style}{$message}{$styleClose}[/italic][/color][/font] +chat-language-entity-whisper-wrap-emote = [italic]{$message}[/italic] +chat-language-entity-whisper-wrap-language = [font size=10][color={$color}][bolditalic]({$language}) [/bolditalic][/color][/font] +chat-language-entity-whisper-wrap-prefix = [BubbleHeader]{$language}[italic]{$spacing}[Name]{$entityName}[/Name]{$spacingClose}[/italic][/BubbleHeader] +chat-language-entity-whisper-wrap-message = [font size=11]{$prefix}[italic]{$space}{$verb}[/italic][BubbleContent]{$message}[/BubbleContent][/font] +chat-language-entity-whisper-wrap-message-singular = [font size=11]{$prefix}[italic]{$space}{$verb}[/italic]"[BubbleContent]{$message}[/BubbleContent]"[/font] +chat-language-entity-whisper-bold = bolditalic + +chat-language-entity-radio-wrap-emote = [italic]{$message}[/italic] +chat-language-entity-radio-wrap-prefix = {$channel} {$language}[bold]{$entityName}[/bold] +chat-language-entity-radio-wrap-message = [color={$color}]{$prefix} {$verb}{$message}[/color] +chat-language-entity-radio-wrap-message-singular = [color={$color}]{$prefix} {$verb}"{$message}"[/color] + +chat-language-entity-telephone-wrap-prefix = [BubbleHeader]{$language}[bold]{$spacing}[Name]{$entityName}[/Name]{$spacingClose}[/bold][/BubbleHeader] +chat-language-entity-telephone-wrap-message = {$prefix}{$space}{$verb}[BubbleContent]{$message}[/BubbleContent] +chat-language-entity-telephone-wrap-message-singular = {$prefix}{$space}{$verb}"[BubbleContent]{$message}[/BubbleContent]" + +chat-language-entity-telepathy-wrap-prefix = [BubbleHeader][bold]{$entityName}:[/bold][BubbleHeader] +chat-language-entity-telepathy-wrap-message = [color={$color}]{$prefix}{$space}[BubbleContent]{$message}[/BubbleContent][/color] +chat-language-entity-telepathy-wrap-message-singular = [color={$color}]{$prefix}{$space}[BubbleContent]{$message}[/BubbleContent][/color] + +chat-language-entity-sign-wrap-dialog = [font="{$fontType}" size={$fontSize}][color={$fontColor}][italic]{$style}{$message}{$styleClose}[/italic][/color][/font] +chat-language-entity-sign-wrap-emote = [italic]{$message}[/italic] +chat-language-entity-sign-wrap-message = {$prefix}{$space}[italic]{$verb}[/italic][BubbleContent]{$message}[/BubbleContent] +chat-language-entity-sign-wrap-message-singular = {$prefix}{$space}[italic]{$verb}"[/italic][BubbleContent]{$message}[/BubbleContent][italic]"[/italic] diff --git a/Resources/Locale/en-US/_DEN/commands/language.ftl b/Resources/Locale/en-US/_DEN/commands/language.ftl new file mode 100644 index 00000000000..721ca1b7f64 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/commands/language.ftl @@ -0,0 +1,8 @@ +command-description-language-add = Add a language to an entity, optionally specifying speaking capability and fluency. +command-description-language-remove = Remove a language from an entity, optionally removing all matching versions of that language. +command-description-language-get = Retrieve the properties of the most fluent matching language on an entity. +command-description-language-gatall = Retrieve all languages from an entity. +command-description-language-speaks = Determine if an entity speaks a language. +command-description-language-understands = Determine if an entity understands a language, optionally specifying minimum fluency. +cmd-setlang-desc = Set which language is selected for currently speaking. +setlang-completion-hint = The language to speak diff --git a/Resources/Locale/en-US/_DEN/datasets/words.ftl b/Resources/Locale/en-US/_DEN/datasets/words.ftl new file mode 100644 index 00000000000..82be04345c2 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/datasets/words.ftl @@ -0,0 +1,1001 @@ +# The most common 1000 words in the english language (According to the google ngram corpus) +common-words-dataset-1 = the +common-words-dataset-2 = of +common-words-dataset-3 = and +common-words-dataset-4 = to +common-words-dataset-5 = a +common-words-dataset-6 = in +common-words-dataset-7 = for +common-words-dataset-8 = is +common-words-dataset-9 = on +common-words-dataset-10 = that +common-words-dataset-11 = by +common-words-dataset-12 = this +common-words-dataset-13 = with +common-words-dataset-14 = i +common-words-dataset-15 = you +common-words-dataset-16 = it +common-words-dataset-17 = not +common-words-dataset-18 = or +common-words-dataset-19 = be +common-words-dataset-20 = are +common-words-dataset-21 = from +common-words-dataset-22 = at +common-words-dataset-23 = as +common-words-dataset-24 = your +common-words-dataset-25 = all +common-words-dataset-26 = have +common-words-dataset-27 = new +common-words-dataset-28 = more +common-words-dataset-29 = an +common-words-dataset-30 = was +common-words-dataset-31 = we +common-words-dataset-32 = will +common-words-dataset-33 = home +common-words-dataset-34 = can +common-words-dataset-35 = us +common-words-dataset-36 = about +common-words-dataset-37 = if +common-words-dataset-38 = page +common-words-dataset-39 = my +common-words-dataset-40 = has +common-words-dataset-41 = search +common-words-dataset-42 = free +common-words-dataset-43 = but +common-words-dataset-44 = our +common-words-dataset-45 = one +common-words-dataset-46 = other +common-words-dataset-47 = do +common-words-dataset-48 = no +common-words-dataset-49 = information +common-words-dataset-50 = time +common-words-dataset-51 = they +common-words-dataset-52 = site +common-words-dataset-53 = he +common-words-dataset-54 = up +common-words-dataset-55 = may +common-words-dataset-56 = what +common-words-dataset-57 = which +common-words-dataset-58 = their +common-words-dataset-59 = news +common-words-dataset-60 = out +common-words-dataset-61 = use +common-words-dataset-62 = any +common-words-dataset-63 = there +common-words-dataset-64 = see +common-words-dataset-65 = only +common-words-dataset-66 = so +common-words-dataset-67 = his +common-words-dataset-68 = when +common-words-dataset-69 = contact +common-words-dataset-70 = here +common-words-dataset-71 = business +common-words-dataset-72 = who +common-words-dataset-73 = web +common-words-dataset-74 = also +common-words-dataset-75 = now +common-words-dataset-76 = help +common-words-dataset-77 = get +common-words-dataset-78 = pm +common-words-dataset-79 = view +common-words-dataset-80 = online +common-words-dataset-81 = c +common-words-dataset-82 = e +common-words-dataset-83 = first +common-words-dataset-84 = am +common-words-dataset-85 = been +common-words-dataset-86 = would +common-words-dataset-87 = how +common-words-dataset-88 = were +common-words-dataset-89 = me +common-words-dataset-90 = s +common-words-dataset-91 = services +common-words-dataset-92 = some +common-words-dataset-93 = these +common-words-dataset-94 = click +common-words-dataset-95 = its +common-words-dataset-96 = like +common-words-dataset-97 = service +common-words-dataset-98 = x +common-words-dataset-99 = than +common-words-dataset-100 = find +common-words-dataset-101 = price +common-words-dataset-102 = date +common-words-dataset-103 = back +common-words-dataset-104 = top +common-words-dataset-105 = people +common-words-dataset-106 = had +common-words-dataset-107 = list +common-words-dataset-108 = name +common-words-dataset-109 = just +common-words-dataset-110 = over +common-words-dataset-111 = state +common-words-dataset-112 = year +common-words-dataset-113 = day +common-words-dataset-114 = into +common-words-dataset-115 = email +common-words-dataset-116 = two +common-words-dataset-117 = health +common-words-dataset-118 = n +common-words-dataset-119 = world +common-words-dataset-120 = re +common-words-dataset-121 = next +common-words-dataset-122 = used +common-words-dataset-123 = go +common-words-dataset-124 = b +common-words-dataset-125 = work +common-words-dataset-126 = last +common-words-dataset-127 = most +common-words-dataset-128 = products +common-words-dataset-129 = music +common-words-dataset-130 = buy +common-words-dataset-131 = data +common-words-dataset-132 = make +common-words-dataset-133 = them +common-words-dataset-134 = should +common-words-dataset-135 = product +common-words-dataset-136 = system +common-words-dataset-137 = post +common-words-dataset-138 = her +common-words-dataset-139 = city +common-words-dataset-140 = t +common-words-dataset-141 = add +common-words-dataset-142 = policy +common-words-dataset-143 = number +common-words-dataset-144 = such +common-words-dataset-145 = please +common-words-dataset-146 = available +common-words-dataset-147 = copyright +common-words-dataset-148 = support +common-words-dataset-149 = message +common-words-dataset-150 = after +common-words-dataset-151 = best +common-words-dataset-152 = software +common-words-dataset-153 = then +common-words-dataset-154 = jan +common-words-dataset-155 = good +common-words-dataset-156 = video +common-words-dataset-157 = well +common-words-dataset-158 = d +common-words-dataset-159 = where +common-words-dataset-160 = info +common-words-dataset-161 = rights +common-words-dataset-162 = public +common-words-dataset-163 = books +common-words-dataset-164 = high +common-words-dataset-165 = school +common-words-dataset-166 = through +common-words-dataset-167 = m +common-words-dataset-168 = each +common-words-dataset-169 = links +common-words-dataset-170 = she +common-words-dataset-171 = review +common-words-dataset-172 = years +common-words-dataset-173 = order +common-words-dataset-174 = very +common-words-dataset-175 = privacy +common-words-dataset-176 = book +common-words-dataset-177 = items +common-words-dataset-178 = company +common-words-dataset-179 = r +common-words-dataset-180 = read +common-words-dataset-181 = group +common-words-dataset-182 = sex +common-words-dataset-183 = need +common-words-dataset-184 = many +common-words-dataset-185 = user +common-words-dataset-186 = said +common-words-dataset-187 = de +common-words-dataset-188 = does +common-words-dataset-189 = set +common-words-dataset-190 = under +common-words-dataset-191 = general +common-words-dataset-192 = research +common-words-dataset-193 = university +common-words-dataset-194 = january +common-words-dataset-195 = mail +common-words-dataset-196 = full +common-words-dataset-197 = map +common-words-dataset-198 = reviews +common-words-dataset-199 = program +common-words-dataset-200 = life +common-words-dataset-201 = know +common-words-dataset-202 = games +common-words-dataset-203 = way +common-words-dataset-204 = days +common-words-dataset-205 = management +common-words-dataset-206 = p +common-words-dataset-207 = part +common-words-dataset-208 = could +common-words-dataset-209 = great +common-words-dataset-210 = united +common-words-dataset-211 = hotel +common-words-dataset-212 = real +common-words-dataset-213 = f +common-words-dataset-214 = item +common-words-dataset-215 = international +common-words-dataset-216 = center +common-words-dataset-217 = ebay +common-words-dataset-218 = must +common-words-dataset-219 = store +common-words-dataset-220 = travel +common-words-dataset-221 = comments +common-words-dataset-222 = made +common-words-dataset-223 = development +common-words-dataset-224 = report +common-words-dataset-225 = off +common-words-dataset-226 = member +common-words-dataset-227 = details +common-words-dataset-228 = line +common-words-dataset-229 = terms +common-words-dataset-230 = before +common-words-dataset-231 = hotels +common-words-dataset-232 = did +common-words-dataset-233 = send +common-words-dataset-234 = right +common-words-dataset-235 = type +common-words-dataset-236 = because +common-words-dataset-237 = local +common-words-dataset-238 = those +common-words-dataset-239 = using +common-words-dataset-240 = results +common-words-dataset-241 = office +common-words-dataset-242 = education +common-words-dataset-243 = national +common-words-dataset-244 = car +common-words-dataset-245 = design +common-words-dataset-246 = take +common-words-dataset-247 = posted +common-words-dataset-248 = internet +common-words-dataset-249 = address +common-words-dataset-250 = community +common-words-dataset-251 = within +common-words-dataset-252 = states +common-words-dataset-253 = area +common-words-dataset-254 = want +common-words-dataset-255 = phone +common-words-dataset-256 = dvd +common-words-dataset-257 = shipping +common-words-dataset-258 = reserved +common-words-dataset-259 = subject +common-words-dataset-260 = between +common-words-dataset-261 = forum +common-words-dataset-262 = family +common-words-dataset-263 = l +common-words-dataset-264 = long +common-words-dataset-265 = based +common-words-dataset-266 = w +common-words-dataset-267 = code +common-words-dataset-268 = show +common-words-dataset-269 = o +common-words-dataset-270 = even +common-words-dataset-271 = black +common-words-dataset-272 = check +common-words-dataset-273 = special +common-words-dataset-274 = prices +common-words-dataset-275 = website +common-words-dataset-276 = index +common-words-dataset-277 = being +common-words-dataset-278 = women +common-words-dataset-279 = much +common-words-dataset-280 = sign +common-words-dataset-281 = file +common-words-dataset-282 = link +common-words-dataset-283 = open +common-words-dataset-284 = today +common-words-dataset-285 = technology +common-words-dataset-286 = south +common-words-dataset-287 = case +common-words-dataset-288 = project +common-words-dataset-289 = same +common-words-dataset-290 = pages +common-words-dataset-291 = uk +common-words-dataset-292 = version +common-words-dataset-293 = section +common-words-dataset-294 = own +common-words-dataset-295 = found +common-words-dataset-296 = sports +common-words-dataset-297 = house +common-words-dataset-298 = related +common-words-dataset-299 = security +common-words-dataset-300 = both +common-words-dataset-301 = g +common-words-dataset-302 = county +common-words-dataset-303 = american +common-words-dataset-304 = photo +common-words-dataset-305 = game +common-words-dataset-306 = members +common-words-dataset-307 = power +common-words-dataset-308 = while +common-words-dataset-309 = care +common-words-dataset-310 = network +common-words-dataset-311 = down +common-words-dataset-312 = computer +common-words-dataset-313 = systems +common-words-dataset-314 = three +common-words-dataset-315 = total +common-words-dataset-316 = place +common-words-dataset-317 = end +common-words-dataset-318 = following +common-words-dataset-319 = download +common-words-dataset-320 = h +common-words-dataset-321 = him +common-words-dataset-322 = without +common-words-dataset-323 = per +common-words-dataset-324 = access +common-words-dataset-325 = think +common-words-dataset-326 = north +common-words-dataset-327 = resources +common-words-dataset-328 = current +common-words-dataset-329 = posts +common-words-dataset-330 = big +common-words-dataset-331 = media +common-words-dataset-332 = law +common-words-dataset-333 = control +common-words-dataset-334 = water +common-words-dataset-335 = history +common-words-dataset-336 = pictures +common-words-dataset-337 = size +common-words-dataset-338 = art +common-words-dataset-339 = personal +common-words-dataset-340 = since +common-words-dataset-341 = including +common-words-dataset-342 = guide +common-words-dataset-343 = shop +common-words-dataset-344 = directory +common-words-dataset-345 = board +common-words-dataset-346 = location +common-words-dataset-347 = change +common-words-dataset-348 = white +common-words-dataset-349 = text +common-words-dataset-350 = small +common-words-dataset-351 = rating +common-words-dataset-352 = rate +common-words-dataset-353 = government +common-words-dataset-354 = children +common-words-dataset-355 = during +common-words-dataset-356 = usa +common-words-dataset-357 = return +common-words-dataset-358 = students +common-words-dataset-359 = v +common-words-dataset-360 = shopping +common-words-dataset-361 = account +common-words-dataset-362 = times +common-words-dataset-363 = sites +common-words-dataset-364 = level +common-words-dataset-365 = digital +common-words-dataset-366 = profile +common-words-dataset-367 = previous +common-words-dataset-368 = form +common-words-dataset-369 = events +common-words-dataset-370 = love +common-words-dataset-371 = old +common-words-dataset-372 = john +common-words-dataset-373 = main +common-words-dataset-374 = call +common-words-dataset-375 = hours +common-words-dataset-376 = image +common-words-dataset-377 = department +common-words-dataset-378 = title +common-words-dataset-379 = description +common-words-dataset-380 = non +common-words-dataset-381 = k +common-words-dataset-382 = y +common-words-dataset-383 = insurance +common-words-dataset-384 = another +common-words-dataset-385 = why +common-words-dataset-386 = shall +common-words-dataset-387 = property +common-words-dataset-388 = class +common-words-dataset-389 = cd +common-words-dataset-390 = still +common-words-dataset-391 = money +common-words-dataset-392 = quality +common-words-dataset-393 = every +common-words-dataset-394 = listing +common-words-dataset-395 = content +common-words-dataset-396 = country +common-words-dataset-397 = private +common-words-dataset-398 = little +common-words-dataset-399 = visit +common-words-dataset-400 = save +common-words-dataset-401 = tools +common-words-dataset-402 = low +common-words-dataset-403 = reply +common-words-dataset-404 = customer +common-words-dataset-405 = december +common-words-dataset-406 = compare +common-words-dataset-407 = movies +common-words-dataset-408 = include +common-words-dataset-409 = college +common-words-dataset-410 = value +common-words-dataset-411 = article +common-words-dataset-412 = york +common-words-dataset-413 = man +common-words-dataset-414 = card +common-words-dataset-415 = jobs +common-words-dataset-416 = provide +common-words-dataset-417 = j +common-words-dataset-418 = food +common-words-dataset-419 = source +common-words-dataset-420 = author +common-words-dataset-421 = different +common-words-dataset-422 = press +common-words-dataset-423 = u +common-words-dataset-424 = learn +common-words-dataset-425 = sale +common-words-dataset-426 = around +common-words-dataset-427 = print +common-words-dataset-428 = course +common-words-dataset-429 = job +common-words-dataset-430 = canada +common-words-dataset-431 = process +common-words-dataset-432 = teen +common-words-dataset-433 = room +common-words-dataset-434 = stock +common-words-dataset-435 = training +common-words-dataset-436 = too +common-words-dataset-437 = credit +common-words-dataset-438 = point +common-words-dataset-439 = join +common-words-dataset-440 = science +common-words-dataset-441 = men +common-words-dataset-442 = categories +common-words-dataset-443 = advanced +common-words-dataset-444 = west +common-words-dataset-445 = sales +common-words-dataset-446 = look +common-words-dataset-447 = english +common-words-dataset-448 = left +common-words-dataset-449 = team +common-words-dataset-450 = estate +common-words-dataset-451 = box +common-words-dataset-452 = conditions +common-words-dataset-453 = select +common-words-dataset-454 = windows +common-words-dataset-455 = photos +common-words-dataset-456 = gay +common-words-dataset-457 = thread +common-words-dataset-458 = week +common-words-dataset-459 = category +common-words-dataset-460 = note +common-words-dataset-461 = live +common-words-dataset-462 = large +common-words-dataset-463 = gallery +common-words-dataset-464 = table +common-words-dataset-465 = register +common-words-dataset-466 = however +common-words-dataset-467 = june +common-words-dataset-468 = october +common-words-dataset-469 = november +common-words-dataset-470 = market +common-words-dataset-471 = library +common-words-dataset-472 = really +common-words-dataset-473 = action +common-words-dataset-474 = start +common-words-dataset-475 = series +common-words-dataset-476 = model +common-words-dataset-477 = features +common-words-dataset-478 = air +common-words-dataset-479 = industry +common-words-dataset-480 = plan +common-words-dataset-481 = human +common-words-dataset-482 = provided +common-words-dataset-483 = tv +common-words-dataset-484 = yes +common-words-dataset-485 = required +common-words-dataset-486 = second +common-words-dataset-487 = hot +common-words-dataset-488 = accessories +common-words-dataset-489 = cost +common-words-dataset-490 = movie +common-words-dataset-491 = forums +common-words-dataset-492 = march +common-words-dataset-493 = la +common-words-dataset-494 = september +common-words-dataset-495 = better +common-words-dataset-496 = say +common-words-dataset-497 = questions +common-words-dataset-498 = july +common-words-dataset-499 = yahoo +common-words-dataset-500 = going +common-words-dataset-501 = medical +common-words-dataset-502 = test +common-words-dataset-503 = friend +common-words-dataset-504 = come +common-words-dataset-505 = dec +common-words-dataset-506 = server +common-words-dataset-507 = pc +common-words-dataset-508 = study +common-words-dataset-509 = application +common-words-dataset-510 = cart +common-words-dataset-511 = staff +common-words-dataset-512 = articles +common-words-dataset-513 = san +common-words-dataset-514 = feedback +common-words-dataset-515 = again +common-words-dataset-516 = play +common-words-dataset-517 = looking +common-words-dataset-518 = issues +common-words-dataset-519 = april +common-words-dataset-520 = never +common-words-dataset-521 = users +common-words-dataset-522 = complete +common-words-dataset-523 = street +common-words-dataset-524 = topic +common-words-dataset-525 = comment +common-words-dataset-526 = financial +common-words-dataset-527 = things +common-words-dataset-528 = working +common-words-dataset-529 = against +common-words-dataset-530 = standard +common-words-dataset-531 = tax +common-words-dataset-532 = person +common-words-dataset-533 = below +common-words-dataset-534 = mobile +common-words-dataset-535 = less +common-words-dataset-536 = got +common-words-dataset-537 = blog +common-words-dataset-538 = party +common-words-dataset-539 = payment +common-words-dataset-540 = equipment +common-words-dataset-541 = login +common-words-dataset-542 = student +common-words-dataset-543 = let +common-words-dataset-544 = programs +common-words-dataset-545 = offers +common-words-dataset-546 = legal +common-words-dataset-547 = above +common-words-dataset-548 = recent +common-words-dataset-549 = park +common-words-dataset-550 = stores +common-words-dataset-551 = side +common-words-dataset-552 = act +common-words-dataset-553 = problem +common-words-dataset-554 = red +common-words-dataset-555 = give +common-words-dataset-556 = memory +common-words-dataset-557 = performance +common-words-dataset-558 = social +common-words-dataset-559 = q +common-words-dataset-560 = august +common-words-dataset-561 = quote +common-words-dataset-562 = language +common-words-dataset-563 = story +common-words-dataset-564 = sell +common-words-dataset-565 = options +common-words-dataset-566 = experience +common-words-dataset-567 = rates +common-words-dataset-568 = create +common-words-dataset-569 = key +common-words-dataset-570 = body +common-words-dataset-571 = young +common-words-dataset-572 = america +common-words-dataset-573 = important +common-words-dataset-574 = field +common-words-dataset-575 = few +common-words-dataset-576 = east +common-words-dataset-577 = paper +common-words-dataset-578 = single +common-words-dataset-579 = ii +common-words-dataset-580 = age +common-words-dataset-581 = activities +common-words-dataset-582 = club +common-words-dataset-583 = example +common-words-dataset-584 = girls +common-words-dataset-585 = additional +common-words-dataset-586 = password +common-words-dataset-587 = z +common-words-dataset-588 = latest +common-words-dataset-589 = something +common-words-dataset-590 = road +common-words-dataset-591 = gift +common-words-dataset-592 = question +common-words-dataset-593 = changes +common-words-dataset-594 = night +common-words-dataset-595 = ca +common-words-dataset-596 = hard +common-words-dataset-597 = texas +common-words-dataset-598 = oct +common-words-dataset-599 = pay +common-words-dataset-600 = four +common-words-dataset-601 = poker +common-words-dataset-602 = status +common-words-dataset-603 = browse +common-words-dataset-604 = issue +common-words-dataset-605 = range +common-words-dataset-606 = building +common-words-dataset-607 = seller +common-words-dataset-608 = court +common-words-dataset-609 = february +common-words-dataset-610 = always +common-words-dataset-611 = result +common-words-dataset-612 = audio +common-words-dataset-613 = light +common-words-dataset-614 = write +common-words-dataset-615 = war +common-words-dataset-616 = nov +common-words-dataset-617 = offer +common-words-dataset-618 = blue +common-words-dataset-619 = groups +common-words-dataset-620 = al +common-words-dataset-621 = easy +common-words-dataset-622 = given +common-words-dataset-623 = files +common-words-dataset-624 = event +common-words-dataset-625 = release +common-words-dataset-626 = analysis +common-words-dataset-627 = request +common-words-dataset-628 = fax +common-words-dataset-629 = china +common-words-dataset-630 = making +common-words-dataset-631 = picture +common-words-dataset-632 = needs +common-words-dataset-633 = possible +common-words-dataset-634 = might +common-words-dataset-635 = professional +common-words-dataset-636 = yet +common-words-dataset-637 = month +common-words-dataset-638 = major +common-words-dataset-639 = star +common-words-dataset-640 = areas +common-words-dataset-641 = future +common-words-dataset-642 = space +common-words-dataset-643 = committee +common-words-dataset-644 = hand +common-words-dataset-645 = sun +common-words-dataset-646 = cards +common-words-dataset-647 = problems +common-words-dataset-648 = london +common-words-dataset-649 = washington +common-words-dataset-650 = meeting +common-words-dataset-651 = rss +common-words-dataset-652 = become +common-words-dataset-653 = interest +common-words-dataset-654 = id +common-words-dataset-655 = child +common-words-dataset-656 = keep +common-words-dataset-657 = enter +common-words-dataset-658 = california +common-words-dataset-659 = porn +common-words-dataset-660 = share +common-words-dataset-661 = similar +common-words-dataset-662 = garden +common-words-dataset-663 = schools +common-words-dataset-664 = million +common-words-dataset-665 = added +common-words-dataset-666 = reference +common-words-dataset-667 = companies +common-words-dataset-668 = listed +common-words-dataset-669 = baby +common-words-dataset-670 = learning +common-words-dataset-671 = energy +common-words-dataset-672 = run +common-words-dataset-673 = delivery +common-words-dataset-674 = net +common-words-dataset-675 = popular +common-words-dataset-676 = term +common-words-dataset-677 = film +common-words-dataset-678 = stories +common-words-dataset-679 = put +common-words-dataset-680 = computers +common-words-dataset-681 = journal +common-words-dataset-682 = reports +common-words-dataset-683 = co +common-words-dataset-684 = try +common-words-dataset-685 = welcome +common-words-dataset-686 = central +common-words-dataset-687 = images +common-words-dataset-688 = president +common-words-dataset-689 = notice +common-words-dataset-690 = god +common-words-dataset-691 = original +common-words-dataset-692 = head +common-words-dataset-693 = radio +common-words-dataset-694 = until +common-words-dataset-695 = cell +common-words-dataset-696 = color +common-words-dataset-697 = self +common-words-dataset-698 = council +common-words-dataset-699 = away +common-words-dataset-700 = includes +common-words-dataset-701 = track +common-words-dataset-702 = australia +common-words-dataset-703 = discussion +common-words-dataset-704 = archive +common-words-dataset-705 = once +common-words-dataset-706 = others +common-words-dataset-707 = entertainment +common-words-dataset-708 = agreement +common-words-dataset-709 = format +common-words-dataset-710 = least +common-words-dataset-711 = society +common-words-dataset-712 = months +common-words-dataset-713 = log +common-words-dataset-714 = safety +common-words-dataset-715 = friends +common-words-dataset-716 = sure +common-words-dataset-717 = faq +common-words-dataset-718 = trade +common-words-dataset-719 = edition +common-words-dataset-720 = cars +common-words-dataset-721 = messages +common-words-dataset-722 = marketing +common-words-dataset-723 = tell +common-words-dataset-724 = further +common-words-dataset-725 = updated +common-words-dataset-726 = association +common-words-dataset-727 = able +common-words-dataset-728 = having +common-words-dataset-729 = provides +common-words-dataset-730 = david +common-words-dataset-731 = fun +common-words-dataset-732 = already +common-words-dataset-733 = green +common-words-dataset-734 = studies +common-words-dataset-735 = close +common-words-dataset-736 = common +common-words-dataset-737 = drive +common-words-dataset-738 = specific +common-words-dataset-739 = several +common-words-dataset-740 = gold +common-words-dataset-741 = feb +common-words-dataset-742 = living +common-words-dataset-743 = sep +common-words-dataset-744 = collection +common-words-dataset-745 = called +common-words-dataset-746 = short +common-words-dataset-747 = arts +common-words-dataset-748 = lot +common-words-dataset-749 = ask +common-words-dataset-750 = display +common-words-dataset-751 = limited +common-words-dataset-752 = powered +common-words-dataset-753 = solutions +common-words-dataset-754 = means +common-words-dataset-755 = director +common-words-dataset-756 = daily +common-words-dataset-757 = beach +common-words-dataset-758 = past +common-words-dataset-759 = natural +common-words-dataset-760 = whether +common-words-dataset-761 = due +common-words-dataset-762 = et +common-words-dataset-763 = electronics +common-words-dataset-764 = five +common-words-dataset-765 = upon +common-words-dataset-766 = period +common-words-dataset-767 = planning +common-words-dataset-768 = database +common-words-dataset-769 = says +common-words-dataset-770 = official +common-words-dataset-771 = weather +common-words-dataset-772 = mar +common-words-dataset-773 = land +common-words-dataset-774 = average +common-words-dataset-775 = done +common-words-dataset-776 = technical +common-words-dataset-777 = window +common-words-dataset-778 = france +common-words-dataset-779 = pro +common-words-dataset-780 = region +common-words-dataset-781 = island +common-words-dataset-782 = record +common-words-dataset-783 = direct +common-words-dataset-784 = microsoft +common-words-dataset-785 = conference +common-words-dataset-786 = environment +common-words-dataset-787 = records +common-words-dataset-788 = st +common-words-dataset-789 = district +common-words-dataset-790 = calendar +common-words-dataset-791 = costs +common-words-dataset-792 = style +common-words-dataset-793 = url +common-words-dataset-794 = front +common-words-dataset-795 = statement +common-words-dataset-796 = update +common-words-dataset-797 = parts +common-words-dataset-798 = aug +common-words-dataset-799 = ever +common-words-dataset-800 = downloads +common-words-dataset-801 = early +common-words-dataset-802 = miles +common-words-dataset-803 = sound +common-words-dataset-804 = resource +common-words-dataset-805 = present +common-words-dataset-806 = applications +common-words-dataset-807 = either +common-words-dataset-808 = ago +common-words-dataset-809 = document +common-words-dataset-810 = word +common-words-dataset-811 = works +common-words-dataset-812 = material +common-words-dataset-813 = bill +common-words-dataset-814 = apr +common-words-dataset-815 = written +common-words-dataset-816 = talk +common-words-dataset-817 = federal +common-words-dataset-818 = hosting +common-words-dataset-819 = rules +common-words-dataset-820 = final +common-words-dataset-821 = adult +common-words-dataset-822 = tickets +common-words-dataset-823 = thing +common-words-dataset-824 = centre +common-words-dataset-825 = requirements +common-words-dataset-826 = via +common-words-dataset-827 = cheap +common-words-dataset-828 = nude +common-words-dataset-829 = kids +common-words-dataset-830 = finance +common-words-dataset-831 = true +common-words-dataset-832 = minutes +common-words-dataset-833 = else +common-words-dataset-834 = mark +common-words-dataset-835 = third +common-words-dataset-836 = rock +common-words-dataset-837 = gifts +common-words-dataset-838 = europe +common-words-dataset-839 = reading +common-words-dataset-840 = topics +common-words-dataset-841 = bad +common-words-dataset-842 = individual +common-words-dataset-843 = tips +common-words-dataset-844 = plus +common-words-dataset-845 = auto +common-words-dataset-846 = cover +common-words-dataset-847 = usually +common-words-dataset-848 = edit +common-words-dataset-849 = together +common-words-dataset-850 = videos +common-words-dataset-851 = percent +common-words-dataset-852 = fast +common-words-dataset-853 = function +common-words-dataset-854 = fact +common-words-dataset-855 = unit +common-words-dataset-856 = getting +common-words-dataset-857 = global +common-words-dataset-858 = tech +common-words-dataset-859 = meet +common-words-dataset-860 = far +common-words-dataset-861 = economic +common-words-dataset-862 = en +common-words-dataset-863 = player +common-words-dataset-864 = projects +common-words-dataset-865 = lyrics +common-words-dataset-866 = often +common-words-dataset-867 = subscribe +common-words-dataset-868 = submit +common-words-dataset-869 = germany +common-words-dataset-870 = amount +common-words-dataset-871 = watch +common-words-dataset-872 = included +common-words-dataset-873 = feel +common-words-dataset-874 = though +common-words-dataset-875 = bank +common-words-dataset-876 = risk +common-words-dataset-877 = thanks +common-words-dataset-878 = everything +common-words-dataset-879 = deals +common-words-dataset-880 = various +common-words-dataset-881 = words +common-words-dataset-882 = linux +common-words-dataset-883 = jul +common-words-dataset-884 = production +common-words-dataset-885 = commercial +common-words-dataset-886 = james +common-words-dataset-887 = weight +common-words-dataset-888 = town +common-words-dataset-889 = heart +common-words-dataset-890 = advertising +common-words-dataset-891 = received +common-words-dataset-892 = choose +common-words-dataset-893 = treatment +common-words-dataset-894 = newsletter +common-words-dataset-895 = archives +common-words-dataset-896 = points +common-words-dataset-897 = knowledge +common-words-dataset-898 = magazine +common-words-dataset-899 = error +common-words-dataset-900 = camera +common-words-dataset-901 = jun +common-words-dataset-902 = girl +common-words-dataset-903 = currently +common-words-dataset-904 = construction +common-words-dataset-905 = toys +common-words-dataset-906 = registered +common-words-dataset-907 = clear +common-words-dataset-908 = golf +common-words-dataset-909 = receive +common-words-dataset-910 = domain +common-words-dataset-911 = methods +common-words-dataset-912 = chapter +common-words-dataset-913 = makes +common-words-dataset-914 = protection +common-words-dataset-915 = policies +common-words-dataset-916 = loan +common-words-dataset-917 = wide +common-words-dataset-918 = beauty +common-words-dataset-919 = manager +common-words-dataset-920 = india +common-words-dataset-921 = position +common-words-dataset-922 = taken +common-words-dataset-923 = sort +common-words-dataset-924 = listings +common-words-dataset-925 = models +common-words-dataset-926 = michael +common-words-dataset-927 = known +common-words-dataset-928 = half +common-words-dataset-929 = cases +common-words-dataset-930 = step +common-words-dataset-931 = engineering +common-words-dataset-932 = florida +common-words-dataset-933 = simple +common-words-dataset-934 = quick +common-words-dataset-935 = none +common-words-dataset-936 = wireless +common-words-dataset-937 = license +common-words-dataset-938 = paul +common-words-dataset-939 = friday +common-words-dataset-940 = lake +common-words-dataset-941 = whole +common-words-dataset-942 = annual +common-words-dataset-943 = published +common-words-dataset-944 = later +common-words-dataset-945 = basic +common-words-dataset-946 = sony +common-words-dataset-947 = shows +common-words-dataset-948 = corporate +common-words-dataset-949 = google +common-words-dataset-950 = church +common-words-dataset-951 = method +common-words-dataset-952 = purchase +common-words-dataset-953 = customers +common-words-dataset-954 = active +common-words-dataset-955 = response +common-words-dataset-956 = practice +common-words-dataset-957 = hardware +common-words-dataset-958 = figure +common-words-dataset-959 = materials +common-words-dataset-960 = fire +common-words-dataset-961 = holiday +common-words-dataset-962 = chat +common-words-dataset-963 = enough +common-words-dataset-964 = designed +common-words-dataset-965 = along +common-words-dataset-966 = among +common-words-dataset-967 = death +common-words-dataset-968 = writing +common-words-dataset-969 = speed +common-words-dataset-970 = html +common-words-dataset-971 = countries +common-words-dataset-972 = loss +common-words-dataset-973 = face +common-words-dataset-974 = brand +common-words-dataset-975 = discount +common-words-dataset-976 = higher +common-words-dataset-977 = effects +common-words-dataset-978 = created +common-words-dataset-979 = remember +common-words-dataset-980 = standards +common-words-dataset-981 = oil +common-words-dataset-982 = bit +common-words-dataset-983 = yellow +common-words-dataset-984 = political +common-words-dataset-985 = increase +common-words-dataset-986 = advertise +common-words-dataset-987 = kingdom +common-words-dataset-988 = base +common-words-dataset-989 = near +common-words-dataset-990 = environmental +common-words-dataset-991 = thought +common-words-dataset-992 = stuff +common-words-dataset-993 = french +common-words-dataset-994 = storage +common-words-dataset-995 = oh +common-words-dataset-996 = japan +common-words-dataset-997 = doing +common-words-dataset-998 = loans +common-words-dataset-999 = shoes +common-words-dataset-1000 = entry diff --git a/Resources/Locale/en-US/_DEN/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/_DEN/escape-menu/ui/options-menu.ftl new file mode 100644 index 00000000000..47047a1b141 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/escape-menu/ui/options-menu.ftl @@ -0,0 +1,2 @@ +ui-options-language-related = Language +ui-options-language-hide-fonts = Hide fonts for known languages diff --git a/Resources/Locale/en-US/_DEN/language/language-basic.ftl b/Resources/Locale/en-US/_DEN/language/language-basic.ftl new file mode 100644 index 00000000000..01ef8f1f144 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/language/language-basic.ftl @@ -0,0 +1,3 @@ +language-basic = Basic +language-basic-abbreviation = Basic +language-basic-description = A simple language for testing purposes, woah! diff --git a/Resources/Locale/en-US/_DEN/language/language-debug-parent.ftl b/Resources/Locale/en-US/_DEN/language/language-debug-parent.ftl new file mode 100644 index 00000000000..ce2841a3050 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/language/language-debug-parent.ftl @@ -0,0 +1,11 @@ +language-debug-parent = Debug Parent +language-debug-parent-abbreviation = Debug Parent +language-debug-parent-description = Parent language for debugging child language understanding. + +language-debug-child-1 = Debug Child 1 +language-debug-child-1-abbreviation = Debug Child 1 +language-debug-child-1-description = Child language 1 for debugging parent child language understanding. + +language-debug-child-2 = Debug Child 2 +language-debug-child-2-abbreviation = Debug Child 2 +language-debug-child-2-description = Child language 2 for debugging parent child language understanding. diff --git a/Resources/Locale/en-US/_DEN/language/language-fluencies.ftl b/Resources/Locale/en-US/_DEN/language/language-fluencies.ftl new file mode 100644 index 00000000000..3667d244f58 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/language/language-fluencies.ftl @@ -0,0 +1,6 @@ +fluency-fluent = Fluent +fluency-great = Great +fluency-good = Good +fluency-poor = Poor +fluency-related = Similar to another language +fluency-unfamiliar = Unfamiliar diff --git a/Resources/Locale/en-US/_DEN/language/language-sign.ftl b/Resources/Locale/en-US/_DEN/language/language-sign.ftl new file mode 100644 index 00000000000..eca8ab403bf --- /dev/null +++ b/Resources/Locale/en-US/_DEN/language/language-sign.ftl @@ -0,0 +1,18 @@ +language-sign = Sign +language-sign-abbreviation = Sign +language-sign-description = A simple sign language for debug testing. + +language-speech-verb-sign-default = Sign Language +language-speech-verb-sign-1 = gestures +language-speech-verb-sign-2 = signs +language-speech-verb-sign-3 = waves + +language-speech-verb-sign-whisper = Sign Language (whispered) +language-speech-verb-sign-whisper-1 = subtly gestures +language-speech-verb-sign-whisper-2 = subtly signs +language-speech-verb-sign-whisper-3 = subtly waves + +language-sign-understanding-failure-1 = something +language-sign-understanding-failure-2 = a cryptic message +language-sign-understanding-failure-3 = a signal +language-sign-understanding-failure-4 = a message diff --git a/Resources/Locale/en-US/_DEN/language/language-telepathy.ftl b/Resources/Locale/en-US/_DEN/language/language-telepathy.ftl new file mode 100644 index 00000000000..5fdccda401b --- /dev/null +++ b/Resources/Locale/en-US/_DEN/language/language-telepathy.ftl @@ -0,0 +1,4 @@ +language-telepathic = Telepathic +language-telepathic-abbreviation = Telepathic +language-telepathic-description = A telepathic language, all telepaths can hear this, anywhere! +language-telepathic-dont-whisper = Can't 'whisper' to every telepath. diff --git a/Resources/Locale/en-US/_DEN/language/language-ui.ftl b/Resources/Locale/en-US/_DEN/language/language-ui.ftl new file mode 100644 index 00000000000..0edcdad2c48 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/language/language-ui.ftl @@ -0,0 +1,13 @@ +language-window-title = Languages +game-hud-open-language-menu-button-tooltip = Open the language selection menu +ui-options-function-open-language-menu = Open the language selection menu +language-ui-language-fluency = Fluency: [color={$color}]{$fluency}[/color] +language-ui-language-description = Details: +language-ui-language-currently-speaking = Currently Speaking: +language-ui-speak-language = Speak + +language-child-language-examine = Understanding this language because of its similarity to {$parent}. +language-sourced-from-translator = This language is being translated by a translator. +language-gestalt-language-description = This language is a gestalt, it transfers to all members of the gestalt regardless of distance. + +language-trigger-on-voice-examine = Language: {$language} diff --git a/Resources/Locale/en-US/_DEN/language/language-universal.ftl b/Resources/Locale/en-US/_DEN/language/language-universal.ftl new file mode 100644 index 00000000000..6ac47d84bec --- /dev/null +++ b/Resources/Locale/en-US/_DEN/language/language-universal.ftl @@ -0,0 +1,3 @@ +language-universal = Universal +language-universal-abbreviation = Universal +language-universal-description = A language that allows understanding and speaking to anything or anyone. diff --git a/Resources/Locale/en-US/_DEN/language/language-xeno.ftl b/Resources/Locale/en-US/_DEN/language/language-xeno.ftl new file mode 100644 index 00000000000..aedb8730522 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/language/language-xeno.ftl @@ -0,0 +1,10 @@ +language-xeno-hivemind = Xeno Hivemind +language-xeno-hivemind-abbreviation = Hivemind +language-xeno-hivemind-description = The shared hivemind of the xenomorphs. + +language-xeno-hivemind-dont-whisper = There is no point whispering to the hivemind... + +language-xeno-hivemind-missing-host = There is no queen alive to sustain the hivemind! + +chat-language-entity-xeno-hivemind-wrap-message = [color={$color}]{$prefix}{$space}{$verb}[BubbleContent]{$message}[/BubbleContent][/color] +chat-language-entity-xeno-hivemind-wrap-message-singular = [color={$color}]{$prefix}{$space}{$verb}"[BubbleContent]{$message}[/BubbleContent]"[/color] diff --git a/Resources/Prototypes/Body/species_base.yml b/Resources/Prototypes/Body/species_base.yml index 47810b6bfa5..76915864046 100644 --- a/Resources/Prototypes/Body/species_base.yml +++ b/Resources/Prototypes/Body/species_base.yml @@ -160,6 +160,10 @@ - FootstepSound - DoorBumpOpener - AnomalyHost + # DEN: Languages + - type: LanguageCommunicator + languages: + Basic: [ true, Fluent ] - type: entity abstract: true diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 33653ed2077..f30946fbfe1 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -121,6 +121,10 @@ molsPerSecondPerUnitMass: 0.0005 - type: Speech speechVerb: LargeMob + # DEN: Languages + - type: LanguageCommunicator + languages: + XenoHivemind: [ true, Fluent ] - type: entity name: praetorian @@ -230,6 +234,8 @@ - type: Tag tags: - CannotSuicide + - XenoQueen # Den, Xeno hivemind language. + - type: GestaltHost - type: entity name: ravager diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index d79aa3c9d21..4b35a694fe3 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -106,6 +106,7 @@ radius: 6 castShadows: false enabled: false + - type: UniversalLanguageSpeaker # DEN: Languages, Ghosts speak everything. # proto for player ghosts specifically - type: entity diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 2f2f249a667..2f8199c10d3 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -106,6 +106,10 @@ price: 100 - type: Appearance - type: WiresVisuals + # DEN: Languages + - type: LanguageCommunicator + languages: + Basic: [ true, Fluent ] - type: entity id: VendingMachineWallmount @@ -170,6 +174,10 @@ enabled: false usesApcPower: true - type: Rotatable + # DEN: Languages + - type: LanguageCommunicator + languages: + Basic: [ true, Fluent ] # Vending machines diff --git a/Resources/Prototypes/_DEN/Datasets/words.yml b/Resources/Prototypes/_DEN/Datasets/words.yml new file mode 100644 index 00000000000..56f6f47e54c --- /dev/null +++ b/Resources/Prototypes/_DEN/Datasets/words.yml @@ -0,0 +1,5 @@ +- type: localizedDataset + id: CommonWords + values: + prefix: common-words-dataset- + count: 1000 diff --git a/Resources/Prototypes/_DEN/Entities/Objects/Devices/translator_implants.yml b/Resources/Prototypes/_DEN/Entities/Objects/Devices/translator_implants.yml new file mode 100644 index 00000000000..187e9fa0a36 --- /dev/null +++ b/Resources/Prototypes/_DEN/Entities/Objects/Devices/translator_implants.yml @@ -0,0 +1,11 @@ +- type: entity + parent: BaseSubdermalImplant + id: DebugTranslatorImplant + name: debug translator implant + description: A translator implant for the 'DebugParent' language from Basic + categories: [ HideSpawnMenu ] + components: + - type: Translator + requires: Basic + grants: + DebugParent: [ true, Fluent ] diff --git a/Resources/Prototypes/_DEN/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/_DEN/Entities/Objects/Devices/translators.yml new file mode 100644 index 00000000000..1c359176433 --- /dev/null +++ b/Resources/Prototypes/_DEN/Entities/Objects/Devices/translators.yml @@ -0,0 +1,46 @@ +- type: entity + abstract: true + parent: BaseItem + id: BaseTranslatorUnpowered + name: translator + description: Translates languages + components: + - type: Sprite + sprite: _DEN/Objects/Devices/translator.rsi + state: icon + layers: + - state: icon + - state: translator + map: ["enum.PowerDeviceVisualLayers.Powered"] + shader: unshaded + visible: false + - type: Appearance + - type: ItemToggle + - type: GenericVisualizer + visuals: + enum.ToggleableVisuals.Enabled: + enum.PowerDeviceVisualLayers.Powered: + True: { visible: true } + False: { visible: false } + - type: BatteryVisuals + - type: StaticPrice + price: 50 + +- type: entity + abstract: true + parent: [ BaseTranslatorUnpowered, PowerCellSlotMediumItem ] + id: BaseTranslatorPowered + name: translator + description: Translates languages + components: + - type: ToggleCellDraw + +- type: entity + parent: BaseTranslatorPowered + id: DebugTranslator + name: debug translator + components: + - type: Translator + requires: Basic + grants: + DebugParent: [ true, Fluent ] diff --git a/Resources/Prototypes/_DEN/Entities/Objects/Misc/translator_implanters.yml b/Resources/Prototypes/_DEN/Entities/Objects/Misc/translator_implanters.yml new file mode 100644 index 00000000000..730e6b774d2 --- /dev/null +++ b/Resources/Prototypes/_DEN/Entities/Objects/Misc/translator_implanters.yml @@ -0,0 +1,13 @@ +- type: entity + abstract: true + parent: BaseImplantOnlyImplanter + id: BaseTranslatorImplanter + name: tanslator implanter + +- type: entity + parent: BaseTranslatorImplanter + id: DebugTranslatorImplanter + name: debug translator implanter + components: + - type: Implanter + implant: DebugTranslatorImplant diff --git a/Resources/Prototypes/_DEN/Language/basic.yml b/Resources/Prototypes/_DEN/Language/basic.yml new file mode 100644 index 00000000000..085371b7263 --- /dev/null +++ b/Resources/Prototypes/_DEN/Language/basic.yml @@ -0,0 +1,260 @@ +- type: language + id: Basic + name: language-basic + displayInChat: true + languageComponents: + - type: SpeechTransformable + - type: RadioTransmittable + - type: Audible + - type: WhisperMuffle + muffle: true + - type: SyllableScrambling + syllables: + - 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 diff --git a/Resources/Prototypes/_DEN/Language/debug-related.yml b/Resources/Prototypes/_DEN/Language/debug-related.yml new file mode 100644 index 00000000000..d68a1cdb593 --- /dev/null +++ b/Resources/Prototypes/_DEN/Language/debug-related.yml @@ -0,0 +1,57 @@ +- type: language + id: DebugGroupAbstract + abstract: true + languageComponents: + - type: SpeechTransformable + - type: RadioTransmittable + - type: Audible + - type: WhisperMuffle + muffle: true + - type: SyllableScrambling + syllables: + - oo + - ee + - ooo + - aa + - aaa + - ting + - tang + - wala + - walla + - bing + - bang + +- type: language + parent: DebugGroupAbstract + id: DebugParent + name: language-debug-parent + displayInChat: true + relatedLanguages: + DebugChild1: Related + DebugChild2: Related + +- type: language + parent: DebugGroupAbstract + id: DebugChild1 + name: language-debug-child-1 + displayInChat: true + +- type: language + parent: DebugGroupAbstract + id: DebugChild2 + name: language-debug-child-2 + displayInChat: true + languageComponents: + - type: SyllableScrambling + syllables: + - aa + - ee + - oo + - uu + - yy + - b + - kl + - le + - kw + - lp + - plw diff --git a/Resources/Prototypes/_DEN/Language/fluency.yml b/Resources/Prototypes/_DEN/Language/fluency.yml new file mode 100644 index 00000000000..70a71cdbbf3 --- /dev/null +++ b/Resources/Prototypes/_DEN/Language/fluency.yml @@ -0,0 +1,30 @@ +- type: languageFluency + id: Fluent + name: fluency-fluent + understanding: 100 + +- type: languageFluency + id: Great + name: fluency-great + understanding: 75 + +- type: languageFluency + id: Good + name: fluency-good + understanding: 50 + +- type: languageFluency + id: Poor + name: fluency-poor + understanding: 25 + +# Languages that are related to each other have a related fluency. +- type: languageFluency + id: Related + name: fluency-related + understanding: 20 + +- type: languageFluency + id: Unfamiliar + name: fluency-unfamiliar + understanding: 0 diff --git a/Resources/Prototypes/_DEN/Language/sign.yml b/Resources/Prototypes/_DEN/Language/sign.yml new file mode 100644 index 00000000000..123e54d0b1b --- /dev/null +++ b/Resources/Prototypes/_DEN/Language/sign.yml @@ -0,0 +1,54 @@ +- type: language + id: Sign + name: language-sign + displayInChat: true + speechVerbs: + Local: + defaultVerb: SignVerbDefault + Whisper: + defaultVerb: SignVerbWhisper + wrapperOverrides: + Local: SignWrapper + languageComponents: + - type: LineOfSightLanguage + - type: VisualName + - type: WhisperMuffle + muffle: true + - type: SyllableScrambling + syllables: + - "--" + - "---" + - type: MinimumFluency + minimum: Related + replacements: + Emote: + - language-sign-understanding-failure-1 + - language-sign-understanding-failure-2 + - language-sign-understanding-failure-3 + - language-sign-understanding-failure-4 + +- type: speechVerb + id: SignVerbDefault + name: language-speech-verb-sign-default + speechVerbStrings: + - language-speech-verb-sign-1 + - language-speech-verb-sign-2 + - language-speech-verb-sign-3 + +- type: speechVerb + id: SignVerbWhisper + name: language-speech-verb-sign-whisper + speechVerbStrings: + - language-speech-verb-sign-whisper-1 + - language-speech-verb-sign-whisper-2 + - language-speech-verb-sign-whisper-3 + +- type: languageWrapper + id: SignWrapper + dialog: chat-language-entity-sign-wrap-dialog + emote: chat-language-entity-sign-wrap-emote + language: chat-language-entity-speak-wrap-language + prefix: chat-language-entity-speak-wrap-prefix + message: chat-language-entity-sign-wrap-message + singularMessage: chat-language-entity-sign-wrap-message-singular + boldType: chat-language-entity-whisper-bold diff --git a/Resources/Prototypes/_DEN/Language/telepathy.yml b/Resources/Prototypes/_DEN/Language/telepathy.yml new file mode 100644 index 00000000000..fb8323a751d --- /dev/null +++ b/Resources/Prototypes/_DEN/Language/telepathy.yml @@ -0,0 +1,26 @@ +- type: language + id: Telepathic + name: language-telepathic + fontColor: PaleVioletRed + fontId: BoxRound + wrapperOverrides: + Local: TelepathyWrapper + languageComponents: + - type: ReplaceSpeakerName + replaceName: TELEPATHIC + - type: ChatChannelWhitelist + blacklist: [ Whisper ] + failureMessages: [ language-telepathic-dont-whisper ] + - type: Gestalt + - type: MinimumFluency + minimum: Fluent + +- type: languageWrapper + id: TelepathyWrapper + dialog: chat-language-entity-speak-wrap-dialog + emote: chat-language-entity-speak-wrap-emote + language: chat-language-entity-speak-wrap-language + prefix: chat-language-entity-telepathy-wrap-prefix + message: chat-language-entity-telepathy-wrap-message + singularMessage: chat-language-entity-telepathy-wrap-message-singular + boldType: chat-language-entity-speak-bold diff --git a/Resources/Prototypes/_DEN/Language/universal.yml b/Resources/Prototypes/_DEN/Language/universal.yml new file mode 100644 index 00000000000..1b7226aaf40 --- /dev/null +++ b/Resources/Prototypes/_DEN/Language/universal.yml @@ -0,0 +1,3 @@ +- type: language + id: Universal + name: language-universal diff --git a/Resources/Prototypes/_DEN/Language/wrappers/wrappers.yml b/Resources/Prototypes/_DEN/Language/wrappers/wrappers.yml new file mode 100644 index 00000000000..bbce6c5352d --- /dev/null +++ b/Resources/Prototypes/_DEN/Language/wrappers/wrappers.yml @@ -0,0 +1,39 @@ +- type: languageWrapper + id: SpeakWrapper + dialog: chat-language-entity-speak-wrap-dialog + emote: chat-language-entity-speak-wrap-emote + language: chat-language-entity-speak-wrap-language + prefix: chat-language-entity-speak-wrap-prefix + message: chat-language-entity-speak-wrap-message + singularMessage: chat-language-entity-speak-wrap-message-singular + boldType: chat-language-entity-speak-bold + +- type: languageWrapper + id: WhisperWrapper + dialog: chat-language-entity-whisper-wrap-dialog + emote: chat-language-entity-whisper-wrap-emote + language: chat-language-entity-whisper-wrap-language + prefix: chat-language-entity-whisper-wrap-prefix + message: chat-language-entity-whisper-wrap-message + singularMessage: chat-language-entity-whisper-wrap-message-singular + boldType: chat-language-entity-whisper-bold + +- type: languageWrapper + id: RadioWrapper + dialog: chat-language-entity-speak-wrap-dialog + emote: chat-language-entity-radio-wrap-emote + language: chat-language-entity-speak-wrap-language + prefix: chat-language-entity-radio-wrap-prefix + message: chat-language-entity-radio-wrap-message + singularMessage: chat-language-entity-radio-wrap-message-singular + boldType: chat-language-entity-speak-bold + +- type: languageWrapper + id: TelephoneWrapper + dialog: chat-language-entity-speak-wrap-dialog + emote: chat-language-entity-speak-wrap-emote + language: chat-language-entity-speak-wrap-language + prefix: chat-language-entity-telephone-wrap-prefix + message: chat-language-entity-telephone-wrap-message + singularMessage: chat-language-entity-telephone-wrap-message-singular + boldType: chat-language-entity-speak-bold diff --git a/Resources/Prototypes/_DEN/Language/xeno.yml b/Resources/Prototypes/_DEN/Language/xeno.yml new file mode 100644 index 00000000000..c197deb85df --- /dev/null +++ b/Resources/Prototypes/_DEN/Language/xeno.yml @@ -0,0 +1,31 @@ +- type: language + id: XenoHivemind + name: language-xeno-hivemind + fontColor: Purple + fontId: BoxRound + displayInChat: true + wrapperOverrides: + Local: XenoHivemindWrapper + languageComponents: + - type: Gestalt + requiresHost: true + hostWhitelist: + tags: + - XenoQueen + missingHostPopups: + - language-xeno-hivemind-missing-host + - type: MinimumFluency + minimum: Fluent + - type: ChatChannelWhitelist + blacklist: [ Whisper ] + failureMessages: [ language-xeno-hivemind-dont-whisper ] + +- type: languageWrapper + id: XenoHivemindWrapper + dialog: chat-language-entity-speak-wrap-dialog + emote: chat-language-entity-speak-wrap-emote + language: chat-language-entity-speak-wrap-language + prefix: chat-language-entity-speak-wrap-prefix + message: chat-language-entity-xeno-hivemind-wrap-message + singularMessage: chat-language-entity-xeno-hivemind-wrap-message-singular + boldType: chat-language-entity-speak-bold diff --git a/Resources/Prototypes/_DEN/tags.yml b/Resources/Prototypes/_DEN/tags.yml index 21c486c91c5..a9460942e68 100644 --- a/Resources/Prototypes/_DEN/tags.yml +++ b/Resources/Prototypes/_DEN/tags.yml @@ -3,3 +3,6 @@ - type: Tag id: TeddyRibbon # Ribbon slot for teddy bears + +- type: Tag + id: XenoQueen # Xeno Queen tag for hivemind diff --git a/Resources/Textures/_DEN/Interface/language-solid-full.svg b/Resources/Textures/_DEN/Interface/language-solid-full.svg new file mode 100644 index 00000000000..cb58d88a212 --- /dev/null +++ b/Resources/Textures/_DEN/Interface/language-solid-full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/Textures/_DEN/Interface/language-solid-full.svg.192dpi.png b/Resources/Textures/_DEN/Interface/language-solid-full.svg.192dpi.png new file mode 100644 index 0000000000000000000000000000000000000000..933d6b7e6ad4809a18f02ca13ce2cd97aa5f4ff0 GIT binary patch literal 1512 zcmZ9MdpHwn9LHazVN7#rxt_U96q+%YSuV?(A)=(YJtH!=lglKcmQA_j6w*{~DXCm4 zDwazdW4ftO$Xrw8q`5sE37z%yJm+~%e|*2+-~0Z4pWpl6o4MDWq#&m*2LMnYJ3D!a z)8VINHi#WiIo@JN9&+~D13DgTclN}Cnd8)g z=Sl?@?e^-Y%N`o`t;Muh`ng8fOht$Uc5Cw^O(pRnj;J+pBwu>=_&R5FaV^$vN*Hf9 zRdie-I1Uw-H&iN#mIfT7W#e{VH-W4se;~oZ;f~i!BpJg$yt1>U*EA3&y_zMfHm)@Y z8N))d_6F_r*oFLlgbQWJ?tA5R$&9jb)2XgE%7c)Y8V`fbWh$pbP)}o+Wj^Hx)N@%| zwvcw5=$D}KZ4YJ`M76mQXYdmHx4_wWPkImp7LLq#*|}9M^hR6>l7;I(6t594DBZNA!eC$EPvdSfo;@c&r5et zw@Naz2fGrCy$1sGrl)qQTQ-%xkk8d;`d1`sqZ`u^;~26a%Gb?H`htTPqwv^Q$z#t+U8p!|9u z;OzoJgB8%zgxZxtfd5k+d<9mANd;rcu&u*uew48cqmSf@Bl$2eXB6J{WI<}X3S8s; z-=n6*#=R2MINKiw3^XT)<$g$N$DPvna195z{(rvrCbkv3hzcYna@X>{=4j|7Mek-V zlRTT?cY;c4y7_vB_wUP$aLstWIkvq+PzN+u(Rv04?VcxgBbwO@R=niP9VzGE6LO$B z9IRdTj=Su@p7w^uB95HoJNLU8TUqJ|f*1CLazkBj{X9!PR?(--c+IgMD&5q zew1rIf+4sd%M?bK=rXawXV1tI_j|%Z{o5RQs*hP?6NkTfa!T$?tX5`KGimO{ zxGC?(&sLxd_UT$RB3W-=UrovtgbMhpv*C*qg$L)&AoXo#@sZ|gp|(Ee_C_9Qi=3Rw zicg5c%ohT|TH0_dA^=cK}m-OjhHuA6?qi#W!!x^S->{fiF{-h}T<+Ve!-0j~Wv z<1!OcCrwqo7}-%nqD+|O?>PC5rqZgk>AmyU?;WhsB+zl-O`@cP zh2JwQL}6LQXM&5(HVWv`6@DHUO|g}9hS{Gm)zru5=gux@o3PJZSMCwq^LwzaZ_JZ- zjBGz2#_zXA(drMy6aL_W6Awzh&n3yh+XuGO>^mM;g>cjSCK4+jUtn8!8$E>Kw?Oabyl0`>hu<+crrE6NL_p;nT2ytDnG@=r$mP)KbkC-^6No^@7ihnf} zop_KQ5=0Nh9f}JT2f&~)mL_P73C3){nHdg)#-S~?qs5LcvMO2n9}s;cB>ZsFZ-5yV xhr#{=jxL_zivh#`UZ96ZhsM)`qGNtz%+WXt^e+aww9qPM0Ga6SR7;?y{R^9jk^KMw literal 0 HcmV?d00001 diff --git a/Resources/Textures/_DEN/Objects/Devices/translator.rsi/icon.png b/Resources/Textures/_DEN/Objects/Devices/translator.rsi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6871c808ccdd49673996f8b8f4404ef118734c85 GIT binary patch literal 278 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij?1AIbU_4M@Q<>hT`Y711a{BAiv=MV89^d&Flsg z;w6 zQhU|Z-a6Cv%8BbI{bv4!1BA+Lo2}0W538WU#pihLZ&zz@(>PT+|s@$ z;f&CupQ;C=PPAHcUOTL%$X6+}V=}kyeUH}1?myQVa~Q{oXWeD^{6ysAU#rzwKqoMG My85}Sb4q9e0CGNE+5i9m literal 0 HcmV?d00001 diff --git a/Resources/Textures/_DEN/Objects/Devices/translator.rsi/meta.json b/Resources/Textures/_DEN/Objects/Devices/translator.rsi/meta.json new file mode 100644 index 00000000000..0202c0c39c7 --- /dev/null +++ b/Resources/Textures/_DEN/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/_DEN/Objects/Devices/translator.rsi/translator.png b/Resources/Textures/_DEN/Objects/Devices/translator.rsi/translator.png new file mode 100644 index 0000000000000000000000000000000000000000..6c54a0b86366cef370ee7e2575979bc054a826dd GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V7#Y`V@QPi+bb724=C`kT-dCARMX0%{FPtn zM7@b>Ggy`WPQ1h1>1a^Uq@bh}Rp#`xG0tlB_rgh+7A^Z3Ffrqd#I~w``;=d`)m1S? uB}Iz&N5B8Xx#z0zhx6*&T(($0U_Q1^;pBD8!#jaiF?hQAxvX Date: Fri, 6 Mar 2026 10:25:46 -0500 Subject: [PATCH 02/15] Fix test fails. --- .../_DEN/Language/EntitySystems/LanguageSystem.cs | 5 +---- .../Language/EntitySystems/SharedLanguageSystem.cs | 14 +++++++++----- .../Language/EntitySystems/WhisperMuffleSystem.cs | 1 - Resources/Locale/en-US/_DEN/commands/language.ftl | 2 +- .../Structures/Machines/vending_machines.yml | 8 +++++++- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs b/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs index 6be79d36fc7..84a1c7c3c25 100644 --- a/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs +++ b/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs @@ -2,11 +2,8 @@ using Content.Shared._DEN.Language; using Content.Shared._DEN.Language.Components; using Content.Shared._DEN.Language.EntitySystems; -using Content.Shared.Fax; -using Content.Shared.GameTicking; +using Robust.Shared.Containers; using Robust.Client.Player; -using Robust.Shared.Player; -using Robust.Shared.Timing; namespace Content.Client._DEN.Language.EntitySystems; diff --git a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs index 61d83b588c1..edac874a2c6 100644 --- a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs +++ b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs @@ -11,10 +11,10 @@ namespace Content.Shared._DEN.Language.EntitySystems; public abstract partial class SharedLanguageSystem : EntitySystem { [Dependency] protected readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] protected readonly SharedContainerSystem _container = default!; + [Dependency] protected readonly IGameTiming _timing = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly INetManager _netMan = default!; - [Dependency] protected readonly IGameTiming _timing = default!; public static readonly ProtoId MaximumFluency = "Fluent"; public static readonly ProtoId MinimumFluency = "Unfamiliar"; @@ -28,7 +28,8 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnLanguageCommunicatorInit); + SubscribeLocalEvent(OnLanguageCommunicatorCompInit); + SubscribeLocalEvent(OnLanguageCommunicatorMapInit); SubscribeLocalEvent(OnLanguageCommunicatorShutdown); SubscribeLocalEvent( OnLanguageCommunicatorEntityInserted); @@ -58,13 +59,16 @@ private void OnRequestSetSpokenLanguage(RequestSetSpokenLanguageEvent evt, Entit TrySetLanguage(user, (languageEnt, langComp)); } - private void OnLanguageCommunicatorInit(Entity ent, ref ComponentInit evt) + private void OnLanguageCommunicatorCompInit(Entity ent, ref ComponentInit evt) { ent.Comp.Languages = _container.EnsureContainer(ent, LanguageCommunicatorComponent.ContainerId); + } + private void OnLanguageCommunicatorMapInit(Entity ent, ref MapInitEvent evt) + { foreach (var (language, (speaks, fluency)) in ent.Comp.BaseLanguages) { - TryAddLanguage(ent, language, speaks, fluency, out _); + TryAddLanguage(ent, language, speaks, fluency, out var lang); } } diff --git a/Content.Shared/_DEN/Language/EntitySystems/WhisperMuffleSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/WhisperMuffleSystem.cs index bc4e1de244c..088dbe12cc7 100644 --- a/Content.Shared/_DEN/Language/EntitySystems/WhisperMuffleSystem.cs +++ b/Content.Shared/_DEN/Language/EntitySystems/WhisperMuffleSystem.cs @@ -8,7 +8,6 @@ namespace Content.Shared._DEN.Language.EntitySystems; public sealed partial class WhisperMuffleSystem : EntitySystem { [Dependency] private readonly ExamineSystemShared _examine = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedChatSystem _chat = default!; private EntityQuery _xforms; diff --git a/Resources/Locale/en-US/_DEN/commands/language.ftl b/Resources/Locale/en-US/_DEN/commands/language.ftl index 721ca1b7f64..0e0bb3ae5b4 100644 --- a/Resources/Locale/en-US/_DEN/commands/language.ftl +++ b/Resources/Locale/en-US/_DEN/commands/language.ftl @@ -1,7 +1,7 @@ command-description-language-add = Add a language to an entity, optionally specifying speaking capability and fluency. command-description-language-remove = Remove a language from an entity, optionally removing all matching versions of that language. command-description-language-get = Retrieve the properties of the most fluent matching language on an entity. -command-description-language-gatall = Retrieve all languages from an entity. +command-description-language-getall = Retrieve all languages from an entity. command-description-language-speaks = Determine if an entity speaks a language. command-description-language-understands = Determine if an entity understands a language, optionally specifying minimum fluency. cmd-setlang-desc = Set which language is selected for currently speaking. diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 2f8199c10d3..2abfba6732d 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -1,4 +1,4 @@ -# base +# base - type: entity id: VendingMachine @@ -107,6 +107,9 @@ - type: Appearance - type: WiresVisuals # DEN: Languages + - type: ContainerContainer + containers: + languages: !type:Container - type: LanguageCommunicator languages: Basic: [ true, Fluent ] @@ -175,6 +178,9 @@ usesApcPower: true - type: Rotatable # DEN: Languages + - type: ContainerContainer + containers: + languages: !type:Container - type: LanguageCommunicator languages: Basic: [ true, Fluent ] From ae6358e0a3bf0f63479d0b7468cc0ab61b364d95 Mon Sep 17 00:00:00 2001 From: Dirius Date: Fri, 6 Mar 2026 15:19:26 -0500 Subject: [PATCH 03/15] Language Font hiding options --- .../Language/EntitySystems/LanguageSystem.cs | 2 +- .../_DEN/Options/UI/Tabs/DenTab.xaml | 2 +- .../_DEN/Options/UI/Tabs/DenTab.xaml.cs | 19 +++++++++++++++++- .../_DEN/Chat/Systems/ChatSystem.Language.cs | 7 ++++++- .../Language/EntitySystems/LanguageSystem.cs | 20 +++++++++++++------ Content.Shared/_DEN/CCVars/DenCCVars.cs | 11 ++++++++-- .../LanguageFontSuppressionComponent.cs | 6 +++++- .../_DEN/Language/HideFontsMessage.cs | 5 +++-- .../_DEN/escape-menu/ui/options-menu.ftl | 5 ++++- 9 files changed, 61 insertions(+), 16 deletions(-) diff --git a/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs b/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs index 84a1c7c3c25..1960e1f69cb 100644 --- a/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs +++ b/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs @@ -30,7 +30,7 @@ private void OnLocalPlayerAttached(EntityUid newEntity) RaiseNetworkEvent(new HideFontsMessage(_cfg.GetCVar(DenCCVars.HideLanguageFonts))); } - private void SetHideLanguageFonts(bool hide) + private void SetHideLanguageFonts(HideLanguageFontSetting hide) { RaiseNetworkEvent(new HideFontsMessage(hide)); } diff --git a/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml index 559134d3828..e575342b1b0 100644 --- a/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml +++ b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml @@ -7,7 +7,7 @@ diff --git a/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs index b9e3a68da31..a04bca5333e 100644 --- a/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs +++ b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs @@ -1,3 +1,4 @@ +using Content.Client.Options.UI; using Content.Shared._DEN.CCVars; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface; @@ -12,6 +13,22 @@ public DenTab() { RobustXamlLoader.Load(this); - Control.AddOptionCheckBox(DenCCVars.HideLanguageFonts, HideLanguageFonts); + Control.AddOptionDropDown( + DenCCVars.HideLanguageFonts, + HideLanguageFonts, + [ + new OptionDropDownCVar.ValueOption( + HideLanguageFontSetting.None, + Loc.GetString("ui-options-language-hide-fonts-none") + ), + new OptionDropDownCVar.ValueOption( + HideLanguageFontSetting.All, + Loc.GetString("ui-options-language-hide-fonts-all") + ), + new OptionDropDownCVar.ValueOption( + HideLanguageFontSetting.Understood, + Loc.GetString("ui-options-language-hide-fonts-understood") + ), + ]); } } diff --git a/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs b/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs index 9fe42798648..28d38fe405e 100644 --- a/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs +++ b/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs @@ -229,7 +229,12 @@ public void SendComplexMessageToEntity(EntityUid source, if (message.Parts.Count == 0) return; - var useLanguageFont = !HasComp(listener); + var hasMaxUnderstanding = understanding >= _prototypeManager.Index(SharedLanguageSystem.MaximumFluency); + var useLanguageFont = true; + if (TryComp(listener, out var suppression)) + { + useLanguageFont = suppression.AllFonts || hasMaxUnderstanding; + } var hideLanguage = !(language.DisplayInChat && _prototypeManager.Index(language.UnderstandingForDisplay) <= understanding) || understandEv.HideLanguage; diff --git a/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs b/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs index 7b5526eb567..92ec6d04dd0 100644 --- a/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs +++ b/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs @@ -1,3 +1,4 @@ +using Content.Shared._DEN.CCVars; using Content.Shared._DEN.Language; using Content.Shared._DEN.Language.Components; using Content.Shared._DEN.Language.EntitySystems; @@ -27,13 +28,20 @@ private void OnHideFontsRequest(HideFontsMessage msg, EntitySessionEventArgs arg if (senderSession.AttachedEntity is not { } senderEnt) return; - if (msg.Hide) + switch (msg.Hide) { - EnsureComp(senderEnt); - } - else - { - RemComp(senderEnt); + case HideLanguageFontSetting.All: + EnsureComp(senderEnt, out var comp); + comp.AllFonts = true; + break; + case HideLanguageFontSetting.Understood: + EnsureComp(senderEnt, out var comp2); + comp2.AllFonts = false; + break; + default: + case HideLanguageFontSetting.None: + RemComp(senderEnt); + break; } } diff --git a/Content.Shared/_DEN/CCVars/DenCCVars.cs b/Content.Shared/_DEN/CCVars/DenCCVars.cs index dafe63459aa..b44066d253b 100644 --- a/Content.Shared/_DEN/CCVars/DenCCVars.cs +++ b/Content.Shared/_DEN/CCVars/DenCCVars.cs @@ -32,6 +32,13 @@ public sealed class DenCCVars public static readonly CVarDef DefaultLanguage = CVarDef.Create("languages.default_language", "Basic", CVar.ARCHIVE); - public static readonly CVarDef HideLanguageFonts = - CVarDef.Create("languages.hide_fonts", false, CVar.CLIENTONLY | CVar.ARCHIVE); + public static readonly CVarDef HideLanguageFonts = + CVarDef.Create("languages.hide_fonts", HideLanguageFontSetting.None, CVar.CLIENTONLY | CVar.ARCHIVE); +} + +public enum HideLanguageFontSetting +{ + None, + Understood, + All, } diff --git a/Content.Shared/_DEN/Language/Components/LanguageFontSuppressionComponent.cs b/Content.Shared/_DEN/Language/Components/LanguageFontSuppressionComponent.cs index 1ee3e881797..3c4a23ee42b 100644 --- a/Content.Shared/_DEN/Language/Components/LanguageFontSuppressionComponent.cs +++ b/Content.Shared/_DEN/Language/Components/LanguageFontSuppressionComponent.cs @@ -4,4 +4,8 @@ namespace Content.Shared._DEN.Language.Components; /// Marks a user as desiring not to see the language font on languages they understand. /// [RegisterComponent] -public sealed partial class LanguageFontSuppressionComponent : Component; +public sealed partial class LanguageFontSuppressionComponent : Component +{ + [DataField] + public bool AllFonts; +} diff --git a/Content.Shared/_DEN/Language/HideFontsMessage.cs b/Content.Shared/_DEN/Language/HideFontsMessage.cs index 786dfbcc410..760889cec7f 100644 --- a/Content.Shared/_DEN/Language/HideFontsMessage.cs +++ b/Content.Shared/_DEN/Language/HideFontsMessage.cs @@ -1,3 +1,4 @@ +using Content.Shared._DEN.CCVars; using Robust.Shared.Serialization; namespace Content.Shared._DEN.Language; @@ -5,9 +6,9 @@ namespace Content.Shared._DEN.Language; [Serializable, NetSerializable] public sealed class HideFontsMessage : EntityEventArgs { - public bool Hide { get; } + public HideLanguageFontSetting Hide { get; } - public HideFontsMessage(bool hide) + public HideFontsMessage(HideLanguageFontSetting hide) { Hide = hide; } diff --git a/Resources/Locale/en-US/_DEN/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/_DEN/escape-menu/ui/options-menu.ftl index 47047a1b141..665d237c536 100644 --- a/Resources/Locale/en-US/_DEN/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/_DEN/escape-menu/ui/options-menu.ftl @@ -1,2 +1,5 @@ ui-options-language-related = Language -ui-options-language-hide-fonts = Hide fonts for known languages +ui-options-language-hide-fonts = Hide language fonts +ui-options-language-hide-fonts-none = None +ui-options-language-hide-fonts-understood = Understood +ui-options-language-hide-fonts-all = All From 31b220b8e7157361b08c920a8195dd1f53f9305f Mon Sep 17 00:00:00 2001 From: Dirius Date: Sat, 7 Mar 2026 23:11:54 -0500 Subject: [PATCH 04/15] Animals and Bots and Borgs oh my --- .../Effects/MakeSentientEntityEffectSystem.cs | 2 ++ ...MakeSentientEntityEffectSystem.Language.cs | 20 +++++++++++++ .../EntitySystems/SharedLanguageSystem.API.cs | 11 +++++++ .../Language/Prototypes/LanguagePrototype.cs | 29 ++++++++++++------- .../en-US/_DEN/language/language-animal.ftl | 3 ++ .../Locale/en-US/_DEN/language/language.ftl | 3 ++ Resources/Prototypes/Body/Animals/animal.yml | 7 +++++ .../Mobs/Cyborgs/base_borg_chassis.yml | 5 ++++ .../Prototypes/Entities/Mobs/NPCs/silicon.yml | 7 +++++ Resources/Prototypes/_DEN/Language/animal.yml | 4 +++ Resources/Prototypes/_DEN/Language/base.yml | 10 +++++++ Resources/Prototypes/_DEN/Language/basic.yml | 8 ++--- .../_DEN/Language/debug-related.yml | 10 ++----- Resources/Prototypes/_DEN/Language/sign.yml | 2 +- .../Prototypes/_DEN/Language/telepathy.yml | 2 +- Resources/Prototypes/_DEN/Language/xeno.yml | 2 +- 16 files changed, 99 insertions(+), 26 deletions(-) create mode 100644 Content.Server/_DEN/EntityEffects/Effects/MakeSentientEntityEffectSystem.Language.cs create mode 100644 Resources/Locale/en-US/_DEN/language/language-animal.ftl create mode 100644 Resources/Locale/en-US/_DEN/language/language.ftl create mode 100644 Resources/Prototypes/_DEN/Language/animal.yml create mode 100644 Resources/Prototypes/_DEN/Language/base.yml diff --git a/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs index c623b258576..ef4e76a05c2 100644 --- a/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs @@ -23,6 +23,8 @@ protected override void Effect(Entity entity, ref EntityEffec RemComp(entity); // TODO: Make MonkeyAccent a replacement accent and remove MonkeyAccent code-smell. RemComp(entity); + + MakeSentientLanguages(entity); // DEN: Languages } // Stops from adding a ghost role to things like people who already have a mind diff --git a/Content.Server/_DEN/EntityEffects/Effects/MakeSentientEntityEffectSystem.Language.cs b/Content.Server/_DEN/EntityEffects/Effects/MakeSentientEntityEffectSystem.Language.cs new file mode 100644 index 00000000000..55389cc3b8f --- /dev/null +++ b/Content.Server/_DEN/EntityEffects/Effects/MakeSentientEntityEffectSystem.Language.cs @@ -0,0 +1,20 @@ +using Content.Shared._DEN.Language; +using Content.Shared._DEN.Language.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +public sealed partial class MakeSentientEntityEffectSystem +{ + [Dependency] private readonly SharedLanguageSystem _languageSystem = default!; + + private static readonly ProtoId _animalLanugage = "Animal"; + + private void MakeSentientLanguages(EntityUid target) + { + _languageSystem.TryRemoveLanguage(target, _animalLanugage); + var defaultLang = _languageSystem.GetDefaultLanguage(); + if (!_languageSystem.SpeaksLanguage(target, defaultLang)) + _languageSystem.TryAddLanguage(target, defaultLang, out _); + } +} diff --git a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs index 7961b69f81c..76174c6f8cf 100644 --- a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs +++ b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs @@ -169,6 +169,17 @@ public bool TryRemoveLanguages(EntityUid target, ProtoId lang #endregion #region Get Methods + + /// + /// Fetches the current default language. + /// + /// The ProtoId of the current default language. + [PublicAPI] + public ProtoId GetDefaultLanguage() + { + return _defaultLanguage; + } + /// /// Retrieves the currently spoken language of the entity. If the entity isn't currently set to one, but it /// does speak one, then it will be set to the first language it speaks. diff --git a/Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs b/Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs index 1c273a4df6f..3d0f3826d66 100644 --- a/Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs +++ b/Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs @@ -7,9 +7,12 @@ namespace Content.Shared._DEN.Language; [Prototype] +[DataDefinition] public sealed partial class LanguagePrototype : IPrototype, IInheritingPrototype { - [IdDataField] public string ID { get; private set; } = default!; + [ViewVariables] + [IdDataField] + public string ID { get; private set; } = default!; [DataField(required: true)] public LocId Name = default!; @@ -20,10 +23,23 @@ public sealed partial class LanguagePrototype : IPrototype, IInheritingPrototype [ViewVariables(VVAccess.ReadOnly)] public LocId Description => Name + "-description"; + [ViewVariables(VVAccess.ReadOnly)] public string LocalizedName => Loc.GetString(Name); + + [ViewVariables(VVAccess.ReadOnly)] public string LocalizedAbbreviation => Loc.GetString(Abbreviation); + + [ViewVariables(VVAccess.ReadOnly)] public string LocalizedDescription => Loc.GetString(Description); + [ViewVariables] + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] + public string[]? Parents { get; private set; } + + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; private set; } + /// /// Speech verb overrides per channel, with optional suffix verbs. /// @@ -75,16 +91,9 @@ public sealed partial class LanguagePrototype : IPrototype, IInheritingPrototype /// Other components to add to the language entity. These are used to add language specific effects /// such as being spoken, signed, telepathic, or other such behavior. /// - [DataField] + [DataField("components")] [AlwaysPushInheritance] - public ComponentRegistry? LanguageComponents; - - [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] - public string[]? Parents { get; private set; } - - [NeverPushInheritance] - [AbstractDataField] - public bool Abstract { get; private set; } + public ComponentRegistry LanguageComponents = new(); } [Serializable, NetSerializable, DataDefinition] diff --git a/Resources/Locale/en-US/_DEN/language/language-animal.ftl b/Resources/Locale/en-US/_DEN/language/language-animal.ftl new file mode 100644 index 00000000000..48d55e3059d --- /dev/null +++ b/Resources/Locale/en-US/_DEN/language/language-animal.ftl @@ -0,0 +1,3 @@ +language-animal = Animal +language-animal-abbreviation = Animal +language-animal-description = Basic animal squeaks, chirps, and growls. diff --git a/Resources/Locale/en-US/_DEN/language/language.ftl b/Resources/Locale/en-US/_DEN/language/language.ftl new file mode 100644 index 00000000000..f64650969d8 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/language/language.ftl @@ -0,0 +1,3 @@ +language-base-audible = Base Audible +language-base-audible-abbreviation = Audible +language-base-audible-description = Base language for any audible languages (you should never see this) diff --git a/Resources/Prototypes/Body/Animals/animal.yml b/Resources/Prototypes/Body/Animals/animal.yml index bf8d15c9211..97bd42e1c14 100644 --- a/Resources/Prototypes/Body/Animals/animal.yml +++ b/Resources/Prototypes/Body/Animals/animal.yml @@ -53,3 +53,10 @@ - id: OrganAnimalStomach - id: OrganAnimalLiver - id: OrganAnimalKidneys + # DEN: Languages + - type: ContainerContainer + containers: + languages: !type:Container + - type: LanguageCommunicator + languages: + Animal: [ true, Fluent ] diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index e45fd88686f..586369f1ccd 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -123,6 +123,7 @@ cell_slot: !type:ContainerSlot { } borg_module: !type:Container { } part-container: !type:Container + languages: !type:Container { } # DEN: Languages - type: PowerCellSlot cellSlotId: cell_slot fitsInCharger: true @@ -252,6 +253,10 @@ requiredMobState: - Critical - Dead + # DEN: Languages + - type: LanguageCommunicator + languages: + Basic: [ true, Fluent ] - type: entity abstract: true diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index a4d5e4100f6..2e0834e7ab9 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -109,6 +109,13 @@ - type: Speech speechVerb: Robotic speechSounds: Pai #couldn't decide if this should be borg or pai sounds so I flipped a coin. + # DEN: Languages + - type: ContainerContainer + containers: + languages: !type:Container + - type: LanguageCommunicator + languages: + Basic: [ true, Fluent ] - type: entity parent: MobSiliconBase diff --git a/Resources/Prototypes/_DEN/Language/animal.yml b/Resources/Prototypes/_DEN/Language/animal.yml new file mode 100644 index 00000000000..ed8b6f70eee --- /dev/null +++ b/Resources/Prototypes/_DEN/Language/animal.yml @@ -0,0 +1,4 @@ +- type: language + parent: BaseAudible + id: Animal + name: language-animal diff --git a/Resources/Prototypes/_DEN/Language/base.yml b/Resources/Prototypes/_DEN/Language/base.yml new file mode 100644 index 00000000000..0a9a26f8dfe --- /dev/null +++ b/Resources/Prototypes/_DEN/Language/base.yml @@ -0,0 +1,10 @@ +- type: language + abstract: true + id: BaseAudible + name: language-base-audible + components: + - type: SpeechTransformable + - type: RadioTransmittable + - type: Audible + - type: WhisperMuffle + muffle: true diff --git a/Resources/Prototypes/_DEN/Language/basic.yml b/Resources/Prototypes/_DEN/Language/basic.yml index 085371b7263..5b8b48725da 100644 --- a/Resources/Prototypes/_DEN/Language/basic.yml +++ b/Resources/Prototypes/_DEN/Language/basic.yml @@ -1,13 +1,9 @@ - type: language + parent: BaseAudible id: Basic name: language-basic displayInChat: true - languageComponents: - - type: SpeechTransformable - - type: RadioTransmittable - - type: Audible - - type: WhisperMuffle - muffle: true + components: - type: SyllableScrambling syllables: - a diff --git a/Resources/Prototypes/_DEN/Language/debug-related.yml b/Resources/Prototypes/_DEN/Language/debug-related.yml index d68a1cdb593..0df1a26c08d 100644 --- a/Resources/Prototypes/_DEN/Language/debug-related.yml +++ b/Resources/Prototypes/_DEN/Language/debug-related.yml @@ -1,12 +1,8 @@ - type: language + parent: BaseAudible id: DebugGroupAbstract abstract: true - languageComponents: - - type: SpeechTransformable - - type: RadioTransmittable - - type: Audible - - type: WhisperMuffle - muffle: true + components: - type: SyllableScrambling syllables: - oo @@ -41,7 +37,7 @@ id: DebugChild2 name: language-debug-child-2 displayInChat: true - languageComponents: + components: - type: SyllableScrambling syllables: - aa diff --git a/Resources/Prototypes/_DEN/Language/sign.yml b/Resources/Prototypes/_DEN/Language/sign.yml index 123e54d0b1b..f2506bebf45 100644 --- a/Resources/Prototypes/_DEN/Language/sign.yml +++ b/Resources/Prototypes/_DEN/Language/sign.yml @@ -9,7 +9,7 @@ defaultVerb: SignVerbWhisper wrapperOverrides: Local: SignWrapper - languageComponents: + components: - type: LineOfSightLanguage - type: VisualName - type: WhisperMuffle diff --git a/Resources/Prototypes/_DEN/Language/telepathy.yml b/Resources/Prototypes/_DEN/Language/telepathy.yml index fb8323a751d..53c7a2b37c1 100644 --- a/Resources/Prototypes/_DEN/Language/telepathy.yml +++ b/Resources/Prototypes/_DEN/Language/telepathy.yml @@ -5,7 +5,7 @@ fontId: BoxRound wrapperOverrides: Local: TelepathyWrapper - languageComponents: + components: - type: ReplaceSpeakerName replaceName: TELEPATHIC - type: ChatChannelWhitelist diff --git a/Resources/Prototypes/_DEN/Language/xeno.yml b/Resources/Prototypes/_DEN/Language/xeno.yml index c197deb85df..e6bcfca26fd 100644 --- a/Resources/Prototypes/_DEN/Language/xeno.yml +++ b/Resources/Prototypes/_DEN/Language/xeno.yml @@ -6,7 +6,7 @@ displayInChat: true wrapperOverrides: Local: XenoHivemindWrapper - languageComponents: + components: - type: Gestalt requiresHost: true hostWhitelist: From d81ca9b77a3bc6bbff6291c84f4c07eced0990d7 Mon Sep 17 00:00:00 2001 From: Dirius Date: Sat, 7 Mar 2026 23:43:39 -0500 Subject: [PATCH 05/15] My turn to almost make a heisentest, woops. --- .../SurveillanceCameraMicrophoneSystem.Language.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.Language.cs b/Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.Language.cs index d59fd5ddfbf..285d50a7bad 100644 --- a/Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.Language.cs +++ b/Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.Language.cs @@ -1,4 +1,3 @@ -using Content.Shared._DEN.Language; using Content.Shared._DEN.Language.Components; using Content.Shared._DEN.Speech; using Content.Shared.Chat; @@ -8,11 +7,14 @@ namespace Content.Server.SurveillanceCamera; public sealed partial class SurveillanceCameraMicrophoneSystem { - private readonly EntityQuery _audibleQuery = default!; - private readonly EntityQuery _losQuery = default!; + private EntityQuery _audibleQuery; + private EntityQuery _losQuery; private void InitializeLanguage() { + _audibleQuery = GetEntityQuery(); + _losQuery = GetEntityQuery(); + SubscribeLocalEvent(RelayEntityLanguageMessage); SubscribeLocalEvent(CanListenLanguage); } From 2ae42f3071ac160565ccc588f3fe0b1c1fabde08 Mon Sep 17 00:00:00 2001 From: Dirius Date: Sun, 8 Mar 2026 00:03:35 -0500 Subject: [PATCH 06/15] Put LanguageFontSuppression on the mind. --- .../_DEN/Language/EntitySystems/LanguageSystem.cs | 6 ++++-- .../_DEN/Chat/Systems/ChatSystem.Language.cs | 7 +++++-- .../_DEN/Language/EntitySystems/LanguageSystem.cs | 11 ++++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs b/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs index 1960e1f69cb..10a1d6f7e6d 100644 --- a/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs +++ b/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs @@ -2,6 +2,7 @@ using Content.Shared._DEN.Language; using Content.Shared._DEN.Language.Components; using Content.Shared._DEN.Language.EntitySystems; +using Content.Shared.GameTicking; using Robust.Shared.Containers; using Robust.Client.Player; @@ -19,13 +20,14 @@ public override void Initialize() base.Initialize(); _cfg.OnValueChanged(DenCCVars.HideLanguageFonts, SetHideLanguageFonts); - _playerManager.LocalPlayerAttached += OnLocalPlayerAttached; SubscribeLocalEvent(OnLanguageComponentHandleState); SubscribeLocalEvent(OnLanguageCommunicatorHandleState); + + SubscribeLocalEvent(OnPlayerSpawnComplete); } - private void OnLocalPlayerAttached(EntityUid newEntity) + private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent evt) { RaiseNetworkEvent(new HideFontsMessage(_cfg.GetCVar(DenCCVars.HideLanguageFonts))); } diff --git a/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs b/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs index 28d38fe405e..2c51955f0a2 100644 --- a/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs +++ b/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs @@ -6,6 +6,7 @@ using Content.Shared.Chat; using Content.Shared.Database; using Content.Shared.IdentityManagement; +using Content.Shared.Mind; using Content.Shared.Radio; using Robust.Shared.Network; using Robust.Shared.Player; @@ -18,6 +19,7 @@ namespace Content.Server.Chat.Systems; public sealed partial class ChatSystem { [Dependency] private readonly LanguageSystem _language = default!; + [Dependency] private readonly SharedMindSystem _mindSystem = default!; public static readonly ProtoId SpeakWrapper = "SpeakWrapper"; public static readonly ProtoId WhisperWrapper = "WhisperWrapper"; @@ -231,9 +233,10 @@ public void SendComplexMessageToEntity(EntityUid source, var hasMaxUnderstanding = understanding >= _prototypeManager.Index(SharedLanguageSystem.MaximumFluency); var useLanguageFont = true; - if (TryComp(listener, out var suppression)) + if (_mindSystem.TryGetMind(listener, out var mindId, out _) && + TryComp(mindId, out var suppression)) { - useLanguageFont = suppression.AllFonts || hasMaxUnderstanding; + useLanguageFont = !(suppression.AllFonts || hasMaxUnderstanding); } var hideLanguage = !(language.DisplayInChat && _prototypeManager.Index(language.UnderstandingForDisplay) <= understanding) || diff --git a/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs b/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs index 92ec6d04dd0..465b2cc1e03 100644 --- a/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs +++ b/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs @@ -3,6 +3,7 @@ using Content.Shared._DEN.Language.Components; using Content.Shared._DEN.Language.EntitySystems; using Content.Shared.Chat; +using Content.Shared.Mind; using Robust.Shared.Prototypes; namespace Content.Server._DEN.Language.EntitySystems; @@ -10,6 +11,7 @@ namespace Content.Server._DEN.Language.EntitySystems; public sealed partial class LanguageSystem : SharedLanguageSystem { [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedMindSystem _mindSystem = default!; public override void Initialize() { @@ -28,19 +30,22 @@ private void OnHideFontsRequest(HideFontsMessage msg, EntitySessionEventArgs arg if (senderSession.AttachedEntity is not { } senderEnt) return; + if (!_mindSystem.TryGetMind(senderEnt, out var mind, out var mindComp)) + return; + switch (msg.Hide) { case HideLanguageFontSetting.All: - EnsureComp(senderEnt, out var comp); + EnsureComp(mind, out var comp); comp.AllFonts = true; break; case HideLanguageFontSetting.Understood: - EnsureComp(senderEnt, out var comp2); + EnsureComp(mind, out var comp2); comp2.AllFonts = false; break; default: case HideLanguageFontSetting.None: - RemComp(senderEnt); + RemComp(mind); break; } } From edb50897ef2387599e79593b34e348938369231e Mon Sep 17 00:00:00 2001 From: Dirius Date: Sun, 8 Mar 2026 00:31:18 -0500 Subject: [PATCH 07/15] Polymorph languages --- .../Language/EntitySystems/LanguageSystem.cs | 19 +++++++++++++++++++ .../LanguageFollowsMindComponent.cs | 4 ++++ .../EntitySystems/SharedLanguageSystem.API.cs | 17 +++++++++++++++++ .../Prototypes/_DEN/Language/telepathy.yml | 1 + 4 files changed, 41 insertions(+) create mode 100644 Content.Shared/_DEN/Language/Components/LanguageFollowsMindComponent.cs diff --git a/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs b/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs index 465b2cc1e03..f2cf726b4b8 100644 --- a/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs +++ b/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs @@ -4,6 +4,8 @@ using Content.Shared._DEN.Language.EntitySystems; using Content.Shared.Chat; using Content.Shared.Mind; +using Content.Shared.Polymorph; +using Robust.Shared.Containers; using Robust.Shared.Prototypes; namespace Content.Server._DEN.Language.EntitySystems; @@ -20,9 +22,26 @@ public override void Initialize() SubscribeLocalEvent>( OnAttemptUnderstandingRelay); + SubscribeLocalEvent(OnPolymorph); + SubscribeNetworkEvent(OnHideFontsRequest); } + private void OnPolymorph(Entity ent, ref PolymorphedEvent evt) + { + if (!TryGetLanguageEntities(ent, out var languages)) + return; + + foreach (var language in languages) + { + if (!HasComp(language)) + continue; + + _container.TryRemoveFromContainer(language.Owner, true); + TryAddLanguage(evt.NewEntity, language); + } + } + private void OnHideFontsRequest(HideFontsMessage msg, EntitySessionEventArgs args) { var senderSession = args.SenderSession; diff --git a/Content.Shared/_DEN/Language/Components/LanguageFollowsMindComponent.cs b/Content.Shared/_DEN/Language/Components/LanguageFollowsMindComponent.cs new file mode 100644 index 00000000000..e1fe8e01206 --- /dev/null +++ b/Content.Shared/_DEN/Language/Components/LanguageFollowsMindComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Shared._DEN.Language.Components; + +[RegisterComponent] +public sealed partial class LanguageFollowsMindComponent : Component; diff --git a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs index 76174c6f8cf..c664f8e255e 100644 --- a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs +++ b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs @@ -64,6 +64,23 @@ public bool TrySetLanguage(EntityUid target, Entity languageE } #region Add Methods + + /// + /// Adds a language entity to the target entity. + /// + /// + /// + /// + public bool TryAddLanguage(EntityUid target, + Entity languageEntity) + { + var communicator = EnsureComp(target); + if (communicator.Languages is not { } languages) + return false; + + return _container.Insert(languageEntity.Owner, languages); + } + /// /// Adds a language to the target entity. The entity will be able to speak and fully understand the language. /// This may add multiple languages if the language has related languages. diff --git a/Resources/Prototypes/_DEN/Language/telepathy.yml b/Resources/Prototypes/_DEN/Language/telepathy.yml index 53c7a2b37c1..a8b6958c1f8 100644 --- a/Resources/Prototypes/_DEN/Language/telepathy.yml +++ b/Resources/Prototypes/_DEN/Language/telepathy.yml @@ -14,6 +14,7 @@ - type: Gestalt - type: MinimumFluency minimum: Fluent + - type: LanguageFollowsMind - type: languageWrapper id: TelepathyWrapper From f9268d3eab52ddba84b2f204a8eaeb1a6424d2ea Mon Sep 17 00:00:00 2001 From: Dirius Date: Tue, 10 Mar 2026 04:36:41 -0400 Subject: [PATCH 08/15] Fix vocalizing over the radio. --- .../Vocalization/Systems/RadioVocalizationSystem.Language.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Content.Server/_DEN/Vocalization/Systems/RadioVocalizationSystem.Language.cs b/Content.Server/_DEN/Vocalization/Systems/RadioVocalizationSystem.Language.cs index 5354008a713..fc345a186af 100644 --- a/Content.Server/_DEN/Vocalization/Systems/RadioVocalizationSystem.Language.cs +++ b/Content.Server/_DEN/Vocalization/Systems/RadioVocalizationSystem.Language.cs @@ -43,7 +43,8 @@ private bool TrySpeakLanguageRadio(Entity entity, ChatSystem.WhisperWrapper, ChatChannel.Whisper, ChatTransmitRange.Normal, - radioChannel); + radioChannel, + languageOverride: language); return true; } From c251a9295c93e83db83d5b62c28d36bc234a5edf Mon Sep 17 00:00:00 2001 From: Dirius Date: Tue, 10 Mar 2026 18:59:26 -0400 Subject: [PATCH 09/15] Add the ability to 'disable' languages with a CVar --- .../Language/EntitySystems/LanguageSystem.cs | 8 +- .../_DEN/Options/UI/Tabs/DenTab.xaml.cs | 29 +++--- .../Systems/Language/LanguageUIController.cs | 17 ++++ .../_DEN/Chat/Systems/ChatSystem.Language.cs | 6 ++ .../_DEN/Language/Commands/LanguageCommand.cs | 2 +- .../UniversalLanguageSpeakerSystem.cs | 2 +- Content.Shared/_DEN/CCVars/DenCCVars.cs | 20 ++++- .../EntitySystems/SharedLanguageSystem.API.cs | 88 +++++++++++++++++-- .../EntitySystems/SharedLanguageSystem.cs | 41 +++++++-- .../EntitySystems/TranslatorSystem.cs | 2 +- .../en-US/_DEN/language/language-default.ftl | 3 + .../Prototypes/_DEN/Language/default.yml | 10 +++ 12 files changed, 190 insertions(+), 38 deletions(-) create mode 100644 Resources/Locale/en-US/_DEN/language/language-default.ftl create mode 100644 Resources/Prototypes/_DEN/Language/default.yml diff --git a/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs b/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs index 10a1d6f7e6d..fc79401380d 100644 --- a/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs +++ b/Content.Client/_DEN/Language/EntitySystems/LanguageSystem.cs @@ -3,7 +3,6 @@ using Content.Shared._DEN.Language.Components; using Content.Shared._DEN.Language.EntitySystems; using Content.Shared.GameTicking; -using Robust.Shared.Containers; using Robust.Client.Player; namespace Content.Client._DEN.Language.EntitySystems; @@ -14,12 +13,14 @@ public sealed class LanguageSystem : SharedLanguageSystem public event Action>? OnLanguageEntityUpdate; public event Action?>? OnLanguageCommunicatorUpdate; + public event Action? OnLanguagesEnabledUpdate; public override void Initialize() { base.Initialize(); _cfg.OnValueChanged(DenCCVars.HideLanguageFonts, SetHideLanguageFonts); + _cfg.OnValueChanged(DenCCVars.LanguageEnabled, SetLanguageEnabledState); SubscribeLocalEvent(OnLanguageComponentHandleState); SubscribeLocalEvent(OnLanguageCommunicatorHandleState); @@ -27,6 +28,11 @@ public override void Initialize() SubscribeLocalEvent(OnPlayerSpawnComplete); } + private void SetLanguageEnabledState(bool enabled) + { + OnLanguagesEnabledUpdate?.Invoke(enabled); + } + private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent evt) { RaiseNetworkEvent(new HideFontsMessage(_cfg.GetCVar(DenCCVars.HideLanguageFonts))); diff --git a/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs index a04bca5333e..a61a781d472 100644 --- a/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs +++ b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs @@ -1,3 +1,4 @@ +using Content.Client._DEN.Language.EntitySystems; using Content.Client.Options.UI; using Content.Shared._DEN.CCVars; using Robust.Client.AutoGenerated; @@ -13,22 +14,22 @@ public DenTab() { RobustXamlLoader.Load(this); - Control.AddOptionDropDown( - DenCCVars.HideLanguageFonts, + Control.AddOptionDropDown(DenCCVars.HideLanguageFonts, HideLanguageFonts, [ - new OptionDropDownCVar.ValueOption( - HideLanguageFontSetting.None, - Loc.GetString("ui-options-language-hide-fonts-none") - ), - new OptionDropDownCVar.ValueOption( - HideLanguageFontSetting.All, - Loc.GetString("ui-options-language-hide-fonts-all") - ), - new OptionDropDownCVar.ValueOption( - HideLanguageFontSetting.Understood, - Loc.GetString("ui-options-language-hide-fonts-understood") - ), + new OptionDropDownCVar.ValueOption(HideLanguageFontSetting.None, + Loc.GetString("ui-options-language-hide-fonts-none")), + new OptionDropDownCVar.ValueOption(HideLanguageFontSetting.All, + Loc.GetString("ui-options-language-hide-fonts-all")), + new OptionDropDownCVar.ValueOption(HideLanguageFontSetting.Understood, + Loc.GetString("ui-options-language-hide-fonts-understood")), ]); + + IoCManager.Resolve().OnLanguagesEnabledUpdate += OnLanguageEnableChanged; + } + + private void OnLanguageEnableChanged(bool enabled) + { + HideLanguageFonts.Visible = enabled; } } diff --git a/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs b/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs index cfbd23264a6..b9c19bd372a 100644 --- a/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs +++ b/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs @@ -244,6 +244,19 @@ private void NeedsFullRebuild() _window.NeedsFullRebuild = true; } + private void CheckLanguageEnabled(bool enabled) + { + if (_window is { IsOpen: true } && !enabled) + { + _window.Close(); + } + + if (LanguageButton == null) + return; + + LanguageButton.Visible = enabled; + } + private void OnPlayerAttached(EntityUid uid) { NeedsFullRebuild(); @@ -265,6 +278,8 @@ public void OnStateEntered(GameplayState state) _window.OnClose += DeactivateButton; _window.OnOpen += ActivateButton; + CheckLanguageEnabled(_languageSystem.LanguagesEnabled); + CommandBinds.Builder .Bind(ContentKeyFunctions.OpenLanguageMenu, InputCmdHandler.FromDelegate(_ => ToggleWindow())) @@ -288,6 +303,7 @@ public void OnSystemLoaded(LanguageSystem system) { system.OnLanguageEntityUpdate += OnLanguageUpdated; system.OnLanguageCommunicatorUpdate += OnLanguageCommunicatorUpdated; + system.OnLanguagesEnabledUpdate += CheckLanguageEnabled; _playerManager.LocalPlayerAttached += OnPlayerAttached; } @@ -295,6 +311,7 @@ public void OnSystemUnloaded(LanguageSystem system) { system.OnLanguageEntityUpdate -= OnLanguageUpdated; system.OnLanguageCommunicatorUpdate -= OnLanguageCommunicatorUpdated; + system.OnLanguagesEnabledUpdate -= CheckLanguageEnabled; _playerManager.LocalPlayerAttached -= OnPlayerAttached; } } diff --git a/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs b/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs index 2c51955f0a2..b30e6872657 100644 --- a/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs +++ b/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs @@ -36,6 +36,12 @@ public void SendEntityComplexSpeech(EntityUid source, string? verbOverride = null, Entity? languageOverride = null) { + // Eat overrides so we can force the disabled language. + if (!_language.LanguagesEnabled) + { + languageOverride = null; + } + // Getting this first makes sure that if the language defaulted to something new it is set for CanSpeak var retrievedLanguage = languageOverride ?? _language.GetCurrentLanguageEntity(source); if (retrievedLanguage is not { } languageEnt) diff --git a/Content.Server/_DEN/Language/Commands/LanguageCommand.cs b/Content.Server/_DEN/Language/Commands/LanguageCommand.cs index 1a99d335cc4..bacd00b8756 100644 --- a/Content.Server/_DEN/Language/Commands/LanguageCommand.cs +++ b/Content.Server/_DEN/Language/Commands/LanguageCommand.cs @@ -26,7 +26,7 @@ public EntityUid Add([PipedArgument] EntityUid target, [CommandArgument(typeof(FluencyProtoIdParser))] string fluency = "Fluent") { _language ??= GetSys(); - _language.TryAddLanguage(target, language, speaks, fluency, out var _); + _language.TryAddLanguage(target, language, fluency, speaks, out var _); return target; } diff --git a/Content.Server/_DEN/Language/EntitySystems/UniversalLanguageSpeakerSystem.cs b/Content.Server/_DEN/Language/EntitySystems/UniversalLanguageSpeakerSystem.cs index 1135401e68d..fb2fed0e2f1 100644 --- a/Content.Server/_DEN/Language/EntitySystems/UniversalLanguageSpeakerSystem.cs +++ b/Content.Server/_DEN/Language/EntitySystems/UniversalLanguageSpeakerSystem.cs @@ -22,7 +22,7 @@ public override void Initialize() private void OnUniversalLanguageStartup(Entity entity, ref ComponentStartup args) { - if (_language.TryAddLanguage(entity, Universal, true, SharedLanguageSystem.MaximumFluency, out var langs)) + if (_language.TryAddLanguage(entity, Universal, SharedLanguageSystem.MaximumFluency, true, out var langs)) { if (langs.FirstOrNull() is { } lang && TryComp(lang, out var langComp)) { diff --git a/Content.Shared/_DEN/CCVars/DenCCVars.cs b/Content.Shared/_DEN/CCVars/DenCCVars.cs index b44066d253b..aa74e96f2de 100644 --- a/Content.Shared/_DEN/CCVars/DenCCVars.cs +++ b/Content.Shared/_DEN/CCVars/DenCCVars.cs @@ -5,33 +5,45 @@ namespace Content.Shared._DEN.CCVars; [CVarDefs] public sealed class DenCCVars { + /// + /// Allows the Language system to be 'disabled'. This does not actually prevent language related events from + /// occurring, because of how much of the chat infrastructure is replaced with language based systems. Instead + /// this setting hides the language UI on clients, prevents language from being changed, and forces every entity + /// to use a 'Default' language that behaves the same way as language-less chat. + /// + public static readonly CVarDef LanguageEnabled = + CVarDef.Create("languages.language_enabled", true, CVar.ARCHIVE | CVar.SERVER | CVar.NOTIFY | CVar.REPLICATED); + /// /// The maximum number of message translations to cache at a time. /// The total size will cap out at this times the number of languages times the number of /// different 'understanding' variants in use. /// public static readonly CVarDef LanguageMessageCacheSize = - CVarDef.Create("languages.message_cache_size", 20, CVar.ARCHIVE); + CVarDef.Create("languages.message_cache_size", 20, CVar.ARCHIVE | CVar.SERVER); /// /// The number of words to keep in the word cache at a time. /// public static readonly CVarDef LanguageWordCacheSize = - CVarDef.Create("languages.word_cache_size", 50, CVar.ARCHIVE); + CVarDef.Create("languages.word_cache_size", 50, CVar.ARCHIVE | CVar.SERVER); /// /// Whether or not to give an entity that tries speaking without LanguageCommunicatorComponent a language. /// public static readonly CVarDef FallbackDefaultLanguage = - CVarDef.Create("languages.fallback_default_language", false, CVar.ARCHIVE); + CVarDef.Create("languages.fallback_default_language", false, CVar.ARCHIVE | CVar.SERVER); /// /// The default spoken language. If fallback_default_language is set, entities without LanguageCommunicatorComponent /// will use this. Systems that directly send messages will also use this language. /// public static readonly CVarDef DefaultLanguage = - CVarDef.Create("languages.default_language", "Basic", CVar.ARCHIVE); + CVarDef.Create("languages.default_language", "Basic", CVar.ARCHIVE | CVar.SERVER); + /// + /// Client's preference for how to display language fonts. + /// public static readonly CVarDef HideLanguageFonts = CVarDef.Create("languages.hide_fonts", HideLanguageFontSetting.None, CVar.CLIENTONLY | CVar.ARCHIVE); } diff --git a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs index c664f8e255e..bcb8c477fb1 100644 --- a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs +++ b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs @@ -39,7 +39,7 @@ public bool TrySetLanguage(EntityUid target, ProtoId language /// /// Entity to set the language on. /// The language entity to try to set as the spoken language. - /// + /// Whether the operation succeeded. [PublicAPI] public bool TrySetLanguage(EntityUid target, Entity languageEntity) { @@ -70,7 +70,8 @@ public bool TrySetLanguage(EntityUid target, Entity languageE /// /// /// - /// + /// Whether the operation succeeded. + [PublicAPI] public bool TryAddLanguage(EntityUid target, Entity languageEntity) { @@ -94,7 +95,7 @@ public bool TryAddLanguage(EntityUid target, ProtoId language, out List> languageEntities) { - return TryAddLanguage(target, language,true, DefaultLanguageFluency, out languageEntities); + return TryAddLanguage(target, language, DefaultLanguageFluency, true, out languageEntities); } /// @@ -103,15 +104,15 @@ public bool TryAddLanguage(EntityUid target, /// /// The entity to add the language to. /// The ID of the language to add. - /// Whether the target should be able to speak the language. /// The amount of fluency the target should have with the language. + /// Whether the target should be able to speak the language. /// The list of added languages. /// Whether the operation succeeded. Note that languages may have still been added if a related language failed. [PublicAPI] public bool TryAddLanguage(EntityUid target, ProtoId languageProto, - bool speaks, ProtoId fluencyProto, + bool speaks, out List> languageEntities) { languageEntities = []; @@ -186,7 +187,6 @@ public bool TryRemoveLanguages(EntityUid target, ProtoId lang #endregion #region Get Methods - /// /// Fetches the current default language. /// @@ -208,10 +208,24 @@ public ProtoId GetDefaultLanguage() [PublicAPI] public Entity? GetCurrentLanguageEntity(EntityUid target, bool forceDefault = false) { + if (!LanguagesEnabled) + { + if (!TryGetOrAddLanguageEntity(target, DisabledLanguage, out var langEnt)) + { + Log.Warning("Languages are disabled but was unable to add the forced disabled language. This is a bug."); + return null; + } + var comm = EnsureComp(target); + comm.CurrentLanguage = langEnt; + return langEnt; + } + + forceDefault = forceDefault || _fallbackDefaultLanguage; + LanguageCommunicatorComponent? communicator; if (!TryComp(target, out communicator)) { - if (forceDefault || _fallbackDefaultLanguage) + if (forceDefault) { InsertLanguageAndChildren(target, _defaultLanguage, DefaultLanguageFluency, true, out _); communicator = EnsureComp(target); // Should already exist here. @@ -225,7 +239,16 @@ public ProtoId GetDefaultLanguage() if (communicator.CurrentLanguage is null || Deleted(communicator.CurrentLanguage)) { if (!TryGetLanguageEntities(target, out var languageEntities)) - return null; + { + if (!forceDefault) + return null; + + InsertLanguageAndChildren(target, + _defaultLanguage, + DefaultLanguageFluency, + true, + out _); + } var spokenLanguages = languageEntities.FindAll(lang => lang.Comp.Speaks); if (communicator.LastSpokenLanguage is { } lastSpoken) @@ -264,7 +287,7 @@ public ProtoId GetDefaultLanguage() { var languageEnt = GetCurrentLanguageEntity(target); - return languageEnt?.Comp?.Language; + return languageEnt?.Comp.Language; } /// @@ -384,6 +407,53 @@ public bool TryGetLanguages(EntityUid target, return true; } + /// + /// Tries to retrieve a language from an entity if it already has it. Otherwise, adds the language to the entity + /// and returns that. + /// + /// The entity to operate on + /// The language to retrieve + /// The language entity returned + /// Whether the operation was successful + [PublicAPI] + public bool TryGetOrAddLanguageEntity(EntityUid target, + ProtoId language, + [NotNullWhen(true)] out Entity? languageEntity) + { + return TryGetOrAddLanguageEntity(target, language, DefaultLanguageFluency, true, out languageEntity); + } + + /// + /// Tries to retrieve a language from an entity if it already has it. Otherwise, adds the language to the entity + /// and returns that. + /// + /// The entity to operate on + /// The language to retrieve + /// The fluency to add if the entity doesn't have the language + /// Whether the entity should speak the language if added by default + /// The language entity returned + /// Whether the operation was successful + [PublicAPI] + public bool TryGetOrAddLanguageEntity(EntityUid target, + ProtoId language, + ProtoId fluencyProto, + bool speaks, + [NotNullWhen(true)] out Entity? languageEntity) + { + languageEntity = null; + + if (TryGetLanguageEntity(target, language, out languageEntity)) + return true; + + if (TryAddLanguage(target, language, fluencyProto, speaks, out var langs)) + { + languageEntity = langs.FirstOrNull(); + return languageEntity != null; + } + + return false; + } + /// /// Checks whether the provided entity can speak the passed language. /// diff --git a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs index edac874a2c6..68fb00b8dc2 100644 --- a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs +++ b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs @@ -4,30 +4,36 @@ using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Prototypes; -using Robust.Shared.Timing; namespace Content.Shared._DEN.Language.EntitySystems; public abstract partial class SharedLanguageSystem : EntitySystem { + public bool LanguagesEnabled { get; private set; } + [Dependency] protected readonly IConfigurationManager _cfg = default!; [Dependency] protected readonly SharedContainerSystem _container = default!; - [Dependency] protected readonly IGameTiming _timing = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly INetManager _netMan = default!; public static readonly ProtoId MaximumFluency = "Fluent"; public static readonly ProtoId MinimumFluency = "Unfamiliar"; + public static readonly ProtoId DisabledLanguage = "Default"; + private static ProtoId _defaultLanguage = "Basic"; private bool _fallbackDefaultLanguage; private EntityQuery _languageQuery; + private EntityQueryEnumerator _languageCommunicatorQuery; public override void Initialize() { base.Initialize(); + _languageQuery = GetEntityQuery(); + _languageCommunicatorQuery = EntityQueryEnumerator(); + SubscribeLocalEvent(OnLanguageCommunicatorCompInit); SubscribeLocalEvent(OnLanguageCommunicatorMapInit); SubscribeLocalEvent(OnLanguageCommunicatorShutdown); @@ -42,8 +48,26 @@ public override void Initialize() _cfg.OnValueChanged(DenCCVars.FallbackDefaultLanguage, fallback => _fallbackDefaultLanguage = fallback, true); _cfg.OnValueChanged(DenCCVars.DefaultLanguage, lang => _defaultLanguage = lang, true); + _cfg.OnValueChanged(DenCCVars.LanguageEnabled, OnLanguageEnableChanged, true); + } - _languageQuery = GetEntityQuery(); + private void OnLanguageEnableChanged(bool enabled) + { + LanguagesEnabled = enabled; + + if (!enabled) + return; + + while (_languageCommunicatorQuery.MoveNext(out var entity, out _)) + { + if (!TryGetLanguageEntities(entity, DisabledLanguage, out var languages)) + continue; + + foreach (var lang in languages) + { + PredictedQueueDel(lang); + } + } } private void OnRequestSetSpokenLanguage(RequestSetSpokenLanguageEvent evt, EntitySessionEventArgs args) @@ -53,7 +77,7 @@ private void OnRequestSetSpokenLanguage(RequestSetSpokenLanguageEvent evt, Entit var languageEnt = GetEntity(evt.LanguageEntity); - if (!TryComp(languageEnt, out var langComp)) + if (!TryComp(languageEnt, out var langComp) || langComp.Holder != user) return; TrySetLanguage(user, (languageEnt, langComp)); @@ -66,9 +90,12 @@ private void OnLanguageCommunicatorCompInit(Entity ent, ref MapInitEvent evt) { + if (!LanguagesEnabled) + return; + foreach (var (language, (speaks, fluency)) in ent.Comp.BaseLanguages) { - TryAddLanguage(ent, language, speaks, fluency, out var lang); + TryAddLanguage(ent, language, fluency, speaks, out _); } } @@ -177,8 +204,8 @@ private Entity SpawnLanguageEntity(ProtoId languageComp.Fluency = fluencyProto; languageComp.Language = languageProto; languageComp.Speaks = speaks; - if (language.LanguageComponents is not null) - EntityManager.AddComponents(languageEnt, language.LanguageComponents); + + EntityManager.AddComponents(languageEnt, language.LanguageComponents); return (languageEnt, languageComp); } diff --git a/Content.Shared/_DEN/Language/EntitySystems/TranslatorSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/TranslatorSystem.cs index 7443f37a5c1..e2404a19579 100644 --- a/Content.Shared/_DEN/Language/EntitySystems/TranslatorSystem.cs +++ b/Content.Shared/_DEN/Language/EntitySystems/TranslatorSystem.cs @@ -125,7 +125,7 @@ private void TryAddTranslation(Entity ent, EntityUid target List> languages = []; foreach (var (lang, (speaks, fluency)) in ent.Comp.GrantedLanguageProtos) { - _language.TryAddLanguage(target, lang, speaks, fluency, out var newLangs); + _language.TryAddLanguage(target, lang, fluency, speaks, out var newLangs); languages.AddRange(newLangs); } ent.Comp.GrantedLanguages.AddRange(languages); diff --git a/Resources/Locale/en-US/_DEN/language/language-default.ftl b/Resources/Locale/en-US/_DEN/language/language-default.ftl new file mode 100644 index 00000000000..0c4731b31bc --- /dev/null +++ b/Resources/Locale/en-US/_DEN/language/language-default.ftl @@ -0,0 +1,3 @@ +language-default = Default +language-default-abbreviation = Default +language-default-description = Languages are disabled. diff --git a/Resources/Prototypes/_DEN/Language/default.yml b/Resources/Prototypes/_DEN/Language/default.yml new file mode 100644 index 00000000000..ea342924775 --- /dev/null +++ b/Resources/Prototypes/_DEN/Language/default.yml @@ -0,0 +1,10 @@ +- type: language + id: Default + name: language-default + displayInChat: false + components: + - type: SpeechTransformable + - type: RadioTransmittable + - type: Audible + - type: WhisperMuffle + muffle: true From f9e7143db92607334bff6a7aa4123cbc9f71c212 Mon Sep 17 00:00:00 2001 From: Dirius Date: Tue, 10 Mar 2026 19:16:02 -0400 Subject: [PATCH 10/15] Add a CVar for removing detailed speech. --- Content.Shared/_DEN/CCVars/DenCCVars.cs | 7 +++++++ Content.Shared/_DEN/Chat/SharedChatSystem.Language.cs | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Content.Shared/_DEN/CCVars/DenCCVars.cs b/Content.Shared/_DEN/CCVars/DenCCVars.cs index aa74e96f2de..7c245a8068c 100644 --- a/Content.Shared/_DEN/CCVars/DenCCVars.cs +++ b/Content.Shared/_DEN/CCVars/DenCCVars.cs @@ -14,6 +14,13 @@ public sealed class DenCCVars public static readonly CVarDef LanguageEnabled = CVarDef.Create("languages.language_enabled", true, CVar.ARCHIVE | CVar.SERVER | CVar.NOTIFY | CVar.REPLICATED); + /// + /// Whether or not to allow detailed speech, that is, prefixing a message with an ! in order to allow special + /// formatting related to mixed emotes and dialogs in a message, or emoting over the radio. + /// + public static readonly CVarDef DetailedSpeechEnabled = + CVarDef.Create("languages.detailed_speech_enabled", true, CVar.ARCHIVE | CVar.SERVER); + /// /// The maximum number of message translations to cache at a time. /// The total size will cap out at this times the number of languages times the number of diff --git a/Content.Shared/_DEN/Chat/SharedChatSystem.Language.cs b/Content.Shared/_DEN/Chat/SharedChatSystem.Language.cs index 6132368120d..c14f1b97b3f 100644 --- a/Content.Shared/_DEN/Chat/SharedChatSystem.Language.cs +++ b/Content.Shared/_DEN/Chat/SharedChatSystem.Language.cs @@ -1,7 +1,9 @@ using System.Linq; using System.Text; +using Content.Shared._DEN.CCVars; using Content.Shared._DEN.Language; using Content.Shared.Speech; +using Robust.Shared.Configuration; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -10,6 +12,8 @@ namespace Content.Shared.Chat; public abstract partial class SharedChatSystem { + [Dependency] private readonly IConfigurationManager _cfg = default!; + // TODO: Kill the other spot where this is getting called from and more this into WhisperMuffle (if we even keep using it) public ComplexChatMessage ObfuscateComplexChatMessage(ComplexChatMessage message, float amount) { @@ -66,7 +70,7 @@ public ComplexChatMessage ConvertMessageToComplex(string message) var isDetailed = false; var needsSpacing = true; var needsSeparation = false; - if (message.StartsWith('!')) + if (_cfg.GetCVar(DenCCVars.DetailedSpeechEnabled) && message.StartsWith('!')) { isDetailed = true; message = message[1..].Trim(); From ae2106f6d1181b97d9176d86e82a96f10f746643 Mon Sep 17 00:00:00 2001 From: Dirius Date: Wed, 11 Mar 2026 19:25:23 -0400 Subject: [PATCH 11/15] That's not how THESE ones are used. --- .../_DEN/Language/EntitySystems/SharedLanguageSystem.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs index 68fb00b8dc2..4c08f15e36b 100644 --- a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs +++ b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.cs @@ -25,14 +25,12 @@ public abstract partial class SharedLanguageSystem : EntitySystem private bool _fallbackDefaultLanguage; private EntityQuery _languageQuery; - private EntityQueryEnumerator _languageCommunicatorQuery; public override void Initialize() { base.Initialize(); _languageQuery = GetEntityQuery(); - _languageCommunicatorQuery = EntityQueryEnumerator(); SubscribeLocalEvent(OnLanguageCommunicatorCompInit); SubscribeLocalEvent(OnLanguageCommunicatorMapInit); @@ -58,7 +56,8 @@ private void OnLanguageEnableChanged(bool enabled) if (!enabled) return; - while (_languageCommunicatorQuery.MoveNext(out var entity, out _)) + var query = EntityQueryEnumerator(); + while(query.MoveNext(out var entity, out _)) { if (!TryGetLanguageEntities(entity, DisabledLanguage, out var languages)) continue; From 6f640c46801a7cc0a4832f23e5ee9cfbfdfb1a90 Mon Sep 17 00:00:00 2001 From: Dirius Date: Thu, 12 Mar 2026 03:39:00 -0400 Subject: [PATCH 12/15] GenPop IDs talk so they need a language too... --- .../Entities/Objects/Misc/identification_cards.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml index 189982ff7d1..f991882d9ca 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml @@ -877,6 +877,13 @@ - Recyclable - type: StaticPrice # these are infinitely producible. price: 0 + # DEN: It speaks, it needs languages. + - type: ContainerContainer + containers: + languages: !type:Container + - type: LanguageCommunicator + languages: + Basic: [ true, Fluent ] - type: entity parent: IDCardStandard From 8834b4acb983b93ccb0b70d5c0da1f9d01fd4362 Mon Sep 17 00:00:00 2001 From: Dirius Date: Thu, 19 Mar 2026 03:34:25 -0400 Subject: [PATCH 13/15] No dependency on the system in the UI. --- Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs index a61a781d472..b7f52b81b9b 100644 --- a/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs +++ b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs @@ -4,6 +4,7 @@ using Robust.Client.AutoGenerated; using Robust.Client.UserInterface; using Robust.Client.UserInterface.XAML; +using Robust.Shared.Configuration; namespace Content.Client._DEN.Options.UI.Tabs; @@ -25,7 +26,7 @@ public DenTab() Loc.GetString("ui-options-language-hide-fonts-understood")), ]); - IoCManager.Resolve().OnLanguagesEnabledUpdate += OnLanguageEnableChanged; + IoCManager.Resolve().OnValueChanged(DenCCVars.LanguageEnabled, OnLanguageEnableChanged); } private void OnLanguageEnableChanged(bool enabled) From 87567bd4de44612e2e1f973028ba8c33a912d88f Mon Sep 17 00:00:00 2001 From: Dirius Date: Thu, 19 Mar 2026 22:48:57 -0400 Subject: [PATCH 14/15] Add radial language selection wheel. --- Content.Client/Input/ContentContexts.cs | 1 + .../Options/UI/Tabs/KeyRebindTab.xaml.cs | 1 + .../Language/LanguageQuickMenuController.cs | 112 ++++++++++++++++++ .../Systems/Language/LanguageUIController.cs | 19 +-- Content.Shared/Input/ContentKeyFunctions.cs | 1 + .../EntitySystems/SharedLanguageSystem.API.cs | 46 +++++++ .../Language/Prototypes/LanguagePrototype.cs | 4 + Resources/Prototypes/_DEN/Language/basic.yml | 3 + .../_DEN/Language/debug-related.yml | 3 + Resources/Prototypes/_DEN/Language/sign.yml | 3 + .../Prototypes/_DEN/Language/telepathy.yml | 3 + Resources/keybinds.yml | 4 + 12 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 Content.Client/_DEN/UserInterface/Systems/Language/LanguageQuickMenuController.cs diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index cce17f50c00..91a54660483 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -66,6 +66,7 @@ public static void SetupContexts(IInputContextContainer contexts) human.AddFunction(ContentKeyFunctions.OpenCharacterMenu); human.AddFunction(ContentKeyFunctions.OpenEmotesMenu); human.AddFunction(ContentKeyFunctions.OpenLanguageMenu); // DEN: Languages + human.AddFunction(ContentKeyFunctions.OpenQuickLanguageMenu); // DEN: Languages human.AddFunction(ContentKeyFunctions.ActivateItemInWorld); human.AddFunction(ContentKeyFunctions.ThrowItemInHand); human.AddFunction(ContentKeyFunctions.AltActivateItemInWorld); diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index a3987e781c1..dc6f42c02da 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -233,6 +233,7 @@ void AddToggleCvarCheckBox(string checkBoxName, CVarDef cvar) AddButton(ContentKeyFunctions.OpenActionsMenu); AddButton(ContentKeyFunctions.OpenEmotesMenu); AddButton(ContentKeyFunctions.OpenLanguageMenu); // DEN: Languages + AddButton(ContentKeyFunctions.OpenQuickLanguageMenu); // DEN: Languages AddButton(ContentKeyFunctions.ToggleRoundEndSummaryWindow); AddButton(ContentKeyFunctions.OpenEntitySpawnWindow); AddButton(ContentKeyFunctions.OpenSandboxWindow); diff --git a/Content.Client/_DEN/UserInterface/Systems/Language/LanguageQuickMenuController.cs b/Content.Client/_DEN/UserInterface/Systems/Language/LanguageQuickMenuController.cs new file mode 100644 index 00000000000..19ba8e3b934 --- /dev/null +++ b/Content.Client/_DEN/UserInterface/Systems/Language/LanguageQuickMenuController.cs @@ -0,0 +1,112 @@ +using System.Linq; +using Content.Client._DEN.Language.EntitySystems; +using Content.Client.Gameplay; +using Content.Client.UserInterface.Controls; +using Content.Shared._DEN.Language.Components; +using Content.Shared.Input; +using JetBrains.Annotations; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controllers; +using Robust.Shared.Input.Binding; +using Robust.Shared.Prototypes; + +namespace Content.Client._DEN.UserInterface.Systems.Language; + +[UsedImplicitly] +public sealed class LanguageQuickMenuController : UIController, IOnStateChanged, IOnSystemChanged +{ + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [UISystemDependency] private readonly LanguageSystem _languageSystem = default!; + + private SimpleRadialMenu? _menu; + + private void ToggleMenu() + { + if (_menu is { IsOpen: true }) + { + _menu?.Close(); + } + else if (_menu is not null && _playerManager.LocalEntity is {} player) + { + var buttons = BuildLanguageButtons(player); + + _menu.SetButtons(buttons ?? []); + _menu.OpenOverMouseScreenPosition(); + } + } + + private IEnumerable? BuildLanguageButtons(EntityUid player) + { + if (!_languageSystem.TryGetSpokenLanguageEntities(player, out var languages)) + return null; + + var languageButtons = languages.Select(language => + { + var langProto = _prototypeManager.Index(language.Comp.Language); + return new RadialMenuActionOption>(OnLanguageChosen, language) + { + IconSpecifier = RadialMenuIconSpecifier.With(langProto.Icon), + ToolTip = langProto.LocalizedName, + }; + }); + return languageButtons; + + } + + private void OnLanguageChosen(Entity language) + { + _languageSystem.TrySetSpokenLanguage(language); + } + + private void CheckLanguageEnabled(bool enabled) + { + if (_menu is { IsOpen: true } && !enabled) + { + _menu.Close(); + _menu = null; + } + + if (enabled) + { + if (_menu is null) + _menu = UIManager.CreateWindow(); + + CommandBinds.Builder + .Bind(ContentKeyFunctions.OpenQuickLanguageMenu, + InputCmdHandler.FromDelegate(_ => ToggleMenu())) + .Register(); + } + else + { + CommandBinds.Unregister(); + } + } + + public void OnStateEntered(GameplayState state) + { + CheckLanguageEnabled(_languageSystem.LanguagesEnabled); + } + + public void OnStateExited(GameplayState state) + { + if (_menu != null) + { + _menu.Close(); + _menu = null; + } + + CommandBinds.Unregister(); + } + + public void OnSystemLoaded(LanguageSystem system) + { + system.OnLanguagesEnabledUpdate += CheckLanguageEnabled; + } + + public void OnSystemUnloaded(LanguageSystem system) + { + system.OnLanguagesEnabledUpdate -= CheckLanguageEnabled; + } +} diff --git a/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs b/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs index b9c19bd372a..613ef24bdca 100644 --- a/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs +++ b/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs @@ -2,13 +2,11 @@ using Content.Client._DEN.Language.EntitySystems; using Content.Client._DEN.UserInterface.Systems.Language.Controls; using Content.Client._DEN.UserInterface.Systems.Language.Windows; -using Content.Client.CrewManifest.UI; using Content.Client.Gameplay; using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Systems.MenuBar.Widgets; using Content.Shared._DEN.Language.Components; using Content.Shared.Input; -using Content.Shared.Interaction.Events; using JetBrains.Annotations; using Robust.Client.Player; using Robust.Client.UserInterface; @@ -254,6 +252,17 @@ private void CheckLanguageEnabled(bool enabled) if (LanguageButton == null) return; + if (enabled) + { + CommandBinds.Builder + .Bind(ContentKeyFunctions.OpenLanguageMenu, + InputCmdHandler.FromDelegate(_ => ToggleWindow())) + .Register(); + } + else + { + CommandBinds.Unregister(); + } LanguageButton.Visible = enabled; } @@ -280,10 +289,6 @@ public void OnStateEntered(GameplayState state) CheckLanguageEnabled(_languageSystem.LanguagesEnabled); - CommandBinds.Builder - .Bind(ContentKeyFunctions.OpenLanguageMenu, - InputCmdHandler.FromDelegate(_ => ToggleWindow())) - .Register(); NeedsFullRebuild(); } @@ -295,8 +300,6 @@ public void OnStateExited(GameplayState state) _window.Close(); _window = null; } - - CommandBinds.Unregister(); } public void OnSystemLoaded(LanguageSystem system) diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index dd7109635cc..b126b37dcec 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -135,5 +135,6 @@ public static BoundKeyFunction[] GetHotbarBoundKeys() => public static readonly BoundKeyFunction MappingOpenContextMenu = "MappingOpenContextMenu"; public static readonly BoundKeyFunction OpenLanguageMenu = "OpenLanguageMenu"; // DEN: Languages + public static readonly BoundKeyFunction OpenQuickLanguageMenu = "OpenQuickLanguageMenu"; // DEN: Languages } } diff --git a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs index bcb8c477fb1..f5d33073af1 100644 --- a/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs +++ b/Content.Shared/_DEN/Language/EntitySystems/SharedLanguageSystem.API.cs @@ -311,6 +311,29 @@ public bool TryGetLanguageEntity(EntityUid target, return true; } + /// + /// Retrieves all the language entities from a target which it speaks. + /// + /// The target entities + /// All the language entities on the target which it can speak. + /// Whether any languages were returned. + [PublicAPI] + public bool TryGetSpokenLanguageEntities(EntityUid target, + out List> languageEntities) + { + languageEntities = []; + + if (TryGetLanguageEntities(target, out var languages)) + { + languageEntities.AddRange( + from languageEnt in languages + where languageEnt.Comp.Speaks + select languageEnt); + } + + return languageEntities.Count > 0; + } + /// /// Retrieves all the language entities from a target. /// @@ -361,6 +384,29 @@ public bool TryGetLanguageEntities(EntityUid target, return languageEntities.Count > 0; } + /// + /// Retrieves a list of all the languages which an entity speaks. + /// + /// The target entity. + /// The list of spoken languages in the form (LanguageProtoID, FluencyID, speaks) + /// Whether any spoken languages were retrieved. + [PublicAPI] + public bool TryGetSpokenLanguages(EntityUid target, + out List<(ProtoId, ProtoId, bool)> languages) + { + languages = []; + + if (TryGetLanguages(target, out var allLangs)) + { + languages.AddRange( + from language in allLangs + where language.Item3 + select language); + } + + return languages.Count > 0; + } + /// /// Retrieves a list of all the languages an entity has matching the passed prototype /// as well as their fluency values and speaking state. diff --git a/Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs b/Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs index 3d0f3826d66..d526e95cb40 100644 --- a/Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs +++ b/Content.Shared/_DEN/Language/Prototypes/LanguagePrototype.cs @@ -3,6 +3,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; +using Robust.Shared.Utility; namespace Content.Shared._DEN.Language; @@ -32,6 +33,9 @@ public sealed partial class LanguagePrototype : IPrototype, IInheritingPrototype [ViewVariables(VVAccess.ReadOnly)] public string LocalizedDescription => Loc.GetString(Description); + [DataField] + public SpriteSpecifier Icon; + [ViewVariables] [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] public string[]? Parents { get; private set; } diff --git a/Resources/Prototypes/_DEN/Language/basic.yml b/Resources/Prototypes/_DEN/Language/basic.yml index 5b8b48725da..ff615fa532b 100644 --- a/Resources/Prototypes/_DEN/Language/basic.yml +++ b/Resources/Prototypes/_DEN/Language/basic.yml @@ -3,6 +3,9 @@ id: Basic name: language-basic displayInChat: true + icon: + sprite: "/Textures/Effects/speech.rsi" + state: default0 components: - type: SyllableScrambling syllables: diff --git a/Resources/Prototypes/_DEN/Language/debug-related.yml b/Resources/Prototypes/_DEN/Language/debug-related.yml index 0df1a26c08d..3c3ddf1f69e 100644 --- a/Resources/Prototypes/_DEN/Language/debug-related.yml +++ b/Resources/Prototypes/_DEN/Language/debug-related.yml @@ -22,6 +22,9 @@ id: DebugParent name: language-debug-parent displayInChat: true + icon: + sprite: "/Textures/Effects/speech.rsi" + state: xenoborg0 relatedLanguages: DebugChild1: Related DebugChild2: Related diff --git a/Resources/Prototypes/_DEN/Language/sign.yml b/Resources/Prototypes/_DEN/Language/sign.yml index f2506bebf45..f84c7a18ff6 100644 --- a/Resources/Prototypes/_DEN/Language/sign.yml +++ b/Resources/Prototypes/_DEN/Language/sign.yml @@ -9,6 +9,9 @@ defaultVerb: SignVerbWhisper wrapperOverrides: Local: SignWrapper + icon: + sprite: "/Textures/Effects/speech.rsi" + state: clock0 components: - type: LineOfSightLanguage - type: VisualName diff --git a/Resources/Prototypes/_DEN/Language/telepathy.yml b/Resources/Prototypes/_DEN/Language/telepathy.yml index a8b6958c1f8..ad71ab8c4ee 100644 --- a/Resources/Prototypes/_DEN/Language/telepathy.yml +++ b/Resources/Prototypes/_DEN/Language/telepathy.yml @@ -5,6 +5,9 @@ fontId: BoxRound wrapperOverrides: Local: TelepathyWrapper + icon: + sprite: "/Textures/Effects/speech.rsi" + state: alien0 components: - type: ReplaceSpeakerName replaceName: TELEPATHIC diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 7b6f701c88f..0b0cd80c406 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -655,3 +655,7 @@ binds: - function: OpenLanguageMenu type: State key: L +- function: OpenQuickLanguageMenu + type: State + key: L + mod1: Shift From d3e0d63e30df117658c57a7b210b175f134624df Mon Sep 17 00:00:00 2001 From: Dirius Date: Thu, 19 Mar 2026 23:08:13 -0400 Subject: [PATCH 15/15] Localization wooo --- Resources/Locale/en-US/_DEN/language/language-ui.ftl | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/Locale/en-US/_DEN/language/language-ui.ftl b/Resources/Locale/en-US/_DEN/language/language-ui.ftl index 0edcdad2c48..ba7d70d5323 100644 --- a/Resources/Locale/en-US/_DEN/language/language-ui.ftl +++ b/Resources/Locale/en-US/_DEN/language/language-ui.ftl @@ -1,6 +1,7 @@ language-window-title = Languages game-hud-open-language-menu-button-tooltip = Open the language selection menu ui-options-function-open-language-menu = Open the language selection menu +ui-options-function-open-quick-language-menu = Open the language quick select menu language-ui-language-fluency = Fluency: [color={$color}]{$fluency}[/color] language-ui-language-description = Details: language-ui-language-currently-speaking = Currently Speaking: