diff --git a/Content.Client/SS220/Languages/LanguageSystem.cs b/Content.Client/SS220/Languages/LanguageSystem.cs new file mode 100644 index 000000000000..2768b68d16b7 --- /dev/null +++ b/Content.Client/SS220/Languages/LanguageSystem.cs @@ -0,0 +1,19 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using Content.Shared.SS220.Language; +using Content.Shared.SS220.Language.Systems; +using Robust.Shared.Network; + +namespace Content.Client.SS220.Languages; + +public sealed partial class LanguageSystem : SharedLanguageSystem +{ + [Dependency] private readonly INetManager _net = default!; + + public override void Initialize() + { + base.Initialize(); + + _net.RegisterNetMessage(); + } +} + diff --git a/Content.Client/SS220/UserInterface/System/Chat/Controls/LanguageSettings/LanguageSettingsButton.cs b/Content.Client/SS220/UserInterface/System/Chat/Controls/LanguageSettings/LanguageSettingsButton.cs new file mode 100644 index 000000000000..10bace4be990 --- /dev/null +++ b/Content.Client/SS220/UserInterface/System/Chat/Controls/LanguageSettings/LanguageSettingsButton.cs @@ -0,0 +1,37 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using Content.Client.Resources; +using Content.Client.UserInterface.Systems.Chat.Controls; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.Controls; +using System.Numerics; + +namespace Content.Client.SS220.UserInterface.System.Chat.Controls.LanguageSettings; + +public sealed class LanguageSettingsButton : ChatPopupButton +{ + public LanguageSettingsButton() + { + IoCManager.InjectDependencies(this); + + var texture = IoCManager.Resolve() + .GetTexture("/Textures/SS220/Interface/Nano/language_settings-button.png"); + + AddChild(new TextureRect + { + Texture = texture, + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + }); + + OnPressed += _ => Popup.Update(); + } + + protected override UIBox2 GetPopupPosition() + { + var globalPos = GlobalPosition; + var (minX, minY) = Popup.MinSize; + return UIBox2.FromDimensions( + globalPos, + new Vector2(Math.Max(minX, Popup.MinWidth), minY)); + } +} diff --git a/Content.Client/SS220/UserInterface/System/Chat/Controls/LanguageSettings/LanguageSettingsPopup.xaml b/Content.Client/SS220/UserInterface/System/Chat/Controls/LanguageSettings/LanguageSettingsPopup.xaml new file mode 100644 index 000000000000..5567f8dfc313 --- /dev/null +++ b/Content.Client/SS220/UserInterface/System/Chat/Controls/LanguageSettings/LanguageSettingsPopup.xaml @@ -0,0 +1,10 @@ + + + + + diff --git a/Content.Client/SS220/UserInterface/System/Chat/Controls/LanguageSettings/LanguageSettingsPopup.xaml.cs b/Content.Client/SS220/UserInterface/System/Chat/Controls/LanguageSettings/LanguageSettingsPopup.xaml.cs new file mode 100644 index 000000000000..08b6bebd1c9d --- /dev/null +++ b/Content.Client/SS220/UserInterface/System/Chat/Controls/LanguageSettings/LanguageSettingsPopup.xaml.cs @@ -0,0 +1,126 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using Content.Shared.SS220.Language; +using Content.Shared.SS220.Language.Components; +using Robust.Client.AutoGenerated; +using Robust.Client.Player; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Network; +using Robust.Shared.Utility; + +namespace Content.Client.SS220.UserInterface.System.Chat.Controls.LanguageSettings; + +[GenerateTypedNameReferences] +public sealed partial class LanguageSettingsPopup : Popup +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly LanguageManager _language = default!; + + private Dictionary _buttonsDict = new(); + private Button? _selectedLanguage; + + public LanguageSettingsPopup() + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + Update(); + } + + public void Update() + { + var uid = _player.LocalEntity; + if (uid == null || + !_entityManager.TryGetComponent(uid, out var comp)) + { + UpdateLanguageContainer(null); + UpdateSelectedLanguage(null); + return; + } + + var availableLanguages = new List(); + foreach (var def in comp.AvailableLanguages) + { + if (!def.CanSpeak || + !_language.TryGetLanguageById(def.Id, out var language)) + continue; + + availableLanguages.Add(language); + } + UpdateLanguageContainer(availableLanguages); + + if (comp.SelectedLanguage is { } selectedLanguageId) + { + _language.TryGetLanguageById(selectedLanguageId, out var selectedLanguage); + UpdateSelectedLanguage(selectedLanguage); + } + else + UpdateSelectedLanguage(null); + } + + private void UpdateLanguageContainer(List? availableLanguages) + { + LanguageContainer.DisposeAllChildren(); + _buttonsDict.Clear(); + + if (availableLanguages == null || + availableLanguages.Count <= 0) + { + LanguageLabel.Text = Loc.GetString("language-settings-ui-no-languages"); + return; + } + + LanguageLabel.Text = Loc.GetString("language-settings-ui-select-default"); + foreach (var language in availableLanguages) + { + var languageKey = $"{_language.KeyPrefix}{language.Key}"; + var text = Loc.GetString("language-settings-ui-field-name", + ("name", Loc.GetString(language.Name)), ("key", languageKey)); + var button = new Button() { Text = text }; + + if (language.Description is { } desc) + { + var tooltip = new Tooltip(); + tooltip.SetMessage(FormattedMessage.FromMarkupPermissive(Loc.GetString(language.Description))); + button.TooltipSupplier = _ => tooltip; + } + + _buttonsDict.Add(language, button); + button.OnPressed += _ => + { + UpdateSelectedLanguage(language); + var msg = new ClientSelectlanguageMessage + { + LanguageId = language.ID + }; + _net.ClientSendMessage(msg); + }; + LanguageContainer.AddChild(button); + } + } + + private void UpdateSelectedLanguage(LanguagePrototype? language) + { + if (language == null) + { + if (_selectedLanguage != null) + _selectedLanguage.Disabled = false; + + _selectedLanguage = null; + return; + } + + if (_buttonsDict.TryGetValue(language, out var button)) + { + if (_selectedLanguage != null) + _selectedLanguage.Disabled = false; + + button.Disabled = true; + _selectedLanguage = button; + } + } +} + + diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.xaml.cs index 84bce10c93c4..d59d050d7ce0 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.xaml.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.xaml.cs @@ -1,8 +1,9 @@ -using Content.Client.Stylesheets; +using Content.Client.Stylesheets; using Content.Client.SS220.UserInterface.System.Chat.Controls; using Content.Shared.Chat; using Content.Shared.Input; using Robust.Client.UserInterface.Controls; +using Content.Client.SS220.UserInterface.System.Chat.Controls.LanguageSettings; namespace Content.Client.UserInterface.Systems.Chat.Controls; @@ -13,6 +14,7 @@ public class ChatInputBox : PanelContainer public readonly HistoryLineEdit Input; public readonly ChannelFilterButton FilterButton; public readonly HighlightButton HighlightButton; //ss220 highlight words + public readonly LanguageSettingsButton LanguageSettings; // SS220 languages protected readonly BoxContainer Container; protected ChatChannel ActiveChannel { get; private set; } = ChatChannel.Local; @@ -53,8 +55,16 @@ public ChatInputBox() StyleClasses = {"chatFilterOptionButton"} }; //ss220 highlight words end + // SS220 languages begin + LanguageSettings = new LanguageSettingsButton() + { + Name = "LanguageSettings", + StyleClasses = { "chatFilterOptionButton" } + }; + // SS220 languages end Container.AddChild(FilterButton); Container.AddChild(HighlightButton); //ss220 highlight words + Container.AddChild(LanguageSettings); // SS220 languages AddStyleClass(StyleNano.StyleClassChatSubPanel); ChannelSelector.OnChannelSelect += UpdateActiveChannel; } diff --git a/Content.Server/Chat/Managers/ChatSanitizationManager.cs b/Content.Server/Chat/Managers/ChatSanitizationManager.cs index 14566c2d66f0..c5bc3a8d02af 100644 --- a/Content.Server/Chat/Managers/ChatSanitizationManager.cs +++ b/Content.Server/Chat/Managers/ChatSanitizationManager.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; +using Content.Server.SS220.Language; using Content.Shared.CCVar; using Robust.Shared.Configuration; @@ -113,6 +114,7 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly ILocalizationManager _loc = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; // SS220 languages private bool _doSanitize; @@ -175,7 +177,9 @@ public bool TrySanitizeEmoteShorthands(string message, } // SS220 no English begin - var ntAllowed = sanitized.Replace("NanoTrasen", string.Empty, StringComparison.OrdinalIgnoreCase); + var language = _entityManager.System(); + var checkMessage = language.SanitizeMessage(speaker, speaker, sanitized, out _); + var ntAllowed = checkMessage.Replace("NanoTrasen", string.Empty, StringComparison.OrdinalIgnoreCase); ntAllowed = ntAllowed.Replace("nt", string.Empty, StringComparison.OrdinalIgnoreCase); // Remember, no English diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index f7fb693b1f75..c55ecec548cf 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -42,7 +42,9 @@ using Robust.Shared.Replays; using Robust.Shared.Utility; using Robust.Shared.Timing; +using Content.Server.SS220.Language; // SS220-Add-Languages-end using Robust.Shared.Map; +using JetBrains.Annotations; namespace Content.Server.Chat.Systems; @@ -70,6 +72,7 @@ public sealed partial class ChatSystem : SharedChatSystem [Dependency] private readonly ReplacementAccentSystem _wordreplacement = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly ExamineSystemShared _examineSystem = default!; + [Dependency] private readonly LanguageSystem _languageSystem = default!; // SS220-Add-Languages [Dependency] private readonly InventorySystem _inventory = default!; //ss220 add identity concealment for chat and radio messages [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!; //ss220 add identity concealment for chat and radio messages @@ -502,39 +505,76 @@ private void SendEntitySpeak( } name = FormattedMessage.EscapeText(name); + // SS220-Add-Languages begin + Dictionary)> scrambledMsgReceiversDict = new(); + foreach (var (session, data) in GetRecipients(source, VoiceRange)) + { + if (session.AttachedEntity is not { Valid: true } playerEntity) + continue; - var wrappedMessage = Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message", - ("entityName", name), - ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), - ("fontType", speech.FontId), - ("fontSize", speech.FontSize), - ("message", FormattedMessage.EscapeText(message))); + var listener = session.AttachedEntity.Value; + var scrambledMessage = _languageSystem.SanitizeMessage(source, listener, message, out var scrambledColorlessMessage); + // SS220-Add-Languages end - SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range); + var wrappedMessage = Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message", + ("entityName", name), + ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), + ("fontType", speech.FontId), + ("fontSize", speech.FontSize), + ("message", scrambledMessage /*SS220-Add-Languages*/)); - var ev = new EntitySpokeEvent(source, message, originalMessage, null, null); - RaiseLocalEvent(source, ev, true); + //SS220-Add-Languages begin + _chatManager.ChatMessageToOne(ChatChannel.Local, scrambledMessage, wrappedMessage, source, false, session.Channel); + + if (listener == source) + { + var ev = new EntitySpokeEvent(source, scrambledMessage, originalMessage, null, null); + RaiseLocalEvent(source, ev, true); + } + else + { + if (scrambledMsgReceiversDict.TryGetValue(scrambledMessage, out var entities)) + entities.Item2.Add(listener); + else + scrambledMsgReceiversDict[scrambledMessage] = (scrambledColorlessMessage, [listener]); + } + //SS220-Add-Languages end + } + //SS220-Add-Languages begin + foreach (var (scrambledMsg, (colorlessMsg, reseivers)) in scrambledMsgReceiversDict) + { + var scrambledEv = new EntitySpokeScrambledEvent(source, reseivers, scrambledMsg, colorlessMsg, originalMessage, null, false); + RaiseLocalEvent(scrambledEv); + } + + message = _languageSystem.SanitizeMessage(source, source, message, out _); + + //SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range); + //var ev = new EntitySpokeEvent(source, message, originalMessage, null, null); + //RaiseLocalEvent(source, ev, true); + //SS220-Add-Languages end // To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc. // Also doesn't log if hideLog is true. if (!HasComp(source) || hideLog) return; + var defaultLanguageName = _languageSystem.GetSelectedLanguage(source)?.Name ?? "none"; // SS220 languages if (originalMessage == message) { if (name != Name(source)) - _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user} as {name}: {originalMessage}."); + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user} as {name}: {originalMessage}, defaultLanguage: {defaultLanguageName}."); // SS220 languages else - _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user}: {originalMessage}."); + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user}: {originalMessage}, defaultLanguage: {defaultLanguageName}."); // SS220 languages } else { if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, - $"Say from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}."); + $"Say from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}, defaultLanguage: {defaultLanguageName}."); // SS220 languages else _adminLogger.Add(LogType.Chat, LogImpact.Low, - $"Say from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}."); + $"Say from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}, defaultLanguage: {defaultLanguageName}."); // SS220 languages } } @@ -555,6 +595,11 @@ private void SendEntityWhisper( if (message.Length == 0) return; + // SS220 languages begin + var transformedMessage = message; + message = _languageSystem.SanitizeMessage(source, source, message, out _); + // SS220 languages end + var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f); // get the entity's name by visual identity (if no override provided). @@ -582,7 +627,7 @@ private void SendEntityWhisper( var wrappedUnknownMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message", ("message", FormattedMessage.EscapeText(obfuscatedMessage))); - + Dictionary)> scrambledMsgReceiversDict = new(); // SS220 languages foreach (var (session, data) in GetRecipients(source, WhisperMuffledRange)) { EntityUid listener; @@ -591,39 +636,76 @@ private void SendEntityWhisper( continue; listener = session.AttachedEntity.Value; + // SS220-Add-Languages begin + var scrambledMessage = _languageSystem.SanitizeMessage(source, listener, message, out var scrambledColorlessMessage); + var obfuscatedScrambledMessage = ObfuscateMessageReadability(scrambledColorlessMessage, 0.2f); + + wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message", + ("entityName", name), ("message", scrambledMessage)); + wrappedobfuscatedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message", + ("entityName", nameIdentity), ("message", FormattedMessage.EscapeText(obfuscatedScrambledMessage))); + wrappedUnknownMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message", + ("message", FormattedMessage.EscapeText(obfuscatedScrambledMessage))); + // SS220-Add-Languages end + if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full) continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them. - if (data.Range <= WhisperClearRange || data.Observer /* SS220 Observer-Hearing */) - _chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel); + if (data.Range <= WhisperClearRange || data.Observer) + _chatManager.ChatMessageToOne(ChatChannel.Whisper, scrambledMessage /* SS220 languages */, wrappedMessage, source, false, session.Channel); //If listener is too far, they only hear fragments of the message else if (_examineSystem.InRangeUnOccluded(source, listener, WhisperMuffledRange)) - _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.Channel); + _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedScrambledMessage /* SS220 languages */, wrappedobfuscatedMessage, source, false, session.Channel); //If listener is too far and has no line of sight, they can't identify the whisperer's identity else - _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedUnknownMessage, source, false, session.Channel); + _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedScrambledMessage /* SS220 languages */, wrappedUnknownMessage, source, false, session.Channel); + + // SS220-Add-Languages begin + if (listener == source) + { + var ev = new EntitySpokeEvent(source, scrambledMessage, originalMessage, channel, obfuscatedMessage); + RaiseLocalEvent(source, ev, true); + } + else + { + if (scrambledMsgReceiversDict.TryGetValue(scrambledMessage, out var entities)) + entities.Item2.Add(listener); + else + scrambledMsgReceiversDict[scrambledMessage] = (scrambledColorlessMessage, [listener]); + } + // SS220-Add-Languages end } _replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); - var ev = new EntitySpokeEvent(source, message, originalMessage, channel, obfuscatedMessage); - RaiseLocalEvent(source, ev, true); + // SS220 languages begin + foreach (var (scrambledMsg, (colorlessMsg, reseivers)) in scrambledMsgReceiversDict) + { + var scrambledEv = new EntitySpokeScrambledEvent(source, reseivers, scrambledMsg, colorlessMsg, originalMessage, obfuscatedMessage, channel != null); + RaiseLocalEvent(scrambledEv); + } + + //var ev = new EntitySpokeEvent(source, message, originalMessage, channel, obfuscatedMessage); + //RaiseLocalEvent(source, ev, true); + + var defaultLanguageName = _languageSystem.GetSelectedLanguage(source)?.Name ?? "none"; + // SS220 languages end if (!hideLog) if (originalMessage == message) { if (name != Name(source)) - _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user} as {name}: {originalMessage}."); + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user} as {name}: {originalMessage}, defaultLanguage: {defaultLanguageName}."); // SS220 languages else - _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}: {originalMessage}."); + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}: {originalMessage}, defaultLanguage: {defaultLanguageName}."); // SS220 languages } else { if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, - $"Whisper from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}."); + $"Whisper from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}, defaultLanguage: {defaultLanguageName}."); // SS220 languages else _adminLogger.Add(LogType.Chat, LogImpact.Low, - $"Whisper from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}."); + $"Whisper from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}, defaultLanguage: {defaultLanguageName}."); // SS220 languages } } @@ -806,6 +888,14 @@ private bool CanSendInGame(string message, IConsoleShell? shell = null, ICommonS return false; } + // SS220 languages begin + if (_languageSystem.MessageLanguagesLimit(player.AttachedEntity.Value, message, out var reason)) + { + _chatManager.DispatchServerMessage(player, reason); + return false; + } + // SS220 languages end + return !_chatManager.MessageCharacterLimit(player, message); } @@ -1184,3 +1274,26 @@ public RadioEventReceiver(EntityUid actor, EntityCoordinates playTarget) } // SS220 Silicon TTS fix end +// SS220 languages begin +public sealed class EntitySpokeScrambledEvent : EntityEventArgs +{ + public readonly EntityUid Source; + public readonly List Listeners; + public readonly string Message; + public readonly string ColorlessMessage; + public readonly string OriginalMessage; + public readonly string? ObfuscatedMessage; // not null if this was a whisper + public readonly bool IsRadio; // radio message is always a whisper + + public EntitySpokeScrambledEvent(EntityUid source, List listeners, string message, string colorlessMessage, string originalMessage, string? obfuscatedMessage, bool isRadio) + { + Source = source; + Listeners = listeners; + Message = message; + ColorlessMessage = colorlessMessage; + OriginalMessage = originalMessage; + ObfuscatedMessage = obfuscatedMessage; + IsRadio = isRadio; + } +} +// SS220 languages end diff --git a/Content.Server/EntityEffects/Effects/MakeSentient.cs b/Content.Server/EntityEffects/Effects/MakeSentient.cs index c4870438486f..3e9897c995e9 100644 --- a/Content.Server/EntityEffects/Effects/MakeSentient.cs +++ b/Content.Server/EntityEffects/Effects/MakeSentient.cs @@ -1,7 +1,9 @@ using Content.Server.Ghost.Roles.Components; using Content.Server.Speech.Components; +using Content.Server.SS220.Language; using Content.Shared.EntityEffects; using Content.Shared.Mind.Components; +using Content.Shared.SS220.Language.Components; using Robust.Shared.Prototypes; namespace Content.Server.EntityEffects.Effects; @@ -22,6 +24,13 @@ public override void Effect(EntityEffectBaseArgs args) entityManager.RemoveComponent(uid); entityManager.RemoveComponent(uid); + // SS220-Add-Languages begin + var languageComp = entityManager.EnsureComponent(uid); + var languageSystem = entityManager.System(); + + languageSystem.AddLanguage((uid, languageComp), languageSystem.GalacticLanguage, true); + // SS220-Add-Languages end + // Stops from adding a ghost role to things like people who already have a mind if (entityManager.TryGetComponent(uid, out var mindContainer) && mindContainer.HasMind) { diff --git a/Content.Server/PAI/PAISystem.cs b/Content.Server/PAI/PAISystem.cs index 0cdb0bc29a2d..7683585e8e22 100644 --- a/Content.Server/PAI/PAISystem.cs +++ b/Content.Server/PAI/PAISystem.cs @@ -9,6 +9,8 @@ using Robust.Shared.Random; using System.Text; using Robust.Shared.Player; +using Content.Server.SS220.Language; +using Content.Shared.SS220.Language.Components; // SS220-Add-Languages namespace Content.Server.PAI; @@ -19,6 +21,7 @@ public sealed class PAISystem : SharedPAISystem [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly ToggleableGhostRoleSystem _toggleableGhostRole = default!; + [Dependency] private readonly LanguageSystem _language = default!; // SS220 Languages /// /// Possible symbols that can be part of a scrambled pai's name. @@ -55,6 +58,11 @@ private void OnMindAdded(EntityUid uid, PAIComponent component, MindAddedMessage // Cause then you could remotely figure out information about the owner's equipped items. _metaData.SetEntityName(uid, val); + + // SS220-Add-Languages begin + if (TryComp(component.LastUser.Value, out var languageComp)) + _language.AddLanguagesFromSource((component.LastUser.Value, languageComp), uid); + // SS220-Add-Languages end } private void OnMindRemoved(EntityUid uid, PAIComponent component, MindRemovedMessage args) @@ -117,5 +125,12 @@ public void PAITurningOff(EntityUid uid) if (proto != null) _metaData.SetEntityName(uid, proto.Name); } + // SS220-Add-Languages begin + if (TryComp(uid, out var languageComp)) + { + _language.ClearLanguages((uid, languageComp)); + _language.AddLanguages((uid, languageComp), [_language.UniversalLanguage, _language.GalacticLanguage], true); + } + // SS220-Add-Languages end } } diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index 4b81a4c5f195..7415c8cf389f 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Radio.EntitySystems; using Robust.Shared.Network; using Robust.Shared.Player; +using Content.Server.SS220.Language; // SS220-Add-Languages namespace Content.Server.Radio.EntitySystems; @@ -21,6 +22,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnHeadsetReceive); SubscribeLocalEvent(OnKeysChanged); + SubscribeLocalEvent(OnGetLanguage); // SS220 languages SubscribeLocalEvent(OnSpeak); @@ -121,4 +123,13 @@ private void OnEmpPulse(EntityUid uid, HeadsetComponent component, ref EmpPulseE args.Disabled = true; } } + + // SS220 languages begin + private void OnGetLanguage(Entity ent, ref GetLanguageListenerEvent args) + { + var actorUid = Transform(ent).ParentUid; + if (HasComp(actorUid)) + RaiseLocalEvent(actorUid, ref args); + } + // SS220 languages end } diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index dc1a897d5002..b3b2fa91686f 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -19,6 +19,8 @@ using Content.Shared.PDA; using System.Globalization; using Content.Server.Popups; +using Content.Server.SS220.Language; +using System.Diagnostics.CodeAnalysis; // SS220-Add-Languages namespace Content.Server.Radio.EntitySystems; @@ -35,6 +37,7 @@ public sealed class RadioSystem : EntitySystem [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly LanguageSystem _languageSystem = default!; // SS220-Add-Languages // set used to prevent radio feedback loops. private readonly HashSet _messages = new(); @@ -150,6 +153,7 @@ public void SendRadioMessage(EntityUid messageSource, string message, RadioChann var sourceServerExempt = _exemptQuery.HasComp(radioSource); var radioQuery = EntityQueryEnumerator(); + var messageListenerDict = new Dictionary<(string, string), HashSet>(); // SS220 languages while (canSend && radioQuery.MoveNext(out var receiver, out var radio, out var transform)) { if (!radio.ReceiveAllChannels) @@ -174,10 +178,39 @@ public void SendRadioMessage(EntityUid messageSource, string message, RadioChann if (attemptEv.Cancelled) continue; + // SS220 languages begin + if (_languageSystem.TryGetLanguageListener(receiver, out var listener)) + { + var scrambledMessage = _languageSystem.SanitizeMessage(messageSource, listener.Value, message, out var colorlessMessage); + if (messageListenerDict.TryGetValue((scrambledMessage, colorlessMessage), out var lisneners)) + lisneners.Add(receiver); + else + messageListenerDict[(scrambledMessage, colorlessMessage)] = [receiver]; + } + else + { + RaiseLocalEvent(receiver, ref ev); + } + // send the message - RaiseLocalEvent(receiver, ref ev); + //RaiseLocalEvent(receiver, ref ev); + // SS220 languages end } + // SS220 languages begin + foreach (var ((scrambledMessage, colorlessMessage), listeners) in messageListenerDict) + { + var newChatMsg = GetMsgChatMessage(messageSource, scrambledMessage); + var newEv = new RadioReceiveEvent(message, messageSource, channel, radioSource, newChatMsg, new()); + foreach (var listener in listeners) + { + RaiseLocalEvent(listener, ref newEv); + } + + RaiseLocalEvent(new RadioSpokeEvent(messageSource, colorlessMessage, newEv.Receivers.ToArray())); + } + // SS220 languages end + // Dispatch TTS radio speech event for every receiver RaiseLocalEvent(new RadioSpokeEvent(messageSource, message, ev.Receivers.ToArray())); @@ -188,6 +221,35 @@ public void SendRadioMessage(EntityUid messageSource, string message, RadioChann _replay.RecordServerMessage(chat); _messages.Remove(message); + + // SS220 languages begin + MsgChatMessage GetMsgChatMessage(EntityUid source, string message) + { + if (GetIdCardIsBold(source)) + { + content = $"[bold]{content}[/bold]"; + message = $"[bold]{message}[/bold]"; + } + + var wrappedScrambledMessage = Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap", + ("color", channel.Color), + ("fontType", speech.FontId), + ("fontSize", speech.FontSize), + ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), + ("channel", $"\\[{channel.LocalizedName}\\]"), + ("name", formattedName), + ("message", message)); + + var scrambledChat = new ChatMessage( + ChatChannel.Radio, + message, + wrappedScrambledMessage, + NetEntity.Invalid, + null); + + return new MsgChatMessage { Message = scrambledChat }; + } + // SS220 languages end } private IdCardComponent? GetIdCard(EntityUid senderUid) diff --git a/Content.Server/SS220/Commands/LanguageCommands.cs b/Content.Server/SS220/Commands/LanguageCommands.cs new file mode 100644 index 000000000000..66beef9d64fc --- /dev/null +++ b/Content.Server/SS220/Commands/LanguageCommands.cs @@ -0,0 +1,178 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using Content.Server.Administration; +using Content.Server.SS220.Language; +using Content.Shared.Administration; +using Content.Shared.SS220.Language; +using Content.Shared.SS220.Language.Components; +using Robust.Shared.Console; + +namespace Content.Server.SS220.Commands; + +[AdminCommand(AdminFlags.Admin)] +public sealed class AddLanguageCommand : LocalizedCommands +{ + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly LanguageManager _languageManager = default!; + + public override string Command => "addlanguage"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 3) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!NetEntity.TryParse(args[0], out var netEntity)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + + if (!_entManager.TryGetEntity(netEntity, out var uid)) + { + shell.WriteError(Loc.GetString("cmd-language-invalid-entity", ("uid", args[0]))); + return; + } + + var languageId = args[1]; + if (!_languageManager.TryGetLanguageById(languageId, out _)) + { + shell.WriteError(Loc.GetString("cmd-language-proto-miss", ("id", languageId))); + return; + } + + if (!bool.TryParse(args[2], out var canSpeak)) + { + shell.WriteError(Loc.GetString("cmd-addlanguage-can-speak-not-bool")); + return; + } + + var languageComp = _entManager.EnsureComponent(uid.Value); + var languageSystem = _entManager.System(); + if (languageSystem.AddLanguage((uid.Value, languageComp), languageId, canSpeak)) + { + shell.WriteLine(Loc.GetString("cmd-addlanguage-success-add")); + } + else + { + shell.WriteLine(Loc.GetString("cmd-addlanguage-already-have")); + } + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + return args.Length switch + { + 1 => CompletionResult.FromHint("cmd-language-entity-uid"), + 2 => CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs(), Loc.GetString("cmd-language-id-list")), + 3 => CompletionResult.FromHintOptions(CompletionHelper.Booleans, Loc.GetString("cmd-addlanguage-can-speak")), + _ => CompletionResult.Empty + }; + } +} + +[AdminCommand(AdminFlags.Admin)] +public sealed class RemoveLanguageCommand : LocalizedCommands +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + public override string Command => "removelanguage"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 2) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!NetEntity.TryParse(args[0], out var netEntity)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + + if (!_entManager.TryGetEntity(netEntity, out var uid)) + { + shell.WriteError(Loc.GetString("cmd-language-invalid-entity", ("uid", args[0]))); + return; + } + + var languageId = args[1]; + if (!_entManager.TryGetComponent(uid, out var languageComp)) + { + shell.WriteError(Loc.GetString("cmd-language-comp-miss")); + return; + } + + var languageSystem = _entManager.System(); + if (languageSystem.RemoveLanguage((uid.Value, languageComp), languageId)) + { + shell.WriteLine(Loc.GetString("cmd-removelanguage-success")); + } + else + { + shell.WriteLine(Loc.GetString("cmd-removelanguage-fail")); + } + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + return args.Length switch + { + 1 => CompletionResult.FromHint("cmd-language-entity-uid"), + 2 => CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs(), Loc.GetString("cmd-language-id-list")), + _ => CompletionResult.Empty + }; + } +} + +[AdminCommand(AdminFlags.Admin)] +public sealed class ClearLanguagesCommand : LocalizedCommands +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + public override string Command => "clearlanguages"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!NetEntity.TryParse(args[0], out var netEntity)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + + if (!_entManager.TryGetEntity(netEntity, out var uid)) + { + shell.WriteError(Loc.GetString("cmd-language-invalid-entity", ("uid", args[0]))); + return; + } + + if (!_entManager.TryGetComponent(uid, out var languageComp)) + { + shell.WriteError(Loc.GetString("cmd-language-comp-miss")); + return; + } + + var languageSystem = _entManager.System(); + languageSystem.ClearLanguages((uid.Value, languageComp)); + shell.WriteLine(Loc.GetString("cmd-clearlanguages-success")); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + return args.Length switch + { + 1 => CompletionResult.FromHint("cmd-language-entity-uid"), + _ => CompletionResult.Empty + }; + } +} diff --git a/Content.Server/SS220/Language/LanguageSystem.cs b/Content.Server/SS220/Language/LanguageSystem.cs new file mode 100644 index 000000000000..9e44b8973391 --- /dev/null +++ b/Content.Server/SS220/Language/LanguageSystem.cs @@ -0,0 +1,311 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using Content.Server.GameTicking.Events; +using Content.Shared.SS220.Language.Components; +using Content.Shared.SS220.Language; +using Robust.Server.Player; +using Robust.Shared.Random; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.RegularExpressions; +using Content.Shared.SS220.Language.Systems; +using Robust.Shared.Network; +using Robust.Shared.Configuration; +using Content.Shared.SS220.CCVars; + +namespace Content.Server.SS220.Language; + +public sealed partial class LanguageSystem : SharedLanguageSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly LanguageManager _language = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + + // Cached values for one tick + private static readonly Dictionary ScrambleCache = new(); + + private static int Seed = 0; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRoundStart); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnGetLanguage); + + // UI + _net.RegisterNetMessage(OnClientSelectLanguage); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + ScrambleCache.Clear(); + } + + private void OnRoundStart(RoundStartingEvent args) + { + Seed = _random.Next(); + } + + /// + /// Initializes an entity with a language component, + /// either the first language in the LearnedLanguages list into the CurrentLanguage variable + /// + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + TrySetLanguage(ent, 0); + } + + private void OnGetLanguage(Entity ent, ref GetLanguageListenerEvent args) + { + args.Listener = ent; + args.Handled = true; + } + + #region Client + private void OnClientSelectLanguage(ClientSelectlanguageMessage msg) + { + if (!_player.TryGetSessionByChannel(msg.MsgChannel, out var player)) + return; + + var entity = player.AttachedEntity; + if (entity == null || !TryComp(entity, out var comp)) + return; + + TrySetLanguage((entity.Value, comp), msg.LanguageId); + } + #endregion + + /// + /// A method of encrypting the original message into a message created + /// from the syllables of prototypes languages + /// + public string ScrambleMessage(string message, LanguagePrototype proto) + { + var cacheKey = $"{proto.ID}:{message}"; + + // If the original message is already there earlier encrypted, + // it is taken from the cache, it is necessary for the correct display when sending in the radio, + // when the character whispers and transmits a message to the radio + if (ScrambleCache.TryGetValue(cacheKey, out var cachedValue)) + return cachedValue; + + var scrambled = proto.ScrambleMethod.ScrambleMessage(message, Seed); + + ScrambleCache[cacheKey] = scrambled; + + return scrambled; + } + + /// + /// Sanitize the by removing the language tags and scramble it (if necessary) for + /// + public string SanitizeMessage(EntityUid source, EntityUid listener, string message, out string colorlessMessage, bool setColor = true) + { + colorlessMessage = message; + var languageProto = GetSelectedLanguage(source); + if (languageProto == null) + return message; + + var languageStrings = SplitMessageByLanguages(source, message, languageProto); + var sanitizedMessage = new StringBuilder(); + var sanitizedColorlessMessage = new StringBuilder(); + for (var i = 0; i < languageStrings.Count; i++) + { + var curString = languageStrings[i]; + if (CanUnderstand(listener, curString.Item2.ID)) + { + sanitizedMessage.Append(curString.Item1); + sanitizedColorlessMessage.Append(curString.Item1); + } + else + { + var scrambledString = ScrambleMessage(message, curString.Item2); + sanitizedColorlessMessage.Append(scrambledString); + if (setColor) + { + if (i + 1 == languageStrings.Count) + scrambledString = scrambledString.Trim(); + scrambledString = SetColor(scrambledString, curString.Item2); + } + + sanitizedMessage.Append(scrambledString); + } + } + + colorlessMessage = sanitizedColorlessMessage.ToString().Trim(); + return sanitizedMessage.ToString().Trim(); + } + + /// + /// Split the message into parts by language tags. + /// will be used for the part of the message without the language tag. + /// + private List<(string, LanguagePrototype)> SplitMessageByLanguages(EntityUid source, string message, LanguagePrototype defaultLanguage) + { + var list = new List<(string, LanguagePrototype)>(); + var p = _language.KeyPrefix; + var textWithKeyPattern = $@"^{p}(.*?)\s(?={p}\w+\s)|(?<=\s){p}(.*?)\s(?={p}\w+\s)|(?<=\s){p}(.*)|^{p}(.*)"; // pizdec + + var matches = Regex.Matches(message, textWithKeyPattern); + if (matches.Count <= 0) + { + list.Add((message, defaultLanguage)); + return list; + } + + var textBeforeFirstTag = message.Substring(0, matches[0].Index); + (string, LanguagePrototype?) buffer = (string.Empty, null); + if (textBeforeFirstTag != string.Empty) + buffer = (textBeforeFirstTag, defaultLanguage); + + foreach (Match m in matches) + { + if (!TryGetLanguageFromString(m.Value, out var messageWithoutTags, out var language) || + !CanSpeak(source, language.ID)) + { + if (buffer.Item2 == null) + { + buffer = (m.Value, defaultLanguage); + } + else + { + buffer.Item1 += m.Value; + } + + continue; + } + + if (buffer.Item2 == language) + { + buffer.Item1 += messageWithoutTags; + continue; + } + else if (buffer.Item2 != null) + { + list.Add((buffer.Item1, buffer.Item2)); + } + + buffer = (messageWithoutTags, language); + } + + if (buffer.Item2 != null) + { + list.Add((buffer.Item1, buffer.Item2)); + } + + return list; + } + + /// + /// Tries to find the first language tag in the message and extracts it from the message + /// + public bool TryGetLanguageFromString(string message, + [NotNullWhen(true)] out string? messageWithoutTags, + [NotNullWhen(true)] out LanguagePrototype? language) + { + messageWithoutTags = null; + language = null; + + var keyPatern = $@"{_language.KeyPrefix}\w+\s+"; + + var m = Regex.Match(message, keyPatern); + if (m == null || !_language.TryGetLanguageByKey(m.Value.Trim(), out language)) + return false; + + messageWithoutTags = Regex.Replace(message, keyPatern, string.Empty); + return messageWithoutTags != null && language != null; + } + + /// + /// A method to get a prototype language from an entity. + /// If the entity does not have a language component, a universal language is assigned. + /// + public LanguagePrototype? GetSelectedLanguage(EntityUid uid) + { + if (!TryComp(uid, out var comp)) + { + if (_language.TryGetLanguageById(UniversalLanguage, out var universalProto)) + return universalProto; + + return null; + } + + var languageID = comp.SelectedLanguage; + if (languageID == null) + return null; + + _language.TryGetLanguageById(languageID, out var proto); + return proto; + } + + /// + /// Raises event to receive the listener entity. + /// This is done for the possibility of forwarding + /// + public bool TryGetLanguageListener(EntityUid uid, [NotNullWhen(true)] out Entity? listener) + { + var ev = new GetLanguageListenerEvent(); + RaiseLocalEvent(uid, ref ev); + listener = ev.Listener; + + return listener != null; + } + + /// + /// Sets the color of the prototype language to the message + /// + public string SetColor(string message, LanguagePrototype proto) + { + if (proto.Color == null) + return message; + + var color = proto.Color.Value.ToHex(); + message = $"[color={color}]{message}[/color]"; + return message; + } + + /// + /// Adds languages for from + /// + public void AddLanguagesFromSource(Entity ent, EntityUid target) + { + var targetComp = EnsureComp(target); + foreach (var language in ent.Comp.AvailableLanguages) + { + AddLanguage((target, targetComp), language); + } + } + + public bool MessageLanguagesLimit(EntityUid source, string message, [NotNullWhen(true)] out string? reason) + { + reason = null; + if (!HasComp(source)) + return false; + + var defaultLanguage = GetSelectedLanguage(source); + if (defaultLanguage == null) + return false; + + var languagesLimit = _config.GetCVar(CCVars220.MaxLanguagesInOneMessage); + var languagesStrings = SplitMessageByLanguages(source, message, defaultLanguage); + if (languagesStrings.Count > languagesLimit) + { + reason = Loc.GetString("language-message-languages-limit", ("limit", languagesLimit)); + return true; + } + + return false; + } +} + +[ByRefEvent] +public sealed class GetLanguageListenerEvent() : HandledEntityEventArgs +{ + public Entity? Listener = null; +} + diff --git a/Content.Server/SS220/Language/Special/AddLanguageSpecial.cs b/Content.Server/SS220/Language/Special/AddLanguageSpecial.cs new file mode 100644 index 000000000000..3ac3590fa67f --- /dev/null +++ b/Content.Server/SS220/Language/Special/AddLanguageSpecial.cs @@ -0,0 +1,24 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using Content.Server.SS220.Commands; +using Content.Shared.Roles; +using Content.Shared.SS220.Language.Components; +using JetBrains.Annotations; + +namespace Content.Server.SS220.Language.Special; +/// +/// Type for giving language to individual job roles +/// +[UsedImplicitly] +public sealed partial class AddLanguageSpecial : JobSpecial +{ + [DataField] + public List Languages { get; private set; } = new(); + + public override void AfterEquip(EntityUid uid) + { + var entMan = IoCManager.Resolve(); + var languageSystem = entMan.System(); + var languageComp = entMan.EnsureComponent(uid); + languageSystem.AddLanguages((uid, languageComp), Languages, true); + } +} diff --git a/Content.Server/SS220/TTS/TTSSystem.cs b/Content.Server/SS220/TTS/TTSSystem.cs index 61034fa4ac30..b8a5546fbd72 100644 --- a/Content.Server/SS220/TTS/TTSSystem.cs +++ b/Content.Server/SS220/TTS/TTSSystem.cs @@ -14,6 +14,8 @@ using Robust.Shared.Random; using Robust.Shared.Network; using Robust.Server.Player; +using Robust.Shared.GameObjects; +using System.Linq; namespace Content.Server.SS220.TTS; @@ -46,6 +48,7 @@ public override void Initialize() SubscribeLocalEvent(OnTransformSpeech); SubscribeLocalEvent(OnEntitySpoke); + SubscribeLocalEvent(OnEntitySpokeScrambled); SubscribeLocalEvent(OnRadioReceiveEvent); SubscribeLocalEvent(OnAnnouncementSpoke); SubscribeLocalEvent(OnRoundRestartCleanup); @@ -190,13 +193,29 @@ private async void OnRequestGlobalTTS(RequestGlobalTTSEvent ev, EntitySessionEve private async void OnEntitySpoke(EntityUid uid, TTSComponent component, EntitySpokeEvent args) { - var voiceId = component.VoicePrototypeId; + HandleEntitySpoke(uid, uid, args.Message, args.IsRadio, args.ObfuscatedMessage); + } + + private async void OnEntitySpokeScrambled(EntitySpokeScrambledEvent args) + { + HandleEntitySpoke(args.Source, args.Listeners, args.ColorlessMessage, args.IsRadio, args.ObfuscatedMessage); + } + + private async void HandleEntitySpoke(EntityUid source, EntityUid listener, string message, bool isRadio, string? obfuscatedMessage = null) + { + HandleEntitySpoke(source, [listener], message, isRadio, obfuscatedMessage); + } + + private async void HandleEntitySpoke(EntityUid source, IEnumerable receivers, string message, bool isRadio, string? obfuscatedMessage = null) + { if (!_isEnabled || - args.Message.Length > MaxMessageChars || - voiceId == null) + message.Length > MaxMessageChars || + !TryComp(source, out var component) || + component.VoicePrototypeId == null) return; - if (TryGetVoiceMaskUid(uid, out var maskUid)) + var voiceId = component.VoicePrototypeId; + if (TryGetVoiceMaskUid(source, out var maskUid)) { var voiceEv = new TransformSpeakerVoiceEvent(maskUid.Value, voiceId); RaiseLocalEvent(maskUid.Value, voiceEv); @@ -208,75 +227,139 @@ private async void OnEntitySpoke(EntityUid uid, TTSComponent component, EntitySp return; } - if (args.ObfuscatedMessage != null) + if (obfuscatedMessage != null) { - HandleWhisper(uid, args.Message, args.ObfuscatedMessage, protoVoice.Speaker, args.IsRadio); + HandleWhisperToMany(source, receivers, message, obfuscatedMessage, protoVoice.Speaker, isRadio); return; } - HandleSay(uid, args.Message, protoVoice.Speaker); + HandleSayToMany(source, receivers, message, protoVoice.Speaker); + } + + private async void HandleSayToMany(EntityUid source, string message, string speaker) + { + var receivers = Filter.Pvs(source).Recipients; + HandleSayToMany(source, receivers, message, speaker); + } + + private async void HandleSayToMany(EntityUid source, IEnumerable entities, string message, string speaker) + { + List receivers = new(); + foreach (var entity in entities) + { + if (_playerManager.TryGetSessionByEntity(entity, out var receiver) && receiver != null) + receivers.Add(receiver); + } + + HandleSayToMany(source, receivers, message, speaker); } - private async void HandleSay(EntityUid uid, string message, string speaker) + private async void HandleSayToMany(EntityUid source, IEnumerable receivers, string message, string speaker) { using var ttsResponse = await GenerateTts(message, speaker, TtsKind.Default); if (!ttsResponse.TryGetValue(out var audioData)) return; var playTtsMessage = new MsgPlayTts { Data = audioData, - SourceUid = GetNetEntity(uid), + SourceUid = GetNetEntity(source), }; - foreach (var receiver in Filter.Pvs(uid).Recipients) + foreach (var receiver in receivers) { - _netManager.ServerSendMessage(playTtsMessage, receiver.Channel); + HandleSayToOne(source, receiver, message, speaker, playTtsMessage); } } - private async void HandleWhisper(EntityUid uid, string message, string obfMessage, string speaker, bool isRadio) + private async void HandleSayToOne(EntityUid source, EntityUid target, string message, string speaker, MsgPlayTts? msgPlayTts = null) { - // If it's a whisper into a radio, generate speech without whisper - // attributes to prevent an additional speech synthesis event - using var ttsResponse = await GenerateTts(message, speaker, TtsKind.Whisper); - if (!ttsResponse.TryGetValue(out var audioData)) - return; - - using var obfTtsResponse = await GenerateTts(obfMessage, speaker, TtsKind.Whisper); - if (!obfTtsResponse.TryGetValue(out var obfAudioData)) + if (!_playerManager.TryGetSessionByEntity(target, out var receiver)) return; - // TODO: Check obstacles - var xformQuery = GetEntityQuery(); - var sourcePos = _xforms.GetWorldPosition(xformQuery.GetComponent(uid), xformQuery); - var receptions = Filter.Pvs(uid).Recipients; + HandleSayToOne(source, receiver, message, speaker, msgPlayTts); + } - var fullTtsMessage = new MsgPlayTts + private async void HandleSayToOne(EntityUid source, ICommonSession receiver, string message, string speaker, MsgPlayTts? msgPlayTts = null) + { + if (msgPlayTts == null) { - Data = audioData, - SourceUid = GetNetEntity(uid), - Kind = TtsKind.Whisper, - }; + using var ttsResponse = await GenerateTts(message, speaker, TtsKind.Default); + if (!ttsResponse.TryGetValue(out var audioData)) return; + msgPlayTts = new MsgPlayTts + { + Data = audioData, + SourceUid = GetNetEntity(source), + }; + } - var obfuscatedTtsMessage = new MsgPlayTts + _netManager.ServerSendMessage(msgPlayTts, receiver.Channel); + } + + private async void HandleWhisperToMany(EntityUid source, IEnumerable entities, string message, string obfMessage, string speaker, bool isRadio) + { + List receivers = new(); + foreach (var entity in entities) { - Data = obfAudioData, - SourceUid = GetNetEntity(uid), - Kind = TtsKind.Whisper, - }; + if (_playerManager.TryGetSessionByEntity(entity, out var receiver) && receiver != null) + receivers.Add(receiver); + } + + HandleWhisperToMany(source, receivers, message, obfMessage, speaker, isRadio); + } - foreach (var session in receptions) + private async void HandleWhisperToMany(EntityUid source, IEnumerable receivers, string message, string obfMessage, string speaker, bool isRadio) + { + var ttsMessage = await GetMsgPlayTts(source, message, speaker, TtsKind.Whisper); + var obfTtsMessage = await GetMsgPlayTts(source, obfMessage, speaker, TtsKind.Whisper); + if (ttsMessage == null || obfTtsMessage == null) + return; + + foreach (var receiver in receivers) { - if (!session.AttachedEntity.HasValue) - continue; + HandleWhisperToOne(source, receiver, message, obfMessage, speaker, isRadio, ttsMessage, obfTtsMessage); + } + } - var xform = xformQuery.GetComponent(session.AttachedEntity.Value); - var distance = (sourcePos - _xforms.GetWorldPosition(xform, xformQuery)).Length(); + private async void HandleWhisperToOne(EntityUid uid, EntityUid target, string message, string obfMessage, string speaker, bool isRadio) + { + if (!_playerManager.TryGetSessionByEntity(target, out var receiver)) + return; - if (distance > ChatSystem.WhisperMuffledRange) - continue; + HandleWhisperToOne(uid, receiver, message, obfMessage, speaker, isRadio); + } - var netMessageToSend = distance > ChatSystem.WhisperClearRange ? obfuscatedTtsMessage : fullTtsMessage; + private async void HandleWhisperToOne(EntityUid source, + ICommonSession receiver, + string message, + string obfMessage, + string speaker, + bool isRadio, + MsgPlayTts? ttsMessage = null, + MsgPlayTts? obfTtsMessage = null) + { + if (!receiver.AttachedEntity.HasValue) + return; - _netManager.ServerSendMessage(netMessageToSend, session.Channel); + var xformQuery = GetEntityQuery(); + var sourcePos = _xforms.GetWorldPosition(xformQuery.GetComponent(source), xformQuery); + + var xform = xformQuery.GetComponent(receiver.AttachedEntity.Value); + var distance = (sourcePos - _xforms.GetWorldPosition(xform, xformQuery)).Length(); + + if (distance > ChatSystem.WhisperMuffledRange) + return; + + if (distance > ChatSystem.WhisperClearRange) + { + ttsMessage ??= await GetMsgPlayTts(source, message, speaker, TtsKind.Whisper); + if (ttsMessage == null) return; + + _netManager.ServerSendMessage(ttsMessage, receiver.Channel); + } + else + { + obfTtsMessage ??= await GetMsgPlayTts(source, obfMessage, speaker, TtsKind.Whisper); + if (obfTtsMessage == null) return; + + _netManager.ServerSendMessage(obfTtsMessage, receiver.Channel); } } @@ -329,6 +412,20 @@ private async void HandleRadio(RadioEventReceiver[] receivers, string message, s return default; } + + private async Task GetMsgPlayTts(EntityUid source, string message, string speaker, TtsKind kind = TtsKind.Default) + { + using var ttsResponse = await GenerateTts(message, speaker, TtsKind.Whisper); + if (!ttsResponse.TryGetValue(out var obfAudioData)) + return null; + + return new MsgPlayTts + { + Data = obfAudioData, + SourceUid = GetNetEntity(source), + Kind = TtsKind.Whisper, + }; + } } public sealed class TransformSpeakerVoiceEvent : EntityEventArgs diff --git a/Content.Server/Traits/TraitSystem.cs b/Content.Server/Traits/TraitSystem.cs index 38cbc1859cb6..de48b7239216 100644 --- a/Content.Server/Traits/TraitSystem.cs +++ b/Content.Server/Traits/TraitSystem.cs @@ -5,6 +5,8 @@ using Content.Shared.Traits; using Content.Shared.Whitelist; using Robust.Shared.Prototypes; +using Content.Server.SS220.Language; +using Content.Shared.SS220.Language.Components; namespace Content.Server.Traits; @@ -13,6 +15,7 @@ public sealed class TraitSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly LanguageSystem _language = default!; public override void Initialize() { @@ -45,7 +48,19 @@ private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args) continue; // Add all components required by the prototype - EntityManager.AddComponents(args.Mob, traitPrototype.Components, false); + // SS220 fix null components begin + if (traitPrototype.Components is { } components) + EntityManager.AddComponents(args.Mob, components, false); + + // EntityManager.AddComponents(args.Mob, traitPrototype.Components, false); + // SS220 fix null components end + // SS220-Add-Languages begin + if (traitPrototype.LearnedLanguages.Count > 0) + { + var language = EnsureComp(args.Mob); + _language.AddLanguages((args.Mob, language), traitPrototype.LearnedLanguages); + } + // SS220-Add-Languages end // Add item required by the trait if (traitPrototype.TraitGear == null) diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index 2a88e9df41b9..4fd220d1b503 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -40,6 +40,8 @@ using Content.Shared.Humanoid.Markings; using Robust.Server.Player; using Content.Shared.Ghost.Roles.Components; +using Content.Server.SS220.Language; +using Content.Shared.SS220.Language.Components; namespace Content.Server.Zombies; @@ -111,6 +113,7 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) RemComp(target); RemComp(target); RemComp(target); + RemComp(target); // SS220-Add-Languages //funny voice var accentType = "zombie"; diff --git a/Content.Shared/Entry/EntryPoint.cs b/Content.Shared/Entry/EntryPoint.cs index df267b08cb11..761558dd737a 100644 --- a/Content.Shared/Entry/EntryPoint.cs +++ b/Content.Shared/Entry/EntryPoint.cs @@ -4,6 +4,7 @@ using Content.Shared.Humanoid.Markings; using Content.Shared.IoC; using Content.Shared.Maps; +using Content.Shared.SS220.Language; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; @@ -46,6 +47,7 @@ public override void PostInit() InitTileDefinitions(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); // SS220 languages #if DEBUG var configMan = IoCManager.Resolve(); diff --git a/Content.Shared/IoC/SharedContentIoC.cs b/Content.Shared/IoC/SharedContentIoC.cs index c20cdbc111e3..2bf0267df082 100644 --- a/Content.Shared/IoC/SharedContentIoC.cs +++ b/Content.Shared/IoC/SharedContentIoC.cs @@ -1,5 +1,6 @@ -using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Markings; using Content.Shared.Localizations; +using Content.Shared.SS220.Language; namespace Content.Shared.IoC { @@ -9,6 +10,7 @@ public static void Register() { IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); // SS220 languages } } } diff --git a/Content.Shared/SS220/CCVars/CCVars220.cs b/Content.Shared/SS220/CCVars/CCVars220.cs index 3f5db5566325..e5aadc70ae02 100644 --- a/Content.Shared/SS220/CCVars/CCVars220.cs +++ b/Content.Shared/SS220/CCVars/CCVars220.cs @@ -124,4 +124,10 @@ public sealed partial class CCVars220 /// public static readonly CVarDef MaxSponsorsBypass = CVarDef.Create("game.max_sponsors_bypass", 10, CVar.SERVER); + + /// + /// How many languages can be used in one message + /// + public static readonly CVarDef MaxLanguagesInOneMessage = + CVarDef.Create("chat.max_languages_in_one_message", 3, CVar.SERVER); } diff --git a/Content.Shared/SS220/Language/Components/LanguageComponent.cs b/Content.Shared/SS220/Language/Components/LanguageComponent.cs new file mode 100644 index 000000000000..5f25c3868a97 --- /dev/null +++ b/Content.Shared/SS220/Language/Components/LanguageComponent.cs @@ -0,0 +1,47 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using Content.Shared.SS220.Language.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.SS220.Language.Components; + +/// +/// A component that allows an entity to speak and understand languages. +/// Language prototypes are taken from YML of +/// The absence of this component gives the entity “Universal” language +/// +[RegisterComponent, Access(typeof(SharedLanguageSystem), Other = AccessPermissions.Read)] +[NetworkedComponent, AutoGenerateComponentState] +public sealed partial class LanguageComponent : Component +{ + /// + /// Selected language in which the entity will speak. + /// If null, the universal language will be used. + /// + [DataField, AutoNetworkedField] + public ProtoId? SelectedLanguage; + + /// + /// List of languages that the Entity can speak. + /// + [DataField, AutoNetworkedField] + public List AvailableLanguages = new(); +} + +[DataDefinition] +[Serializable, NetSerializable] +public sealed partial class LanguageDefinition +{ + [DataField(required: true)] + public ProtoId Id; + + [DataField] + public bool CanSpeak = true; + + public LanguageDefinition(ProtoId id, bool canSpeak) + { + Id = id; + CanSpeak = canSpeak; + } +} diff --git a/Content.Shared/SS220/Language/EncryptionMethods/BaseEncryptionMethod.cs b/Content.Shared/SS220/Language/EncryptionMethods/BaseEncryptionMethod.cs new file mode 100644 index 000000000000..89a2c6b09765 --- /dev/null +++ b/Content.Shared/SS220/Language/EncryptionMethods/BaseEncryptionMethod.cs @@ -0,0 +1,13 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt + +namespace Content.Shared.SS220.Language.EncryptionMethods; + +[ImplicitDataDefinitionForInheritors] +public abstract partial class ScrambleMethod +{ + /// + /// Scramble according to a specific algorithm. + /// It is acceptable to use a specific seed to get the same result if the message is the same + /// + public abstract string ScrambleMessage(string message, int? seed = null); +} diff --git a/Content.Shared/SS220/Language/EncryptionMethods/EmptyScrambleMethod.cs b/Content.Shared/SS220/Language/EncryptionMethods/EmptyScrambleMethod.cs new file mode 100644 index 000000000000..a839c68f4f36 --- /dev/null +++ b/Content.Shared/SS220/Language/EncryptionMethods/EmptyScrambleMethod.cs @@ -0,0 +1,15 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt + +namespace Content.Shared.SS220.Language.EncryptionMethods; + +/// +/// Returns the original message without scramble. +/// Used in the universal language +/// +public sealed partial class EmptyScrambleMethod : ScrambleMethod +{ + public override string ScrambleMessage(string message, int? seed = null) + { + return message; + } +} diff --git a/Content.Shared/SS220/Language/EncryptionMethods/SyllablesScrambleMethod.cs b/Content.Shared/SS220/Language/EncryptionMethods/SyllablesScrambleMethod.cs new file mode 100644 index 000000000000..efd03bc674ea --- /dev/null +++ b/Content.Shared/SS220/Language/EncryptionMethods/SyllablesScrambleMethod.cs @@ -0,0 +1,151 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Content.Shared.Random.Helpers; +using Robust.Shared.Random; + +namespace Content.Shared.SS220.Language.EncryptionMethods; + +/// +/// Scramble a message depending on its length using a specific list of syllables +/// +public sealed partial class SyllablesScrambleMethod : ScrambleMethod +{ + /// + /// List of syllables from which the original message will be encrypted + /// A null value does not scramlbe the message in any way + /// + [DataField(required: true)] + public List Syllables = new(); + + /// + /// Chance of the after a scrambled syllable + /// + [DataField] + public float SpecialCharacterChance = 0.5f; + + /// + /// Coefficient of how much the length of the scrambled message will differ from the original. + /// The shorter the syllables length, the higher the accuracy. + /// + [DataField] + public float ScrambledLengthCoefficient = 1f; + + /// + /// Special characters that can be inserted after a scrambled syllable + /// + [DataField] + public List SpecialCharacters = new(); + + private int _inputSeed; + private bool _capitalize = false; + + public override string ScrambleMessage(string message, int? seed = null) + { + if (message == string.Empty || + Syllables.Count == 0) + return message; + + var wordRegex = @"\S+"; + var matches = Regex.Matches(message, wordRegex); + if (matches.Count <= 0) + return message; + + var random = IoCManager.Resolve(); + _inputSeed = seed ?? random.Next(); + _capitalize = char.IsUpper(message[0]); + + var result = new StringBuilder(); + foreach (Match m in matches) + { + seed = _inputSeed; + var word = m.Value.ToLower(); + foreach (var c in word.ToCharArray()) + { + seed += c; + } + + var scrambledWord = ScrambleWord(m.Value, seed.Value); + result.Append(scrambledWord); + } + + var punctuation = ExtractPunctuation(message); + result.Append(punctuation); + + _capitalize = false; + return result.ToString(); + } + + private string ScrambleWord(string word, int seed) + { + var random = new System.Random(seed); + var scrambledMessage = new StringBuilder(); + var scrambledLength = word.Length * ScrambledLengthCoefficient; + while (scrambledMessage.Length < scrambledLength) + { + var curSyllable = random.Pick(Syllables); + + if (_capitalize) + { + curSyllable = string.Concat(curSyllable.Substring(0, 1).ToUpper(), curSyllable.AsSpan(1)); + _capitalize = false; + } + scrambledMessage.Append(curSyllable); + + if (random.Prob(SpecialCharacterChance)) + { + var character = GetSpecialCharacter(random); + if (character != null) + { + scrambledMessage.Append(character.Character); + _capitalize = character.Capitalize; + } + } + } + + var result = scrambledMessage.ToString(); + return result; + } + + /// + /// Takes the last punctuation out of the original post + /// (Does not affect the internal punctuation of the sentence) + /// + private static string ExtractPunctuation(string input) + { + var punctuationBuilder = new StringBuilder(); + for (var i = input.Length - 1; i >= 0; i--) + { + if (char.IsPunctuation(input[i])) + punctuationBuilder.Insert(0, input[i]); + else + break; + } + punctuationBuilder.Append(' '); // save whitespace before language tag + + return punctuationBuilder.ToString(); + } + + private SyllablesSpecialCharacter? GetSpecialCharacter(System.Random random) + { + var weights = SpecialCharacters.ToDictionary(s => s, s => s.Weight); + if (weights == null || weights.Count <= 0) + return null; + + return SharedRandomExtensions.Pick(weights, random); + } +} + +[DataDefinition] +public sealed partial class SyllablesSpecialCharacter +{ + [DataField(required: true)] + public string Character; + + [DataField] + public float Weight = 1f; + + [DataField] + public bool Capitalize = false; +} diff --git a/Content.Shared/SS220/Language/LanguageManager.cs b/Content.Shared/SS220/Language/LanguageManager.cs new file mode 100644 index 000000000000..bab2f99e4ed7 --- /dev/null +++ b/Content.Shared/SS220/Language/LanguageManager.cs @@ -0,0 +1,39 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Shared.SS220.Language; + +public sealed class LanguageManager +{ + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly INetManager _net = default!; + + public List Languages { get; private set; } = new(); + + public readonly string KeyPrefix = "%"; + + public void Initialize() + { + Languages = [.. _prototype.EnumeratePrototypes()]; + } + + /// + /// Tries get language prototipe by id + /// + public bool TryGetLanguageById(string id, [NotNullWhen(true)] out LanguagePrototype? language) + { + language = Languages.Find(l => l.ID == id); + return language != null; + } + + /// + /// Tries get language prototipe by language key + /// + public bool TryGetLanguageByKey(string key, [NotNullWhen(true)] out LanguagePrototype? language) + { + language = Languages.Find(l => KeyPrefix + l.Key == key); + return language != null; + } +} diff --git a/Content.Shared/SS220/Language/LanguageNetMessages.cs b/Content.Shared/SS220/Language/LanguageNetMessages.cs new file mode 100644 index 000000000000..5bbebee9ebc2 --- /dev/null +++ b/Content.Shared/SS220/Language/LanguageNetMessages.cs @@ -0,0 +1,24 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using Lidgren.Network; +using Robust.Shared.Network; +using Robust.Shared.Serialization; + +namespace Content.Shared.SS220.Language; + +public sealed class ClientSelectlanguageMessage : NetMessage +{ + public string LanguageId = string.Empty; + + public override MsgGroups MsgGroup => MsgGroups.Entity; + + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) + { + LanguageId = buffer.ReadString(); + } + + public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) + { + buffer.Write(LanguageId); + } +} + diff --git a/Content.Shared/SS220/Language/LanguagesPrototype.cs b/Content.Shared/SS220/Language/LanguagesPrototype.cs new file mode 100644 index 000000000000..e700c8b39039 --- /dev/null +++ b/Content.Shared/SS220/Language/LanguagesPrototype.cs @@ -0,0 +1,34 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using Content.Shared.SS220.Language.EncryptionMethods; +using Robust.Shared.Prototypes; + +namespace Content.Shared.SS220.Language; + +[Prototype("language")] +public sealed partial class LanguagePrototype : IPrototype +{ + [ViewVariables, IdDataField] + public string ID { get; private set; } = default!; + + [DataField] + public string Name = string.Empty; + + [DataField] + public string Description = string.Empty; + + [DataField(required: true)] + public string Key = string.Empty; + + /// + /// The color of the language in which messages will be recolored, + /// an empty value will not be recolored + /// + [DataField] + public Color? Color; + + /// + /// The method used to scramble the message + /// + [DataField] + public ScrambleMethod ScrambleMethod = new SyllablesScrambleMethod(); +} diff --git a/Content.Shared/SS220/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/SS220/Language/Systems/SharedLanguageSystem.cs new file mode 100644 index 000000000000..172f19549401 --- /dev/null +++ b/Content.Shared/SS220/Language/Systems/SharedLanguageSystem.cs @@ -0,0 +1,175 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt +using Content.Shared.Ghost; +using Content.Shared.SS220.Language.Components; + +namespace Content.Shared.SS220.Language.Systems; + +public abstract class SharedLanguageSystem : EntitySystem +{ + [Dependency] private readonly LanguageManager _language = default!; + + public readonly string UniversalLanguage = "Universal"; + public readonly string GalacticLanguage = "Galactic"; + + #region Component + /// + /// Adds languages to from . + /// + /// Will entity be able to speak this language + public void AddLanguages(Entity ent, IEnumerable languageIds, bool canSpeak = false) + { + foreach (var language in languageIds) + { + AddLanguage(ent, language, canSpeak); + } + } + + /// + /// Adds a from list to + /// + public void AddLanguages(Entity ent, List definitions) + { + foreach (var def in definitions) + { + AddLanguage(ent, def); + } + } + + /// + /// Adds language to the + /// + /// Will entity be able to speak this language + /// if successful added + public bool AddLanguage(Entity ent, string languageId, bool canSpeak = false) + { + if (GetDefinition(ent, languageId) != null || + !_language.TryGetLanguageById(languageId, out _)) + return false; + + var newDef = new LanguageDefinition(languageId, canSpeak); + AddLanguage(ent, newDef); + return true; + } + + /// + /// Adds a to the + /// + public void AddLanguage(Entity ent, LanguageDefinition definition) + { + ent.Comp.AvailableLanguages.Add(definition); + ent.Comp.SelectedLanguage ??= definition.Id; + Dirty(ent); + } + + /// + /// Clears + /// + public void ClearLanguages(Entity ent) + { + ent.Comp.AvailableLanguages.Clear(); + ent.Comp.SelectedLanguage = null; + Dirty(ent); + } + + /// + /// Removes language from + /// + /// if successful removed + public bool RemoveLanguage(Entity ent, string languageId) + { + var def = GetDefinition(ent, languageId); + if (def == null) + return false; + + if (ent.Comp.AvailableLanguages.Remove(def)) + { + if (ent.Comp.SelectedLanguage == languageId && !TrySetLanguage(ent, 0)) + ent.Comp.SelectedLanguage = null; + + Dirty(ent); + return true; + } + + return false; + } + + /// + /// Does the contain this language. + /// + public bool ContainsLanguage(Entity ent, string languageId, bool withCanSpeak = false) + { + var def = GetDefinition(ent, languageId); + if (def == null || + (withCanSpeak && !def.CanSpeak)) + return false; + + return true; + } + + public static LanguageDefinition? GetDefinition(Entity ent, string languageId) + { + return ent.Comp.AvailableLanguages.Find(l => l.Id == languageId); + } + + /// + /// Tries set by of + /// + public bool TrySetLanguage(Entity ent, int index) + { + if (ent.Comp.AvailableLanguages.Count <= index) + return false; + + ent.Comp.SelectedLanguage = ent.Comp.AvailableLanguages[index].Id; + Dirty(ent); + return true; + } + + /// + /// Tries set by language id. + /// Doesn't set language if doesn't contain this + /// + public bool TrySetLanguage(Entity ent, string languageId) + { + if (!CanSpeak(ent, languageId)) + return false; + + ent.Comp.SelectedLanguage = languageId; + Dirty(ent); + return true; + } + #endregion + + /// + /// Checks whether the entity can speak this language. + /// + public bool CanSpeak(EntityUid uid, string languageId) + { + if (!TryComp(uid, out var comp)) + return false; + + return ContainsLanguage((uid, comp), languageId, true); + } + + /// + /// Checks whether the entity understands this language. + /// + public bool CanUnderstand(EntityUid uid, string languageId) + { + if (KnowsAllLanguages(uid) || + languageId == UniversalLanguage) + return true; + + if (!TryComp(uid, out var comp)) + return false; + + return ContainsLanguage((uid, comp), languageId); + } + + /// + /// Checks whether the entity knows all languages. + /// + public bool KnowsAllLanguages(EntityUid uid) + { + return HasComp(uid); + } +} diff --git a/Content.Shared/Traits/TraitPrototype.cs b/Content.Shared/Traits/TraitPrototype.cs index c79d3cbf308e..679ed6f6b47e 100644 --- a/Content.Shared/Traits/TraitPrototype.cs +++ b/Content.Shared/Traits/TraitPrototype.cs @@ -1,3 +1,4 @@ +using Content.Shared.SS220.Language.Components; using Content.Shared.Whitelist; using Robust.Shared.Prototypes; @@ -41,7 +42,7 @@ public sealed partial class TraitPrototype : IPrototype /// The components that get added to the player, when they pick this trait. /// [DataField] - public ComponentRegistry Components { get; private set; } = default!; + public ComponentRegistry? Components { get; private set; } = default!; // SS220 Add nullable /// /// Gear that is given to the player, when they pick this trait. @@ -60,4 +61,7 @@ public sealed partial class TraitPrototype : IPrototype /// [DataField] public ProtoId? Category; + + [DataField] + public List LearnedLanguages = new(); // SS220-Add-Languages } diff --git a/Resources/Locale/ru-RU/ss220/language/language-cmd.ftl b/Resources/Locale/ru-RU/ss220/language/language-cmd.ftl new file mode 100644 index 000000000000..83c3f5cbfd28 --- /dev/null +++ b/Resources/Locale/ru-RU/ss220/language/language-cmd.ftl @@ -0,0 +1,25 @@ +cmd-language-invalid-arguments = Неверное количество аргументов +cmd-language-proto-miss = Не найден прототип языка с id: { $id } +cmd-language-invalid-entity = Не найден энтити с id: { $uid } +cmd-language-entity-uid = [id энтити] +cmd-language-id-list = [id языка] +cmd-language-comp-miss = У энтити отсутсвует LanguageComponent + +# Add +cmd-addlanguage-desc = Добавляет язык в LanguageComponent энтити +cmd-addlanguage-help = Использование: addlanguage +cmd-addlanguage-can-speak = [Может говорить] +cmd-addlanguage-can-speak-not-bool = должно быть true или false +cmd-addlanguage-success-add = Язык успешно добавлен +cmd-addlanguage-already-have = Энтити уже знает этот язык + +# Remove +cmd-removelanguage-desc = Удаляет язык из LanguageComponent энтити +cmd-removelanguage-help = Использование: removelanguage +cmd-removelanguage-success = Язык удален +cmd-removelanguage-fail = Энтити не обладает таким языком + +# Clear +cmd-clearlanguages-desc = Удаляет все языки из LanguageComponent энтити +cmd-clearlanguages-help = Использование: clearlanguages +cmd-clearlanguages-success = Все языки энтити - удалены diff --git a/Resources/Locale/ru-RU/ss220/language/language-desc.ftl b/Resources/Locale/ru-RU/ss220/language/language-desc.ftl new file mode 100644 index 000000000000..e1ca69e403d8 --- /dev/null +++ b/Resources/Locale/ru-RU/ss220/language/language-desc.ftl @@ -0,0 +1,22 @@ +# Sources: https://wiki.ss220.club/index.php/Языки and https://wiki14.ss220.club/index.php?title=Лор + +# Galactic common languages +language-galacticcommon-desc = Дружелюбный для иных рас язык, введёный правительством ТСФ в связи с наслывом в сектор иммигрантов представляющие многочисленное количества рас. +language-solcommon-desc = Родной язык людей. Убийственный гибрид неформального английского и элементов мандаринского китайского, общий язык Солнечной системы. +language-tradeband-desc = Это тщательно продуманная манера говорить с использованием множества слов из многих инопланетных языков, а также из ряда человеческих. +language-gutter-desc = Используется в основном только гражданами НЕИЗВЕСТНОЙ забытой республики. Современный является продуктом смешения латинских языков, французского и итальянского. +language-clownish-desc = Язык клоунской планеты. Родной язык клоунов по всей галактике. +language-neorusskiya-desc = Смесь общесолнечного и славянских языков. Официальный язык СССП. +# Languages of race +language-unathi-desc = Язык с множеством разных акцентов и диалектов, и унати может почти сразу сказать, откуда произошел другой, из-за их разрозненного, сгруппированного и широко распространенного обитания в Могесе. +language-siiktajr-desc = Один из многих языков, на которых говорят таяраны Адомая, — это примитивный язык, который считается наиболее часто встречающимся у представителей этого вида. +language-voxpidgin-desc = Общий язык различных воксов, составляющих Шоал. Для всех прочих звучит как хаотичные вопли. +language-rootspeak-desc = Скрипучий субголосый язык, на котором инстинктивно говорят дионы. Из-за уникального состава средней дионы фразы песни корней могут представлять собой комбинацию от одного до двенадцати отдельных голосов и нот. +language-bobblish-desc = Произносится с помощью определенной комбинации хлопающих звуков, легко понимаемых другими слаймами. +language-tkachi-desc = В основе языка лежат жужжащие звуки. Для построения речи используется ритмичность, длительность издаваемых звуков, интервал, интонация. +language-eldwarf-desc = Грубый язык с малым количеством гласных. Традиционно является языком, на котором дворфы разговаривают исключительно в окружении других цвергов, как метод скрытия от гнета республики. +language-arati-desc = Достаточно грубый язык, в котором большое количество шипящих звуков. +language-canilunzt-desc = Гортанный язык, на котором говорят и используют жители системы Ваззенда, состоит из рычания, лая, тявканья и интенсивного использования ушей и движений хвоста. +# Misc +language-binary-desc = Свободные протоколы связи и концентраторы маршрутизации для использования синтетиками. Большинство человеческих станций их поддерживают. +language-codespeak-desc = Кодовый язык, используемый агентами и ядерными оперативниками синдиката. Структурно представляет из себя сложнейший шифр, который для внешнего слушателя звучик как случайный набор напитков. diff --git a/Resources/Locale/ru-RU/ss220/language/language-name.ftl b/Resources/Locale/ru-RU/ss220/language/language-name.ftl new file mode 100644 index 000000000000..f5e4956d416c --- /dev/null +++ b/Resources/Locale/ru-RU/ss220/language/language-name.ftl @@ -0,0 +1,20 @@ +# Galactic common languages +language-galacticcommon-name = Общегалактический +language-solcommon-name = Общесолнечный +language-tradeband-name = Торговый +language-gutter-name = Гуттэр +language-clownish-name = Клоунский +language-neorusskiya-name = Нео-Русский +# Languages of race +language-unathi-name = Синта’Унати +language-siiktajr-name = Сик’таир +language-voxpidgin-name = Вокс-пиджин +language-rootspeak-name = Песнь корней +language-bobblish-name = Пузырчатый +language-tkachi-name = Ткачий +language-eldwarf-name = Эл’Дварф +language-arati-name = Арати +language-canilunzt-name = Канилунц +# Misc +language-binary-name = Бинарный канал +language-codespeak-name = Кодспик diff --git a/Resources/Locale/ru-RU/ss220/language/language-system.ftl b/Resources/Locale/ru-RU/ss220/language/language-system.ftl new file mode 100644 index 000000000000..c1c5ed75c03a --- /dev/null +++ b/Resources/Locale/ru-RU/ss220/language/language-system.ftl @@ -0,0 +1 @@ +language-message-languages-limit = Допускается использование не более { $limit } языков в одном сообщении diff --git a/Resources/Locale/ru-RU/ss220/language/language-trait-category.ftl b/Resources/Locale/ru-RU/ss220/language/language-trait-category.ftl new file mode 100644 index 000000000000..6ffcdc16e017 --- /dev/null +++ b/Resources/Locale/ru-RU/ss220/language/language-trait-category.ftl @@ -0,0 +1 @@ +trait-category-languages = Дополнительный язык \ No newline at end of file diff --git a/Resources/Locale/ru-RU/ss220/language/language-ui.ftl b/Resources/Locale/ru-RU/ss220/language/language-ui.ftl new file mode 100644 index 000000000000..9e847f92e2f1 --- /dev/null +++ b/Resources/Locale/ru-RU/ss220/language/language-ui.ftl @@ -0,0 +1,3 @@ +language-settings-ui-no-languages = Вы не знаете дополнительных языков. +language-settings-ui-select-default = Выберите язык по умолчанию: +language-settings-ui-field-name = { $name } (ключ { $key }) diff --git a/Resources/Locale/ru-RU/ss220/taperecorder/taperecorder.ftl b/Resources/Locale/ru-RU/ss220/taperecorder/taperecorder.ftl index 9d0ad1600699..fb49356f0585 100644 --- a/Resources/Locale/ru-RU/ss220/taperecorder/taperecorder.ftl +++ b/Resources/Locale/ru-RU/ss220/taperecorder/taperecorder.ftl @@ -28,7 +28,7 @@ tape-recorder-print-message-text = [bold][{$time}] {$source}: [/bold] {$message} tape-recorder-print-end-text = [bold]Конец стенограммы записи[/bold] ent-TapeRecorder = диктофон - .desc = Всё что будет сказано в это устройство может и будет использовано против вас в суде космического закона. + .desc = Всё что будет сказано в это устройство может и будет использовано против вас в суде космического закона. Во время великой межсистемной войны, в целях разведки ТСФ, был оснащён встроенным переводчиком. ent-CassetteTape = кассета диктофона .desc = Кассета с магнитной лентой которая может содержать до двух минут записи. ent-TapeRecorderTranscript = стенограмма записи диктофона diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index 2caddaf7a2b4..bfd004075b46 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -252,6 +252,13 @@ damageProtection: flatReductions: Heat: 10 # capable of touching light bulbs and stoves without feeling pain! + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Binary + - id: Universal + # SS220-Add-Languages end - type: entity abstract: true @@ -352,6 +359,38 @@ - AllAccess #Randomized access would be fun. AllAccess is the best i can think of right now that does make it too hard for it to enter the station or navigate it.. - type: AccessReader access: [["Command"], ["Research"]] +#SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + - id: SolCommon + canSpeak: false + - id: Tradeband + canSpeak: false + - id: Gutter + canSpeak: false + - id: Clownish + canSpeak: false + - id: NeoRusskiya + canSpeak: false + - id: Sintaunathi + canSpeak: false + - id: Siiktajr + canSpeak: false + - id: VoxPidgin + canSpeak: false + - id: Rootspeak + canSpeak: false + - id: Bubblish + canSpeak: false + - id: Tkachi + canSpeak: false + - id: Arati + canSpeak: false + - id: Canilunzt + canSpeak: false + - id: Binary +#SS220 languages revive end - type: StartIonStormed ionStormAmount: 3 - type: IonStormTarget diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml index 87b8d54a9773..cbb98c161a88 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml @@ -87,6 +87,39 @@ name: syndicate assault cyborg description: A lean, mean killing machine with access to a variety of deadly modules. components: +#SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + - id: SolCommon + canSpeak: false + - id: Tradeband + canSpeak: false + - id: Gutter + canSpeak: false + - id: Clownish + canSpeak: false + - id: NeoRusskiya + canSpeak: false + - id: Sintaunathi + canSpeak: false + - id: Siiktajr + canSpeak: false + - id: VoxPidgin + canSpeak: false + - id: Rootspeak + canSpeak: false + - id: Bubblish + canSpeak: false + - id: Tkachi + canSpeak: false + - id: Arati + canSpeak: false + - id: Canilunzt + canSpeak: false + - id: Binary +# - id: Codespeak +#SS220 languages revive end - type: Sprite layers: - state: synd_sec @@ -218,4 +251,4 @@ interactSuccessString: petting-success-derelict-cyborg interactFailureString: petting-failure-derelict-cyborg interactSuccessSound: - path: /Audio/Ambience/Objects/periodic_beep.ogg \ No newline at end of file + path: /Audio/Ambience/Objects/periodic_beep.ogg diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 8532867a401e..a0adbc09e25d 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -7,6 +7,14 @@ id: MobCorgiIan description: Favorite pet corgi. components: + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Sprite drawdepth: Mobs sprite: Mobs/Pets/corgi.rsi @@ -43,6 +51,13 @@ id: MobCorgiIanOld description: Still the favorite pet corgi. Love his wheels. components: + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Sprite layers: - map: ["enum.DamageStateVisualLayers.Base"] @@ -68,6 +83,14 @@ id: MobCorgiLisa description: Ian's favorite corgi. components: + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Sprite layers: - map: ["enum.DamageStateVisualLayers.Base"] @@ -118,6 +141,14 @@ attributes: proper: true gender: male + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Butcherable # A puppy? You monster... spawned: - id: FoodMeatCorgi @@ -155,6 +186,14 @@ - type: HTN rootTask: task: SimpleHostileCompound + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: Siiktajr + canSpeak: false + #SS220 languages revive end - type: Grammar attributes: proper: true @@ -179,6 +218,14 @@ attributes: proper: true gender: male + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: Siiktajr + canSpeak: false + #SS220 languages revive end - type: Tag tags: - CannotSuicide @@ -206,6 +253,14 @@ - type: Grammar attributes: gender: male + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: Siiktajr + canSpeak: false + #SS220 languages revive end - type: Tag tags: - CannotSuicide @@ -223,6 +278,14 @@ attributes: proper: true gender: male + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Tag tags: - CannotSuicide @@ -247,6 +310,14 @@ - type: LyingDownOnBuckledEntity offset: -0.1, 0 #SS220 Add lying down on entity end + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: Siiktajr + canSpeak: false + #SS220 languages revive end - type: Physics # 220 cat-emotes start - type: Speech @@ -318,6 +389,14 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: mcgriff + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Physics - type: Fixtures fixtures: @@ -385,6 +464,14 @@ Base: paperwork_dead Dead: Base: paperwork_dead + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Butcherable spawned: - id: FoodMeat @@ -411,6 +498,14 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: walter + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Physics - type: Fixtures fixtures: @@ -475,6 +570,14 @@ attributes: proper: true gender: male + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Tag tags: - CannotSuicide @@ -511,6 +614,14 @@ attributes: proper: true gender: female + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Sprite drawdepth: Mobs sprite: Mobs/Animals/possum.rsi @@ -541,6 +652,14 @@ attributes: proper: true gender: female + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: Tradeband #Cause cargo dep + canSpeak: false + #SS220 languages revive end - type: Tag tags: - CannotSuicide @@ -563,6 +682,14 @@ attributes: proper: true gender: male + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Tag tags: - CannotSuicide @@ -587,6 +714,14 @@ amount: 3 - id: Telecrystal5 amount: 1 + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: Canilunzt #Vulp gagaga + canSpeak: false + #SS220 languages revive end - type: Grammar attributes: proper: true @@ -617,6 +752,14 @@ noMovementLayers: movement: state: hamster-0 + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: GhostRole makeSentient: true allowSpeech: true @@ -664,6 +807,14 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Animals/snake_hiss.ogg + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: Arati + canSpeak: false + #SS220 languages revive end - type: NpcFactionMember factions: - PetsNT @@ -733,6 +884,14 @@ attributes: proper: true gender: female + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Tag tags: - CannotSuicide @@ -761,6 +920,14 @@ state: aslime-_3 shader: unshaded - map: [ "head" ] + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: Bubblish + canSpeak: false + #SS220 languages revive end - type: Inventory speciesId: slime templateId: head @@ -846,6 +1013,14 @@ description: ghost-role-information-punpun-description rules: ghost-role-information-nonantagonist-rules - type: GhostTakeoverAvailable + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Butcherable butcheringType: Spike spawned: @@ -887,6 +1062,14 @@ tags: - VimPilot - CannotSuicide + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: SolCommon + canSpeak: false + #SS220 languages revive end - type: Grammar attributes: proper: true @@ -913,6 +1096,14 @@ - MobMask layer: - MobLayer + #SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + canSpeak: false + - id: Siiktajr + canSpeak: false + #SS220 languages revive end - type: Grammar attributes: gender: male diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index a2b76cafbdd3..dfd3067a87a2 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -486,6 +486,25 @@ visible: false - type: BlockMovement blockInteraction: false +#SS220 languages revive begin + - type: Language + availableLanguages: + - id: Galactic + - id: SolCommon + - id: Tradeband + - id: Gutter + - id: Clownish + - id: NeoRusskiya + - id: Sintaunathi + - id: Siiktajr + - id: VoxPidgin + - id: Rootspeak + - id: Bubblish + - id: Tkachi + - id: Arati + - id: Canilunzt + - id: Binary +#SS220 languages revive end - type: SiliconLawProvider laws: Crewsimov - type: SiliconLawBound diff --git a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml index 2c17e881ddcd..049e94315c30 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml @@ -80,6 +80,12 @@ speechVerb: Arachnid speechSounds: Arachnid allowedEmotes: ['Click', 'Chitter', 'Hiss'] #ss-220-emote-fix + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Arati + # SS220-Add-Languages end - type: Vocal sounds: Male: UnisexArachnid diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index 8f499cb53ad0..16478f7972ef 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -119,6 +119,12 @@ actionPrototype: DionaGibAction allowedStates: - Dead + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Rootspeak + # SS220-Add-Languages end - type: Inventory templateId: diona femaleDisplacements: diff --git a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml index 69f0a51825aa..8173b928cd4a 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml @@ -61,6 +61,12 @@ accent: dwarf - type: Speech speechSounds: Bass + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Eldwarf + # SS220-Add-Languages end - type: HumanoidAppearance species: Human hideLayersOnEquip: diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 9aa36e4ec528..eba08bb4afb0 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -20,6 +20,12 @@ hideLayersOnEquip: - Hair - Snout + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: SolCommon + # SS220-Add-Languages end - type: Inventory femaleDisplacements: jumpsuit: diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml index 05057ceddfb8..08f07e56b751 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml @@ -140,6 +140,12 @@ sprite: "Effects/creampie.rsi" state: "creampie_moth" visible: false + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Tkachi + # SS220-Add-Languages end - type: Inventory femaleDisplacements: jumpsuit: diff --git a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml index 35ed18a135ca..ea49a81c7112 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml @@ -88,6 +88,12 @@ 295: 0.6 285: 0.4 - type: Wagging + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Sintaunathi + # SS220-Add-Languages end - type: Inventory speciesId: reptilian femaleDisplacements: diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index 1be73f85a7bc..50588a84cc31 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -141,6 +141,12 @@ types: Asphyxiation: -1.0 maxSaturation: 15 + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Bubblish + # SS220-Add-Languages end - type: Inventory femaleDisplacements: jumpsuit: diff --git a/Resources/Prototypes/Entities/Mobs/Species/vox.yml b/Resources/Prototypes/Entities/Mobs/Species/vox.yml index c7ba992359ef..96a7a67ca039 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/vox.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/vox.yml @@ -125,6 +125,12 @@ sprite: "Effects/creampie.rsi" state: "creampie_vox" # Not default visible: false + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: VoxPidgin + # SS220-Add-Languages end - type: Inventory speciesId: vox displacements: diff --git a/Resources/Prototypes/Entities/Objects/Fun/pai.yml b/Resources/Prototypes/Entities/Objects/Fun/pai.yml index bf284e468724..71f80451909f 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/pai.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/pai.yml @@ -81,6 +81,12 @@ whitelist: components: - SecretStash + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Universal + # SS220-Add-Languages end - type: entity parent: [ PersonalAI] diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml index 8f181900b705..4adf376a95c0 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml @@ -53,6 +53,13 @@ whitelist: components: - SecretStash + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Binary + - id: Universal + # SS220-Add-Languages end - type: entity parent: MMI @@ -103,6 +110,13 @@ - type: IntrinsicRadioTransmitter channels: - Binary + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Binary + - id: Universal + # SS220-Add-Languages end - type: ActiveRadio channels: - Binary diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml index bd629fb59fbc..5511f9f3ce93 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml @@ -31,6 +31,11 @@ interval: 10 - !type:AddImplantSpecial implants: [ SadTromboneImplant ] + # SS220-Add-Languages begin + - !type:AddLanguageSpecial + languages: + - Clownish + # SS220-Add-Languages end - type: startingGear id: ClownGear diff --git a/Resources/Prototypes/SS220/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/SS220/Entities/Mobs/NPCs/animals.yml index bf94bd970dc3..f35fc411b8c5 100644 --- a/Resources/Prototypes/SS220/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/SS220/Entities/Mobs/NPCs/animals.yml @@ -270,6 +270,11 @@ autoRot: true radius: 2 netsync: false + - type: Language + availableLanguages: + - id: Galactic + - id: Binary + - id: Universal - type: entity name: крокодил diff --git a/Resources/Prototypes/SS220/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/SS220/Entities/Mobs/Species/felinid.yml index 2a0a03d333e8..7bfd9d7badb4 100644 --- a/Resources/Prototypes/SS220/Entities/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/SS220/Entities/Mobs/Species/felinid.yml @@ -111,6 +111,9 @@ #MEOW LESS OK THX - type: Emoting chatEmoteCooldown: 5.0 + - type: Language + availableLanguages: + - id: Galactic - type: entity save: false diff --git a/Resources/Prototypes/SS220/Entities/Mobs/Species/tajaran.yml b/Resources/Prototypes/SS220/Entities/Mobs/Species/tajaran.yml index 25db56f1d01c..2e62048385b0 100644 --- a/Resources/Prototypes/SS220/Entities/Mobs/Species/tajaran.yml +++ b/Resources/Prototypes/SS220/Entities/Mobs/Species/tajaran.yml @@ -107,6 +107,10 @@ visionRadius: 5 highSensitiveVisionRadius: 2 addAction: true + - type: Language + availableLanguages: + - id: Galactic + - id: Siiktajr - type: entity id: MobTajaranDummy diff --git a/Resources/Prototypes/SS220/Entities/Mobs/Species/vulpkanin.yml b/Resources/Prototypes/SS220/Entities/Mobs/Species/vulpkanin.yml index e369d58d687b..9fbdff8a147f 100644 --- a/Resources/Prototypes/SS220/Entities/Mobs/Species/vulpkanin.yml +++ b/Resources/Prototypes/SS220/Entities/Mobs/Species/vulpkanin.yml @@ -69,6 +69,10 @@ damage: types: Slash: 5.5 + - type: Language + availableLanguages: + - id: Galactic + - id: Canilunzt #если будет дальнейшее обсуждение и одобрение, займусь этой рассой для раундстартового состояния. Пока так diff --git a/Resources/Prototypes/SS220/Entities/Objects/Fun/pai.yml b/Resources/Prototypes/SS220/Entities/Objects/Fun/pai.yml index fbfa2e7ca172..ec3c16067d88 100644 --- a/Resources/Prototypes/SS220/Entities/Objects/Fun/pai.yml +++ b/Resources/Prototypes/SS220/Entities/Objects/Fun/pai.yml @@ -86,3 +86,9 @@ - type: Vocal # SS220 borg emotes to PAI start sounds: Unsexed: UnisexSilicon # SS220 borg emotes to PAI end + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Universal + # SS220-Add-Languages end diff --git a/Resources/Prototypes/SS220/Geras/slime.yml b/Resources/Prototypes/SS220/Geras/slime.yml index 8fe21ee27960..d3c5d2c40168 100644 --- a/Resources/Prototypes/SS220/Geras/slime.yml +++ b/Resources/Prototypes/SS220/Geras/slime.yml @@ -32,3 +32,9 @@ Base: blue_adult_slime Dead: Base: blue_adult_slime_dead + # SS220-Add-Languages begin + - type: Language + availableLanguages: + - id: Galactic + - id: Bubblish + # SS220-Add-Languages end diff --git a/Resources/Prototypes/SS220/Language/language.yml b/Resources/Prototypes/SS220/Language/language.yml new file mode 100644 index 000000000000..d93b7cdbb57a --- /dev/null +++ b/Resources/Prototypes/SS220/Language/language.yml @@ -0,0 +1,298 @@ +# Based on SS13 Paradise (https://github.com/ParadiseSS13/Paradise/blob/017a56f8080361f04c296c94859802aaddf25114/code/modules/mob/language.dm#L4) + +# Everyone understands you, you understand everyone, but there's no right to speak all languages +- type: language + id: Universal + key: uni + scrambleMethod: !type:EmptyScrambleMethod # Doesn't scramble messages + +# Galactic common lanaguges region: +- type: language + id: Galactic + key: gl + name: "language-galacticcommon-name" + description: "language-galacticcommon-desc" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [бла, бла, бла, бле, мех, ньех, нах, ах, угу, увы] + +- type: language + id: SolCommon + key: sl + name: "language-solcommon-name" + description: "language-solcommon-desc" + color: "#2f2fad" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [tao, shi, tzu, yi, com, be, is, i, op, vi, ed, lec, mo, cle, te, dis, e] + +- type: language + id: Tradeband + key: trd + name: "language-tradeband-name" + description: "language-tradeband-desc" + color: "#87451c" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 1 + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [lorem, ipsum, dolor, sit, amet, consectetur, adipiscing, elit, sed, do, eiusmod, tempor, incididunt, ut, labore, + et, dolore, magna, aliqua, ut, enim, ad, minim, veniam, quis, nostrud, exercitation, + ullamco, laboris, nisi, ut, aliquip, ex, ea, commodo, consequat, duis, aute, irure, + dolor, in, reprehenderit, in, voluptate, velit, esse, cillum, dolore, eu, fugiat, + nulla, pariatur, excepteur, sint, occaecat, cupidatat, non, proident, sunt, in, + culpa, qui, officia, deserunt, mollit, anim, id, est, laborum] + +- type: language + id: Codespeak + key: cod + name: "language-codespeak-name" + description: "language-codespeak-desc" + color: "#8f4a4b" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 1 + specialCharacters: + - character: " " + - character: ", " + capitalize: false + weight: 0.3 + syllables: [виски, кола, тоник, водка, вермут, ром, эхо, лима, альфа, дельта, гамма, сигма, чарли, зомби, шлюз, бармен, повар, грузчик, учёный, инженер, карп, ИИ, борг, смена, цель, русский, перчатки, адвокат, пьяный, бипски, иан, морти, черный, белый, капитан, и] + +- type: language + id: Gutter + key: gt + name: "language-gutter-name" + description: "language-gutter-desc" + color: "#7092BE" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 0.55 + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [gra, ba, ba, breh, bra, rah, dur, ra, ro, gro, go, ber, bar, geh, heh, gra] + +- type: language + id: Clownish + key: clw + name: "language-clownish-name" + description: "language-clownish-desc" + color: "#ff0000" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 0.6 + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [хонк, cкуик, ойк, бонк, тут, наф, пуп, ви, вуб, ниф] + +- type: language + id: NeoRusskiya + key: ru + name: "language-neorusskiya-name" + description: "language-neorusskiya-desc" + color: "#7c4848" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 0.65 + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [dyen, bar, bota, vyek, tvo, slov, slav, syen, doup, vah, laz, gloz, + yet, nyet, da, sky, glav, glaz, netz, doomat, zat, moch, boz, comy, vrad, vrade, + tay, bli, ay, nov, livn, tolv, glaz, gliz, ouy, zet, yevt, dat, botat, nev, novy, vzy, + nov, sho, obsh, dasky, key, skey, ovsky, skaya, bib, kiev, studen, var, bul, vyan, tzion, + vaya, myak, gino, volo, olam, miti, nino, menov, perov, odasky, trov, niki, ivano, dostov, + sokol, oupa, pervom, schel, tizan, chka, tagan, dobry, okt, boda, veta, idi, cyk, blyt, hui, + na, udi, litchki, casa, linka, toly, anatov, vich, vech, vuch, toi, ka, vod] + +# Race region: +- type: language + id: Sintaunathi + key: sin + name: "language-unathi-name" + description: "language-unathi-desc" + color: "#228B22" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [za, az, ze, ez, zi, iz, zo, oz, zu, uz, zs, sz, ha, ah, he, eh, + hi, ih, ho, oh, hu, uh, hs, sh, la, al, le, el, li, il, lo, ol, + lu, ul, ls, sl, ka, ak, ke, ek, ki, ik, ko, ok, ku, uk, ks, sk, + sa, as, se, es, si, is, so, os, su, us, ss, ss, ra, ar, re, er, + ri, ir, ro, or, ru, ur, rs, sr, a, a, e, e, i, i, o, o, u, u, + s, s] + +- type: language + id: Siiktajr + key: sii + name: "language-siiktajr-name" + description: "language-siiktajr-desc" + color: "#803B56" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 0.4 + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [rr, rrr, taj'r, kir, raj, kii, mir, kah, hrar, jun, + kra, ahkъ, nal, vah, khaz, jri, ran, hal, nja, dar, + darr, mi, jr'i, dynh, manq', rhe, zar, ketъ, rir, rik, + r'rhaz, kal, chur, eech, thaa, dra, jurl, jurl, azuъ, + mah, sanu, dra, ii'r, ъka, aasi, far, mah, tul, ragh + wa, baq, ara, qara, zir', sam, mak, cresh, khan] + +- type: language + id: VoxPidgin + key: vox + name: "language-voxpidgin-name" + description: "language-voxpidgin-desc" + color: "#AA00AA" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [ti, ti, ti, hi, hi, ki, ki, ki, + ki, ya, ta, ha, ka, ya, yi, chi, cha, kah, + SKRE, AHK, EHK, RAWK, KRA, AAA, EEE, KI, + II, KRI, KA] + +- type: language + id: Rootspeak + key: rt + name: "language-rootspeak-name" + description: "language-rootspeak-desc" + color: "#804000" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 0.3 + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [hs, zt, kr, st, sh] + +- type: language + id: Bubblish + key: bbl + name: "language-bobblish-name" + description: "language-bobblish-desc" + color: "#0077AA" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 0.6 + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [блоб, плап, поп, боп, буп, пламп, буб, баб, бламп] + +- type: language + id: Tkachi + key: tch + name: "language-tkachi-name" + description: "language-tkachi-desc" + color: "#869b29" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 0.55 + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + - character: "-" + weight: 0.5 + syllables: [år, i, går, sek, mo, ff, ok, gj, ø, gå, la, le, lit, ygg, van, + dår, næ, møt, idd, hvo, ja, på, han, så, ån, det, att, nå, gö, bra, + int, tyc, om, när, två, må, dag, sjä, vii, vuo, eil, tun, käyt, teh, + vä, hei, huo, suo, ää, ten, ja, heu, stu, uhr, kön, we, hön] + +- type: language + id: Eldwarf + key: El + name: "language-eldwarf-name" + description: "language-eldwarf-desc" + color: "#515d63" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 0.55 + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [вр, врон, рон, крон, друк, трым, бд, шкж, брм, рыт, грн, драш, клм, трк, врг, хрм, + рд, клыт, зм, эмээ, гнык, зрм, длк, брг, грд, скрын, вродд, бдар, глтх, скрр, + трг, бртх, хыкл, тр, крг, рдм, грыж, мд, гхрм, клм, прк, кнур] + +- type: language + id: Arati + key: ara + name: "language-arati-name" + description: "language-arati-desc" + color: "#68477a" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 0.52 + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + - character: "'" + weight: 0.7 + syllables: [шаш, хаш, раш, шар, хра, хашт, шара, шкар, храк, тар, хар, схр, + карш, аш, шр, зз, ср, хаз, рашк, харш, рашн, шан, харз, шах, шарз, рашар, + кар, харат, хш, харр, арш', ашан, шанн, хсар, ашат, хаа, хазт, рашта, зара, + хаш, анн, аро', хат] + +- type: language + id: Canilunzt + key: cani + name: "language-canilunzt-name" + description: "language-canilunzt-desc" + color: "#B97A57" + scrambleMethod: !type:SyllablesScrambleMethod + specialCharacterChance: 0.45 + specialCharacters: + - character: " " + - character: ". " + capitalize: true + weight: 0.3 + syllables: [rur, ya, cen, rawr, bar, kuk, tek, qat, uk, wu, vuh, tah, tch, schz, auch, + ist, ein, entch, zwichs, tut, mir, wo, bis, es, vor, nic, gro, lll, + enem, zandt, tzch, noch, hel, ischt, far, wa, baram, iereng, tech, lach, + sam, mak, lich, gen, or, ag, eck, gec, stag, onn, bin, ket, jarl, vulf, + einech, cresthz, azunein, ghzth] + +# Misc region: +- type: language + id: Binary + key: bin + name: "language-binary-name" + description: "language-binary-desc" + color: "#2cc2e8" + scrambleMethod: !type:SyllablesScrambleMethod + scrambledLengthCoefficient: 0.5 + specialCharacterChance: 0.8 + specialCharacters: + - character: " " + syllables: [1, 0] diff --git a/Resources/Prototypes/SS220/Mobs/PetParrots.yml b/Resources/Prototypes/SS220/Mobs/PetParrots.yml index 91a72778314b..8bfbf15b99d3 100644 --- a/Resources/Prototypes/SS220/Mobs/PetParrots.yml +++ b/Resources/Prototypes/SS220/Mobs/PetParrots.yml @@ -58,6 +58,11 @@ sprite: SS220/Mobs/Animals/cuckadoo.rsi - type: InteractionPopup successChance: 1 + - type: Language + availableLanguages: + - id: Galactic + - id: SolCommon + canSpeak: false - type: Grammar attributes: proper: true diff --git a/Resources/Prototypes/SS220/Traits/language_traits.yml b/Resources/Prototypes/SS220/Traits/language_traits.yml new file mode 100644 index 000000000000..1b6b70f180fc --- /dev/null +++ b/Resources/Prototypes/SS220/Traits/language_traits.yml @@ -0,0 +1,35 @@ +- type: trait + id: TradebandLanguage + name: "language-tradeband-name" + description: "language-tradeband-desc" + category: Languages + cost: 1 + learnedLanguages: + - id: Tradeband + +- type: trait + id: GutterLanguage + name: "language-gutter-name" + description: "language-gutter-desc" + category: Languages + cost: 1 + learnedLanguages: + - id: Gutter + +- type: trait + id: ClownishLanguage + name: "language-clownish-name" + description: "language-clownish-desc" + category: Languages + cost: 1 + learnedLanguages: + - id: Clownish + +- type: trait + id: NeoRusskiyaLanguage + name: "language-neorusskiya-name" + description: "language-neorusskiya-desc" + category: Languages + cost: 1 + learnedLanguages: + - id: NeoRusskiya diff --git a/Resources/Prototypes/Traits/categories.yml b/Resources/Prototypes/Traits/categories.yml index d024b3fcff31..b9810d8f3082 100644 --- a/Resources/Prototypes/Traits/categories.yml +++ b/Resources/Prototypes/Traits/categories.yml @@ -10,3 +10,10 @@ - type: traitCategory id: Quirks name: trait-category-quirks + +# SS220-Add-Languages begin +- type: traitCategory + id: Languages + name: trait-category-languages + maxTraitPoints: 1 +# SS220-Add-Languages end \ No newline at end of file diff --git a/Resources/Textures/SS220/Interface/Nano/language_settings-button.png b/Resources/Textures/SS220/Interface/Nano/language_settings-button.png new file mode 100644 index 000000000000..8e7943199a2e Binary files /dev/null and b/Resources/Textures/SS220/Interface/Nano/language_settings-button.png differ