diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs
index 01e7dc367c3..91a54660483 100644
--- a/Content.Client/Input/ContentContexts.cs
+++ b/Content.Client/Input/ContentContexts.cs
@@ -65,6 +65,8 @@ 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.OpenQuickLanguageMenu); // 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..dc6f42c02da 100644
--- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
@@ -232,6 +232,8 @@ void AddToggleCvarCheckBox(string checkBoxName, CVarDef cvar)
AddButton(ContentKeyFunctions.OpenAHelp);
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/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 event Action? OnLanguagesEnabledUpdate;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _cfg.OnValueChanged(DenCCVars.HideLanguageFonts, SetHideLanguageFonts);
+ _cfg.OnValueChanged(DenCCVars.LanguageEnabled, SetLanguageEnabledState);
+
+ SubscribeLocalEvent(OnLanguageComponentHandleState);
+ SubscribeLocalEvent(OnLanguageCommunicatorHandleState);
+
+ SubscribeLocalEvent(OnPlayerSpawnComplete);
+ }
+
+ private void SetLanguageEnabledState(bool enabled)
+ {
+ OnLanguagesEnabledUpdate?.Invoke(enabled);
+ }
+
+ private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent evt)
+ {
+ RaiseNetworkEvent(new HideFontsMessage(_cfg.GetCVar(DenCCVars.HideLanguageFonts)));
+ }
+
+ private void SetHideLanguageFonts(HideLanguageFontSetting 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..e575342b1b0
--- /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..b7f52b81b9b
--- /dev/null
+++ b/Content.Client/_DEN/Options/UI/Tabs/DenTab.xaml.cs
@@ -0,0 +1,36 @@
+using Content.Client._DEN.Language.EntitySystems;
+using Content.Client.Options.UI;
+using Content.Shared._DEN.CCVars;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Configuration;
+
+namespace Content.Client._DEN.Options.UI.Tabs;
+
+[GenerateTypedNameReferences]
+public sealed partial class DenTab : Control
+{
+ public DenTab()
+ {
+ RobustXamlLoader.Load(this);
+
+ 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")),
+ ]);
+
+ IoCManager.Resolve().OnValueChanged(DenCCVars.LanguageEnabled, OnLanguageEnableChanged);
+ }
+
+ private void OnLanguageEnableChanged(bool enabled)
+ {
+ HideLanguageFonts.Visible = enabled;
+ }
+}
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/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
new file mode 100644
index 00000000000..613ef24bdca
--- /dev/null
+++ b/Content.Client/_DEN/UserInterface/Systems/Language/LanguageUIController.cs
@@ -0,0 +1,320 @@
+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.Gameplay;
+using Content.Client.UserInterface.Controls;
+using Content.Client.UserInterface.Systems.MenuBar.Widgets;
+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.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 CheckLanguageEnabled(bool enabled)
+ {
+ if (_window is { IsOpen: true } && !enabled)
+ {
+ _window.Close();
+ }
+
+ if (LanguageButton == null)
+ return;
+
+ if (enabled)
+ {
+ CommandBinds.Builder
+ .Bind(ContentKeyFunctions.OpenLanguageMenu,
+ InputCmdHandler.FromDelegate(_ => ToggleWindow()))
+ .Register();
+ }
+ else
+ {
+ CommandBinds.Unregister();
+ }
+ LanguageButton.Visible = enabled;
+ }
+
+ 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;
+
+ CheckLanguageEnabled(_languageSystem.LanguagesEnabled);
+
+
+ NeedsFullRebuild();
+ }
+
+ public void OnStateExited(GameplayState state)
+ {
+ if (_window != null)
+ {
+ _window.Close();
+ _window = null;
+ }
+ }
+
+ public void OnSystemLoaded(LanguageSystem system)
+ {
+ system.OnLanguageEntityUpdate += OnLanguageUpdated;
+ system.OnLanguageCommunicatorUpdate += OnLanguageCommunicatorUpdated;
+ system.OnLanguagesEnabledUpdate += CheckLanguageEnabled;
+ _playerManager.LocalPlayerAttached += OnPlayerAttached;
+ }
+
+ public void OnSystemUnloaded(LanguageSystem system)
+ {
+ system.OnLanguageEntityUpdate -= OnLanguageUpdated;
+ system.OnLanguageCommunicatorUpdate -= OnLanguageCommunicatorUpdated;
+ system.OnLanguagesEnabledUpdate -= CheckLanguageEnabled;
+ _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/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/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..b30e6872657
--- /dev/null
+++ b/Content.Server/_DEN/Chat/Systems/ChatSystem.Language.cs
@@ -0,0 +1,456 @@
+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.Mind;
+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!;
+ [Dependency] private readonly SharedMindSystem _mindSystem = 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)
+ {
+ // 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)
+ {
+ 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 hasMaxUnderstanding = understanding >= _prototypeManager.Index(SharedLanguageSystem.MaximumFluency);
+ var useLanguageFont = true;
+ if (_mindSystem.TryGetMind(listener, out var mindId, out _) &&
+ TryComp(mindId, out var suppression))
+ {
+ useLanguageFont = !(suppression.AllFonts || hasMaxUnderstanding);
+ }
+ 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/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.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..bacd00b8756
--- /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, fluency, speaks, 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..f2cf726b4b8
--- /dev/null
+++ b/Content.Server/_DEN/Language/EntitySystems/LanguageSystem.cs
@@ -0,0 +1,105 @@
+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.Mind;
+using Content.Shared.Polymorph;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+
+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()
+ {
+ base.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;
+
+ 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(mind, out var comp);
+ comp.AllFonts = true;
+ break;
+ case HideLanguageFontSetting.Understood:
+ EnsureComp(mind, out var comp2);
+ comp2.AllFonts = false;
+ break;
+ default:
+ case HideLanguageFontSetting.None:
+ RemComp(mind);
+ break;
+ }
+ }
+
+ 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..fb2fed0e2f1
--- /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, SharedLanguageSystem.MaximumFluency, true, 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..285d50a7bad
--- /dev/null
+++ b/Content.Server/_DEN/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.Language.cs
@@ -0,0 +1,60 @@
+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 EntityQuery _audibleQuery;
+ private EntityQuery _losQuery;
+
+ private void InitializeLanguage()
+ {
+ _audibleQuery = GetEntityQuery();
+ _losQuery = GetEntityQuery();
+
+ 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..fc345a186af
--- /dev/null
+++ b/Content.Server/_DEN/Vocalization/Systems/RadioVocalizationSystem.Language.cs
@@ -0,0 +1,51 @@
+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,
+ languageOverride: language);
+
+ 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