diff --git a/Content.Client/Chat/CollectiveMindChatUpdateSystem.cs b/Content.Client/Chat/CollectiveMindChatUpdateSystem.cs new file mode 100644 index 00000000000..0cbd3ca4bee --- /dev/null +++ b/Content.Client/Chat/CollectiveMindChatUpdateSystem.cs @@ -0,0 +1,32 @@ +using Content.Client.Chat.Managers; +using Content.Shared.Sunrise.CollectiveMind; +using Robust.Client.Player; + +namespace Content.Client.Chat +{ + public sealed class CollectiveMindChatUpdateSystem : EntitySystem + { + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRemove); + } + + public CollectiveMindComponent? Player => CompOrNull(_playerManager.LocalSession?.AttachedEntity); + public bool IsCollectiveMind => Player != null; + + private void OnInit(EntityUid uid, CollectiveMindComponent component, ComponentInit args) + { + _chatManager.UpdatePermissions(); + } + + private void OnRemove(EntityUid uid, CollectiveMindComponent component, ComponentRemove args) + { + _chatManager.UpdatePermissions(); + } + } +} diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs index 67b5f5202f9..9232661933b 100644 --- a/Content.Client/Chat/Managers/ChatManager.cs +++ b/Content.Client/Chat/Managers/ChatManager.cs @@ -14,6 +14,7 @@ internal sealed class ChatManager : IChatManager [Dependency] private readonly IEntitySystemManager _systems = default!; private ISawmill _sawmill = default!; + public event Action? PermissionsUpdated; // Sunrise-Edit public void Initialize() { @@ -67,9 +68,23 @@ public void SendMessage(string text, ChatSelectChannel channel) _consoleHost.ExecuteCommand($"whisper \"{CommandParsing.Escape(str)}\""); break; + // Sunrise-Start + case ChatSelectChannel.CollectiveMind: + _consoleHost.ExecuteCommand($"cmsay \"{CommandParsing.Escape(str)}\""); + break; + // Sunrise-End + + default: throw new ArgumentOutOfRangeException(nameof(channel), channel, null); } } + + // Sunrise-Start + public void UpdatePermissions() + { + PermissionsUpdated?.Invoke(); + } + // Sunrise-End } } diff --git a/Content.Client/Chat/Managers/IChatManager.cs b/Content.Client/Chat/Managers/IChatManager.cs index 6464ca10196..bb067e4c125 100644 --- a/Content.Client/Chat/Managers/IChatManager.cs +++ b/Content.Client/Chat/Managers/IChatManager.cs @@ -5,7 +5,8 @@ namespace Content.Client.Chat.Managers public interface IChatManager { void Initialize(); - + event Action PermissionsUpdated; // Sunrise-Edit public void SendMessage(string text, ChatSelectChannel channel); + public void UpdatePermissions(); // Sunrise-Edit } } diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs index 4550b53ca5d..54cd902e68e 100644 --- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs +++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs @@ -13,9 +13,11 @@ using Content.Client.UserInterface.Screens; using Content.Client.UserInterface.Systems.Chat.Widgets; using Content.Client.UserInterface.Systems.Gameplay; +using Content.Shared._Sunrise.CollectiveMind; using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Chat; +using Content.Shared.Decals; using Content.Shared.Damage.ForceSay; using Content.Shared.Decals; using Content.Shared.Input; @@ -57,6 +59,7 @@ public sealed class ChatUIController : UIController [UISystemDependency] private readonly ExamineSystem? _examine = default; [UISystemDependency] private readonly GhostSystem? _ghost = default; + [UISystemDependency] private readonly CollectiveMindChatUpdateSystem? _collectiveMind = default!; [UISystemDependency] private readonly TypingIndicatorSystem? _typingIndicator = default; [UISystemDependency] private readonly ChatSystem? _chatSys = default; [UISystemDependency] private readonly TransformSystem? _transform = default; @@ -79,7 +82,8 @@ public sealed class ChatUIController : UIController {SharedChatSystem.EmotesAltPrefix, ChatSelectChannel.Emotes}, {SharedChatSystem.AdminPrefix, ChatSelectChannel.Admin}, {SharedChatSystem.RadioCommonPrefix, ChatSelectChannel.Radio}, - {SharedChatSystem.DeadPrefix, ChatSelectChannel.Dead} + {SharedChatSystem.DeadPrefix, ChatSelectChannel.Dead}, + {SharedChatSystem.CollectiveMindPrefix, ChatSelectChannel.CollectiveMind} // Sunrise-Edit }; public static readonly Dictionary ChannelPrefixes = new() @@ -92,7 +96,8 @@ public sealed class ChatUIController : UIController {ChatSelectChannel.Emotes, SharedChatSystem.EmotesPrefix}, {ChatSelectChannel.Admin, SharedChatSystem.AdminPrefix}, {ChatSelectChannel.Radio, SharedChatSystem.RadioCommonPrefix}, - {ChatSelectChannel.Dead, SharedChatSystem.DeadPrefix} + {ChatSelectChannel.Dead, SharedChatSystem.DeadPrefix}, + {ChatSelectChannel.CollectiveMind, SharedChatSystem.CollectiveMindPrefix} // Sunrise-Edit }; /// @@ -173,6 +178,7 @@ public override void Initialize() _sawmill = Logger.GetSawmill("chat"); _sawmill.Level = LogLevel.Info; _admin.AdminStatusUpdated += UpdateChannelPermissions; + _manager.PermissionsUpdated += UpdateChannelPermissions; // Sunrise-Edit _player.LocalPlayerAttached += OnAttachedChanged; _player.LocalPlayerDetached += OnAttachedChanged; _state.OnStateChanged += StateChanged; @@ -222,6 +228,11 @@ public override void Initialize() _input.SetInputCommand(ContentKeyFunctions.CycleChatChannelBackward, InputCmdHandler.FromDelegate(_ => CycleChatChannel(false))); + // Sunrise-Start + _input.SetInputCommand(ContentKeyFunctions.FocusCollectiveMindChat, + InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.CollectiveMind))); + // Sunrise-End + var gameplayStateLoad = UIManager.GetUIController(); gameplayStateLoad.OnScreenLoad += OnScreenLoad; gameplayStateLoad.OnScreenUnload += OnScreenUnload; @@ -552,7 +563,16 @@ private void UpdateChannelPermissions() FilterableChannels |= ChatChannel.AdminAlert; FilterableChannels |= ChatChannel.AdminChat; CanSendChannels |= ChatSelectChannel.Admin; + FilterableChannels |= ChatChannel.CollectiveMind; // Sunrise-Edit + } + + // Sunrise-Start + if (_collectiveMind != null && _collectiveMind.IsCollectiveMind) + { + FilterableChannels |= ChatChannel.CollectiveMind; + CanSendChannels |= ChatSelectChannel.CollectiveMind; } + // Sunrise-End SelectableChannels = CanSendChannels; @@ -683,21 +703,39 @@ private bool TryGetRadioChannel(string text, out RadioChannelPrototype? radioCha && _chatSys.TryProccessRadioMessage(uid, text, out _, out radioChannel, quiet: true); } + // Sunrise-Start + private bool TryGetCollectiveMind(string text, out CollectiveMindPrototype? collectiveMind) + { + collectiveMind = null; + return _player.LocalEntity is { Valid: true } uid + && _chatSys != null + && _chatSys.TryProccessCollectiveMindMessage(uid, text, out _, out collectiveMind, quiet: true); + } + public void UpdateSelectedChannel(ChatBox box) { - var (prefixChannel, _, radioChannel) = SplitInputContents(box.ChatInput.Input.Text.ToLower()); + var (prefixChannel, _, radioChannel, collectiveMind) = SplitInputContents(box.ChatInput.Input.Text.ToLower()); - if (prefixChannel == ChatSelectChannel.None) - box.ChatInput.ChannelSelector.UpdateChannelSelectButton(box.SelectedChannel, null); - else - box.ChatInput.ChannelSelector.UpdateChannelSelectButton(prefixChannel, radioChannel); + switch (prefixChannel) + { + case ChatSelectChannel.None: + box.ChatInput.ChannelSelector.UpdateChannelSelectButton(box.SelectedChannel, null, null); + break; + case ChatSelectChannel.CollectiveMind: + box.ChatInput.ChannelSelector.UpdateChannelSelectButton(prefixChannel, null, collectiveMind); + break; + default: + box.ChatInput.ChannelSelector.UpdateChannelSelectButton(prefixChannel, radioChannel, null); + break; + } } + // Sunrise-End - public (ChatSelectChannel chatChannel, string text, RadioChannelPrototype? radioChannel) SplitInputContents(string text) + public (ChatSelectChannel chatChannel, string text, RadioChannelPrototype? radioChannel, CollectiveMindPrototype? collectiveMind) SplitInputContents(string text) { text = text.Trim(); if (text.Length == 0) - return (ChatSelectChannel.None, text, null); + return (ChatSelectChannel.None, text, null, null); // Sunrise-Edit // We only cut off prefix only if it is not a radio or local channel, which both map to the same /say command // because ???????? @@ -709,20 +747,25 @@ public void UpdateSelectedChannel(ChatBox box) chatChannel = PrefixToChannel.GetValueOrDefault(text[0]); if ((CanSendChannels & chatChannel) == 0) - return (ChatSelectChannel.None, text, null); + return (ChatSelectChannel.None, text, null, null); // Sunrise-Edit if (chatChannel == ChatSelectChannel.Radio) - return (chatChannel, text, radioChannel); + return (chatChannel, text, radioChannel, null); // Sunrise-Edit + + // Sunrise-Start + if (TryGetCollectiveMind(text, out var collectiveMind) && chatChannel == ChatSelectChannel.CollectiveMind) + return (chatChannel, text, radioChannel, collectiveMind); + // Sunrise-End if (chatChannel == ChatSelectChannel.Local) { if (_ghost?.IsGhost != true) - return (chatChannel, text, null); + return (chatChannel, text, null, null); // Sunrise-Edit else chatChannel = ChatSelectChannel.Dead; } - return (chatChannel, text[1..].TrimStart(), null); + return (chatChannel, text[1..].TrimStart(), null, null); // Sunrise-Edit } public void SendMessage(ChatBox box, ChatSelectChannel channel) @@ -737,7 +780,7 @@ public void SendMessage(ChatBox box, ChatSelectChannel channel) if (string.IsNullOrWhiteSpace(text)) return; - (var prefixChannel, text, var _) = SplitInputContents(text); + (var prefixChannel, text, var _, var _) = SplitInputContents(text); // Sunrise-Edit // Check if message is longer than the character limit if (text.Length > MaxMessageLength) diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs index df4f56cb27c..7c57d48788c 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs @@ -23,7 +23,8 @@ public sealed partial class ChannelFilterPopup : Popup ChatChannel.Admin, ChatChannel.AdminAlert, ChatChannel.AdminChat, - ChatChannel.Server + ChatChannel.Server, + ChatChannel.CollectiveMind // Sunrise-Edit }; private readonly Dictionary _filterStates = new(); diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs index 25cf851c7bb..17ac6dbf47a 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Shared._Sunrise.CollectiveMind; using Content.Shared.Chat; namespace Content.Client.UserInterface.Systems.Chat.Controls; @@ -68,9 +69,24 @@ public Color ChannelSelectColor(ChatSelectChannel channel) }; } - public void UpdateChannelSelectButton(ChatSelectChannel channel, Shared.Radio.RadioChannelPrototype? radio) + // Sunrise-Start + public void UpdateChannelSelectButton(ChatSelectChannel channel, Shared.Radio.RadioChannelPrototype? radio, CollectiveMindPrototype? collectiveMind) { - Text = radio != null ? Loc.GetString(radio.Name) : ChannelSelectorName(channel); - Modulate = radio?.Color ?? ChannelSelectColor(channel); + if (radio != null) + { + Text = Loc.GetString(radio.Name); + Modulate = radio?.Color ?? ChannelSelectColor(channel); + } + else if (collectiveMind != null) + { + Text = Loc.GetString(collectiveMind.Name); + Modulate = collectiveMind.Color; + } + else + { + Text = ChannelSelectorName(channel); + Modulate = ChannelSelectColor(channel); + } } + // Sunrise-End } diff --git a/Content.Server/Chat/Commands/CollectiveMindCommand.cs b/Content.Server/Chat/Commands/CollectiveMindCommand.cs new file mode 100644 index 00000000000..ca1496a7a5d --- /dev/null +++ b/Content.Server/Chat/Commands/CollectiveMindCommand.cs @@ -0,0 +1,42 @@ +using Content.Server.Chat.Systems; +using Content.Shared.Administration; +using Robust.Shared.Console; +using Robust.Shared.Enums; + +namespace Content.Server.Chat.Commands +{ + [AnyCommand] + internal sealed class CollectiveMindCommand : IConsoleCommand + { + public string Command => "cmsay"; + public string Description => "Send chat messages to the collective mind."; + public string Help => "cmsay "; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not { } player) + { + shell.WriteError("This command cannot be run from the server."); + return; + } + + if (player.Status != SessionStatus.InGame) + return; + + if (player.AttachedEntity is not {} playerEntity) + { + shell.WriteError("You don't have an entity!"); + return; + } + + if (args.Length < 1) + return; + + var message = string.Join(" ", args).Trim(); + if (string.IsNullOrEmpty(message)) + return; + + EntitySystem.Get().TrySendInGameICMessage(playerEntity, message, InGameICChatType.CollectiveMind, ChatTransmitRange.Normal); + } + } +} diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 9ca94b6f273..9c132fd5b98 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -11,7 +11,7 @@ using Content.Server.Speech.EntitySystems; using Content.Server.Station.Components; using Content.Server.Station.Systems; -using Content.Shared._Sunrise.TTS; +using Content.Shared._Sunrise.CollectiveMind; using Content.Shared.ActionBlocker; using Content.Shared.Administration; using Content.Shared.CCVar; @@ -25,6 +25,7 @@ using Content.Shared.Players; using Content.Shared.Radio; using Content.Shared.Speech; +using Content.Shared.Sunrise.CollectiveMind; using Content.Shared.Whitelist; using Robust.Server.Player; using Robust.Shared.Audio; @@ -247,6 +248,17 @@ public void TrySendInGameICMessage( } } + // Sunrise-Start + if (desiredType == InGameICChatType.CollectiveMind) + { + if (TryProccessCollectiveMindMessage(source, message, out var modMessage, out var channel)) + { + SendCollectiveMindChat(source, modMessage, channel); + return; + } + } + // Sunrise-End + // Otherwise, send whatever type. switch (desiredType) { @@ -404,6 +416,70 @@ public void DispatchStationAnnouncement( #region Private API + // Sunrise-Start + private void SendCollectiveMindChat(EntityUid source, string message, CollectiveMindPrototype? collectiveMind) + { + if (_mobStateSystem.IsDead(source)) + return; + + if (collectiveMind == null || message == "") + return; + + if (!TryComp(source, out var sourseCollectiveMindComp)) + return; + + if (!sourseCollectiveMindComp.Minds.Contains(collectiveMind.ID)) + return; + + var clients = Filter.Empty(); + var mindQuery = EntityQueryEnumerator(); + while (mindQuery.MoveNext(out var uid, out var collectMindComp, out var actorComp)) + { + if (_mobStateSystem.IsDead(uid)) + continue; + + if (collectMindComp.Minds.Contains(collectiveMind.ID)) + { + clients.AddPlayer(actorComp.PlayerSession); + } + } + + var admins = _adminManager.ActiveAdmins + .Select(p => p.ConnectedClient); + string messageWrap; + string adminMessageWrap; + + messageWrap = Loc.GetString("collective-mind-chat-wrap-message", + ("message", message), + ("channel", collectiveMind.LocalizedName)); + + adminMessageWrap = Loc.GetString("collective-mind-chat-wrap-message-admin", + ("source", source), + ("message", message), + ("channel", collectiveMind.LocalizedName)); + + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"CollectiveMind chat from {ToPrettyString(source):Player}: {message}"); + + _chatManager.ChatMessageToManyFiltered(clients, + ChatChannel.CollectiveMind, + message, + messageWrap, + source, + false, + true, + collectiveMind.Color); + + _chatManager.ChatMessageToMany(ChatChannel.CollectiveMind, + message, + adminMessageWrap, + source, + false, + true, + admins, + collectiveMind.Color); + } + // Sunrise-End + private void SendEntitySpeak( EntityUid source, string originalMessage, @@ -743,7 +819,7 @@ private bool CanSendInGame(string message, IConsoleShell? shell = null, ICommonS private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false, bool capitalizeTheWordI = true) { var newMessage = message.Trim(); - newMessage = ReplaceWords(newMessage); // Sunrise-TTS + newMessage = ReplaceWords(newMessage); // Sunrise-Edit newMessage = SanitizeMessageReplaceWords(newMessage); if (capitalize) @@ -983,6 +1059,7 @@ public enum InGameICChatType : byte Speak, Emote, Whisper, // Sunrise-TTS + CollectiveMind, // Sunrise-Edit } /// diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index d45110b8287..e1c8187a347 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -19,7 +19,7 @@ using Content.Shared.Damage; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; -using Content.Shared.CombatMode.Pacification; +using Content.Shared.Cuffs.Components; using Content.Shared.Humanoid; using Content.Shared.Interaction.Components; using Content.Shared.Mobs; @@ -37,6 +37,7 @@ using Content.Shared.Traits.Assorted; using Robust.Shared.Audio.Systems; using Content.Shared.Ghost.Roles.Components; +using Content.Shared.Sunrise.CollectiveMind; namespace Content.Server.Zombies { @@ -132,6 +133,19 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) melee.Angle = 0.0f; melee.HitSound = zombiecomp.BiteSound; + // Sunrise-Start + RemComp(target); + + var collectiveMindComponent = EnsureComp(target); + foreach (var collectiveMind in collectiveMindComponent.Minds.ToArray()) + { + collectiveMindComponent.Minds.Remove(collectiveMind); + } + + if (!collectiveMindComponent.Minds.Contains("Zombie")) + collectiveMindComponent.Minds.Add("Zombie"); + // Sunrise-End + if (mobState.CurrentState == MobState.Alive) { // Groaning when damaged diff --git a/Content.Shared/Chat/ChatChannel.cs b/Content.Shared/Chat/ChatChannel.cs index e8715a6ecb0..ec7ac85097f 100644 --- a/Content.Shared/Chat/ChatChannel.cs +++ b/Content.Shared/Chat/ChatChannel.cs @@ -85,10 +85,17 @@ public enum ChatChannel : ushort /// Unspecified = 1 << 14, + // Sunrise-Start + /// + /// Collective mind channel for entities who have comp. + /// + CollectiveMind = 1 << 15, + // Sunrise-End + /// /// Channels considered to be IC. /// - IC = Local | Whisper | Radio | Dead | Emotes | Damage | Visual | Notifications, + IC = Local | Whisper | Radio | Dead | Emotes | Damage | Visual | Notifications | CollectiveMind, AdminRelated = Admin | AdminAlert | AdminChat, } diff --git a/Content.Shared/Chat/ChatSelectChannel.cs b/Content.Shared/Chat/ChatSelectChannel.cs index c18bb9b8ee3..eaf85710a24 100644 --- a/Content.Shared/Chat/ChatSelectChannel.cs +++ b/Content.Shared/Chat/ChatSelectChannel.cs @@ -51,6 +51,13 @@ public enum ChatSelectChannel : ushort /// Admin = ChatChannel.AdminChat, + // Sunrise-Start + /// + /// CollectiveMind + /// + CollectiveMind = ChatChannel.CollectiveMind, + // Sunrise-End + Console = ChatChannel.Unspecified } } diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs index 96877c006b2..3d69b1ca750 100644 --- a/Content.Shared/Chat/SharedChatSystem.cs +++ b/Content.Shared/Chat/SharedChatSystem.cs @@ -1,4 +1,5 @@ using System.Collections.Frozen; +using Content.Shared._Sunrise.CollectiveMind; using Content.Shared.Popups; using Content.Shared.Radio; using Content.Shared.Speech; @@ -22,6 +23,7 @@ public abstract class SharedChatSystem : EntitySystem public const char AdminPrefix = ']'; public const char WhisperPrefix = ','; public const char DefaultChannelKey = 'р'; // Russian-Localization + public const char CollectiveMindPrefix = '+'; // Sunrise-Edit // Sunrise-TTS-Start public const int VoiceRange = 10; // how far voice goes in world units public const int WhisperClearRange = 2; // how far whisper goes while still being understandable, in world units @@ -44,18 +46,25 @@ public abstract class SharedChatSystem : EntitySystem /// private FrozenDictionary _keyCodes = default!; + private FrozenDictionary _mindKeyCodes = default!; // Sunrise-Edit + public override void Initialize() { base.Initialize(); DebugTools.Assert(_prototypeManager.HasIndex(CommonChannel)); SubscribeLocalEvent(OnPrototypeReload); CacheRadios(); + CacheCollectiveMinds(); // Sunrise-Edit } protected virtual void OnPrototypeReload(PrototypesReloadedEventArgs obj) { if (obj.WasModified()) CacheRadios(); + // Sunrise-Start + if (obj.WasModified()) + CacheCollectiveMinds(); + // Sunrise-End } private void CacheRadios() @@ -64,6 +73,15 @@ private void CacheRadios() .ToFrozenDictionary(x => x.KeyCode); } + // Sunrise-Start + private void CacheCollectiveMinds() + { + _prototypeManager.PrototypesReloaded -= OnPrototypeReload; + _mindKeyCodes = _prototypeManager.EnumeratePrototypes() + .ToFrozenDictionary(x => x.KeyCode); + } + // Sunrise-End + /// /// Attempts to find an applicable for a speaking entity's message. /// If one is not found, returns . @@ -152,6 +170,45 @@ public bool TryProccessRadioMessage( return true; } + // Sunrise-Start + public bool TryProccessCollectiveMindMessage( + EntityUid source, + string input, + out string output, + out CollectiveMindPrototype? channel, + bool quiet = false) + { + output = input.Trim(); + channel = null; + + if (input.Length == 0) + return false; + + if (!input.StartsWith(CollectiveMindPrefix)) + return false; + + if (input.Length < 2 || char.IsWhiteSpace(input[1])) + { + output = SanitizeMessageCapital(input[1..].TrimStart()); + if (!quiet) + _popup.PopupEntity(Loc.GetString("chat-manager-no-radio-key"), source, source); + return true; + } + + var channelKey = input[1]; + channelKey = char.ToLower(channelKey); + output = SanitizeMessageCapital(input[2..].TrimStart()); + + if (_mindKeyCodes.TryGetValue(channelKey, out channel) || quiet) + return true; + + var msg = Loc.GetString("chat-manager-no-such-channel", ("key", channelKey)); + _popup.PopupEntity(msg, source, source); + + return false; + } + // Sunrise-End + public string SanitizeMessageCapital(string message) { if (string.IsNullOrEmpty(message)) diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index 2dd671816fd..55923547401 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -20,6 +20,7 @@ public static class ContentKeyFunctions public static readonly BoundKeyFunction FocusOOC = "FocusOOCWindow"; public static readonly BoundKeyFunction FocusAdminChat = "FocusAdminChatWindow"; public static readonly BoundKeyFunction FocusDeadChat = "FocusDeadChatWindow"; + public static readonly BoundKeyFunction FocusCollectiveMindChat = "FocusCollectiveMindChatWindow"; // Sunrise-Edit public static readonly BoundKeyFunction FocusConsoleChat = "FocusConsoleChatWindow"; public static readonly BoundKeyFunction CycleChatChannelForward = "CycleChatChannelForward"; public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward"; diff --git a/Content.Shared/Sunrise/CollectiveMind/CollectiveMindComponent.cs b/Content.Shared/Sunrise/CollectiveMind/CollectiveMindComponent.cs new file mode 100644 index 00000000000..886c8f3c008 --- /dev/null +++ b/Content.Shared/Sunrise/CollectiveMind/CollectiveMindComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared._Sunrise.CollectiveMind; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.Sunrise.CollectiveMind +{ + [RegisterComponent, NetworkedComponent] + public sealed partial class CollectiveMindComponent : Component + { + [DataField("minds", customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List Minds = new(); + } +} diff --git a/Content.Shared/_Sunrise/CollectiveMind/CollectiveMindPrototype.cs b/Content.Shared/_Sunrise/CollectiveMind/CollectiveMindPrototype.cs new file mode 100644 index 00000000000..f8294f954ea --- /dev/null +++ b/Content.Shared/_Sunrise/CollectiveMind/CollectiveMindPrototype.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared._Sunrise.CollectiveMind; + +[Prototype("collectiveMind")] +public sealed partial class CollectiveMindPrototype : IPrototype +{ + [DataField("name")] + public string Name { get; private set; } = string.Empty; + + [ViewVariables(VVAccess.ReadOnly)] + public string LocalizedName => Loc.GetString(Name); + + [DataField("keycode")] + public char KeyCode { get; private set; } = '\0'; + + [DataField("color")] + public Color Color { get; private set; } = Color.Lime; + + [IdDataField, ViewVariables] + public string ID { get; } = default!; +} diff --git a/Resources/Locale/ru-RU/_sunrise/collective-mind/collective_mind.ftl b/Resources/Locale/ru-RU/_sunrise/collective-mind/collective_mind.ftl new file mode 100644 index 00000000000..611bbbd02c6 --- /dev/null +++ b/Resources/Locale/ru-RU/_sunrise/collective-mind/collective_mind.ftl @@ -0,0 +1,9 @@ +collective-mind-chat-wrap-message = [bold]{$channel}: {$message}[/bold] +collective-mind-chat-wrap-message-admin = {$source} ({$channel}): {$message} +collective-mind-flesh-cult = Плоть +collective-mind-xeno = Ксено +collective-mind-blob = Блоб +collective-mind-dioneas = Дионея +collective-mind-arachnids = Арахниды +collective-mind-carp = Карпы +collective-mind-zombie = Зомби diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 0389d20cbc3..2c5b95aed33 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -2286,6 +2286,9 @@ collection: FootstepSpiderLegs params: volume: -10 + - type: CollectiveMind + minds: + - Arachnids # Sunrise-end - type: entity @@ -3311,6 +3314,9 @@ reformTime: 10 popupText: diona-reform-attempt reformPrototype: MobDionaReformed + - type: CollectiveMind + minds: + - Dioneas - type: entity parent: MobDionaNymph diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index e345ec477b1..2c2558c5a1b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -79,6 +79,11 @@ interactFailureString: petting-failure-carp interactFailureSound: path: /Audio/Effects/bite.ogg + # Sunrise-Start + - type: CollectiveMind + minds: + - Carp + # Sunrise-End - type: entity parent: BaseMobCarp diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 23d3e838e21..45010cfb3dd 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -119,6 +119,11 @@ molsPerSecondPerUnitMass: 0.0005 - type: Speech speechVerb: LargeMob + # Sunrise-Start + - type: CollectiveMind + minds: + - Xeno + # Sunrise-End - type: entity name: praetorian diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml index 0b21f0d7353..a687009fc2b 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml @@ -139,6 +139,11 @@ tags: - CannotSuicide - DoorBumpOpener + # Sunrise-Start + - type: CollectiveMind + minds: + - Carp + # Sunrise-End - type: entity parent: BaseMobDragon diff --git a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml index 7f0e3987ccf..0b9927ca909 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml @@ -123,6 +123,9 @@ collection: FootstepSpiderLegs params: volume: -10 + - type: CollectiveMind + minds: + - Arachnids # Sunrise-end - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index 5bda66d6359..b7ab2c4aedf 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -107,6 +107,11 @@ 32: sprite: Mobs/Species/Human/displacement.rsi state: jumpsuit-female + # Sunrise-Start + - type: CollectiveMind + minds: + - Dioneas + # Sunrise-End - type: entity parent: BaseSpeciesDummy diff --git a/Resources/Prototypes/_Sunrise/CollectiveMinds/collective_mind.yml b/Resources/Prototypes/_Sunrise/CollectiveMinds/collective_mind.yml new file mode 100644 index 00000000000..a598cd43a6b --- /dev/null +++ b/Resources/Prototypes/_Sunrise/CollectiveMinds/collective_mind.yml @@ -0,0 +1,41 @@ +- type: collectiveMind + id: Xeno + name: collective-mind-xeno + keycode: 'к' + color: "#00ff40" + +- type: collectiveMind + id: Blob + name: collective-mind-blob + keycode: 'б' + color: "#66f263" + +- type: collectiveMind + id: FleshCult + name: collective-mind-flesh-cult + keycode: 'п' + color: "#ff2676" + +- type: collectiveMind + id: Dioneas + name: collective-mind-dioneas + keycode: 'д' + color: "#0f6b1e" + +- type: collectiveMind + id: Arachnids + name: collective-mind-arachnids + keycode: 'а' + color: "#706d0d" + +- type: collectiveMind + id: Carp + name: collective-mind-carp + keycode: 'р' + color: "#787878" + +- type: collectiveMind + id: Zombie + name: collective-mind-zombie + keycode: 'з' + color: "#508053" diff --git a/Resources/Prototypes/_Sunrise/Entities/Mobs/Species/humanoid_xeno.yml b/Resources/Prototypes/_Sunrise/Entities/Mobs/Species/humanoid_xeno.yml index a548d69d191..31c5d7d3aba 100644 --- a/Resources/Prototypes/_Sunrise/Entities/Mobs/Species/humanoid_xeno.yml +++ b/Resources/Prototypes/_Sunrise/Entities/Mobs/Species/humanoid_xeno.yml @@ -71,13 +71,11 @@ - shader: StencilClear sprite: _Sunrise/Mobs/Species/HumanoidXeno/parts.rsi state: - # Sunrise-start - map: ["enum.HumanoidVisualLayers.LFoot"] - map: ["enum.HumanoidVisualLayers.RFoot"] - map: ["bra"] - map: ["pants"] - map: ["socks"] - # Sunrise-end - map: ["jumpsuit"] - map: ["enum.HumanoidVisualLayers.LHand"] - map: ["enum.HumanoidVisualLayers.RHand"] @@ -127,6 +125,9 @@ - CanPilot - FootstepSound - DoorBumpOpener + - type: CollectiveMind + minds: + - Xeno - type: entity parent: BaseSpeciesDummy