using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.Administration.Systems; using Content.Server.Animals.Components; using Content.Server.Mind; using Content.Server.Popups; using Content.Server.Radio; using Content.Server.Speech; using Content.Server.Speech.Components; using Content.Server.Vocalization.Systems; using Content.Shared.Animals.Components; using Content.Shared.Animals.Systems; using Content.Shared.Database; using Content.Shared.Mobs.Systems; using Content.Shared.Whitelist; using Robust.Shared.Network; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Animals.Systems; /// /// The ParrotMemorySystem handles remembering messages received through local chat (activelistener) or a radio /// (radiovocalizer) and stores them in a list. When an entity with a VocalizerComponent attempts to vocalize, this will /// try to set the message from memory. /// public sealed partial class ParrotMemorySystem : SharedParrotMemorySystem { [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IAdminManager _admin = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly PopupSystem _popup = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnErase); SubscribeLocalEvent(ListenerOnMapInit); SubscribeLocalEvent(OnListen); SubscribeLocalEvent(OnHeadsetReceive); SubscribeLocalEvent(OnTryVocalize); } private void OnErase(ref EraseEvent args) { DeletePlayerMessages(args.PlayerNetUserId); } private void ListenerOnMapInit(Entity entity, ref MapInitEvent args) { // If an entity has a ParrotListenerComponent it really ought to have an ActiveListenerComponent if (!HasComp(entity)) Log.Warning($"Entity {ToPrettyString(entity)} has a ParrotListenerComponent but was not given an ActiveListenerComponent"); } private void OnListen(Entity entity, ref ListenEvent args) { TryLearn(entity.Owner, args.Message, args.Source); } private void OnHeadsetReceive(Entity entity, ref HeadsetRadioReceiveRelayEvent args) { var message = args.RelayedEvent.Message; var source = args.RelayedEvent.MessageSource; TryLearn(entity.Owner, message, source); } /// /// Called when an entity with a ParrotMemoryComponent tries to vocalize. /// This function picks a message from memory and sets the event to handled /// private void OnTryVocalize(Entity entity, ref TryVocalizeEvent args) { // return if this was already handled if (args.Handled) return; // if there are no memories, return if (entity.Comp.SpeechMemories.Count == 0) return; // get a random memory from the memory list var memory = _random.Pick(entity.Comp.SpeechMemories); args.Message = memory.Message; args.Handled = true; } /// /// Try to learn a new message, returning early if this entity cannot learn a new message, /// the message doesn't pass certain checks, or the chance for learning a new message fails /// /// Entity learning a new word /// Message to learn /// Source EntityUid of the message public void TryLearn(Entity entity, string incomingMessage, EntityUid source) { if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2)) return; if (!_whitelist.CheckBoth(source, entity.Comp2.Blacklist, entity.Comp2.Whitelist)) return; if (source.Equals(entity) || _mobState.IsIncapacitated(entity)) return; // can't learn too soon after having already learnt something else if (_gameTiming.CurTime < entity.Comp1.NextLearnInterval) return; // remove whitespace around message, if any var message = incomingMessage.Trim(); // ignore messages containing tildes. This is a crude way to ignore whispers that are too far away // TODO: this isn't great. This should be replaced with a const or we should have a better way to check faraway messages if (message.Contains('~')) return; // ignore empty messages. These probably aren't sent anyway but just in case if (string.IsNullOrWhiteSpace(message)) return; // ignore messages that are too short or too long if (message.Length < entity.Comp1.MinEntryLength || message.Length > entity.Comp1.MaxEntryLength) return; // only from this point this message has a chance of being learned // set new time for learn interval, regardless of whether the learning succeeds entity.Comp1.NextLearnInterval = _gameTiming.CurTime + entity.Comp1.LearnCooldown; // decide if this message passes the learning chance if (!_random.Prob(entity.Comp1.LearnChance)) return; // actually commit this message to memory Learn((entity, entity.Comp1), message, source); } /// /// Actually learn a message and commit it to memory /// /// Entity learning a new word /// Message to learn /// Source EntityUid of the message private void Learn(Entity entity, string message, EntityUid source) { // log a low-priority chat type log to the admin logger // specifies what message was learnt by what entity, and who taught the message to that entity _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Parroting entity {ToPrettyString(entity):entity} learned the phrase \"{message}\" from {ToPrettyString(source):speaker}"); NetUserId? sourceNetUserId = null; if (_mind.TryGetMind(source, out _, out var mind)) { sourceNetUserId = mind.UserId; } var newMemory = new SpeechMemory(sourceNetUserId, message); // add a new message if there is space in the memory if (entity.Comp.SpeechMemories.Count < entity.Comp.MaxSpeechMemory) { entity.Comp.SpeechMemories.Add(newMemory); return; } // if there's no space in memory, replace something at random var replaceIdx = _random.Next(entity.Comp.SpeechMemories.Count); entity.Comp.SpeechMemories[replaceIdx] = newMemory; } /// /// Delete all messages from a specified player on all ParrotMemoryComponents /// /// The player of whom to delete messages private void DeletePlayerMessages(NetUserId playerNetUserId) { // query to enumerate all entities with a memorycomponent var query = EntityQueryEnumerator(); while (query.MoveNext(out _, out var memory)) { DeletePlayerMessages(memory, playerNetUserId); } } /// /// Delete all messages from a specified player on a given ParrotMemoryComponent /// /// The ParrotMemoryComponent on which to delete messages /// The player of whom to delete messages private void DeletePlayerMessages(ParrotMemoryComponent memoryComponent, NetUserId playerNetUserId) { // this is a sort of expensive operation that is hopefully rare and performed on just a few parrots // with limited memory for (var i = 0; i < memoryComponent.SpeechMemories.Count; i++) { var memory = memoryComponent.SpeechMemories[i]; // netuserid may be null if the message was learnt from a non-player entity if (memory.NetUserId is null) continue; // skip if this memory was not learnt from the target user if (!memory.NetUserId.Equals(playerNetUserId)) continue; // order isn't important in this list so we can use the faster means of removing memoryComponent.SpeechMemories.RemoveSwap(i); } } }