From 9ebf6a24c4ebb16a8a6202951f3eb7934bdd870b Mon Sep 17 00:00:00 2001 From: Crude Oil <124208219+CroilBird@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:04:57 +0200 Subject: [PATCH] Parroting Parrots part 1: Help maints! SQUAWK! Maints! (#38243) * parrots have ears. add poly * high tech parrot functionality * adjust times * add accent to radio message * don't spam everything all at once probably * learn about the existence of prob(float) * actually use Prob(float) correctly * newline * add pet spawner for poly * move chance to talk on radio to component * missing comment * minor edits and doc additions * the reviewerrrrrrr * parrot can't learn when crit or dead * increase default memory * rename poly to polly * crude way to ignore whispers. chatcode please * This is Polly. It is set to broadcast over the engineering frequency * add missing initialize * add displacement map for parrot ears * review comments - Errant * minor things * large rework * fix attempting to talk when entity has no channels * use list of active radios again to track channels * fix bad return, some comments * fix long learn cooldown * minor adjustments * use FromMinutes * the voices told me to make these changes * remove default reassignment * Review changes * remove polly's accent * decouple radio stuff from parrotsystem * minor stuff * split vocalization and parroting * minor review work * re-add missing check * add admin verb for clearing parrot messages * minor action icon update * oops * increase icon number text size * Admin erase parrot messages associated with players * part 1 beck review * add whitelist and blacklist for parrots * Downgrade missing component error to warning * Add comment * add some missing comments * Remove active radio entity tracking, use all inventory slots * Minor changes * small review stuff * review radio stuff * swap ears displacement to invisible death displacement * remove syncsprite * vscode why do yo have to hurt my feelings * review changes * use checkboth --- .../Administration/Systems/AdminSystem.cs | 14 + .../Components/ParrotListenerComponent.cs | 32 +++ .../Components/ParrotMemoryComponent.cs | 57 ++++ .../Animals/Systems/ParrotMemorySystem.cs | 247 ++++++++++++++++++ .../Radio/EntitySystems/HeadsetSystem.cs | 14 +- Content.Server/Radio/RadioEvent.cs | 6 + .../Components/RadioVocalizerComponent.cs | 14 + .../Components/VocalizerComponent.cs | 30 +++ .../Systems/RadioVocalizationSystem.cs | 98 +++++++ .../Systems/VocalizationSystem.cs | 111 ++++++++ .../Locale/en-US/animals/parrot/parrot.ftl | 2 + .../Catalog/Cargo/cargo_livestock.yml | 2 +- .../Markers/Spawners/Mobs/animals.yml | 2 +- .../Entities/Markers/Spawners/Mobs/pets.yml | 15 ++ .../Prototypes/Entities/Mobs/NPCs/animals.yml | 37 ++- .../Prototypes/Entities/Mobs/NPCs/pets.yml | 18 ++ .../parrot_inventory_template.yml | 10 + .../Roles/Jobs/Fun/misc_startinggear.yml | 5 + .../Interface/AdminActions/clear-parrot.png | Bin 0 -> 539 bytes .../Mobs/Animals/parrot.rsi/parrot.png | Bin 16667 -> 0 bytes .../Animals/parrot/displacement.rsi/ears.png | Bin 0 -> 96 bytes .../Animals/parrot/displacement.rsi/meta.json | 18 ++ .../Animals/{ => parrot}/parrot.rsi/dead.png | Bin .../Animals/{ => parrot}/parrot.rsi/icon.png | Bin .../Animals/{ => parrot}/parrot.rsi/meta.json | 0 .../Mobs/Animals/parrot/parrot.rsi/parrot.png | Bin 0 -> 16683 bytes .../Animals/{ => parrot}/parrot.rsi/sit.png | Bin 27 files changed, 725 insertions(+), 7 deletions(-) create mode 100644 Content.Server/Animals/Components/ParrotListenerComponent.cs create mode 100644 Content.Server/Animals/Components/ParrotMemoryComponent.cs create mode 100644 Content.Server/Animals/Systems/ParrotMemorySystem.cs create mode 100644 Content.Server/Vocalization/Components/RadioVocalizerComponent.cs create mode 100644 Content.Server/Vocalization/Components/VocalizerComponent.cs create mode 100644 Content.Server/Vocalization/Systems/RadioVocalizationSystem.cs create mode 100644 Content.Server/Vocalization/Systems/VocalizationSystem.cs create mode 100644 Resources/Locale/en-US/animals/parrot/parrot.ftl create mode 100644 Resources/Prototypes/InventoryTemplates/parrot_inventory_template.yml create mode 100644 Resources/Textures/Interface/AdminActions/clear-parrot.png delete mode 100644 Resources/Textures/Mobs/Animals/parrot.rsi/parrot.png create mode 100644 Resources/Textures/Mobs/Animals/parrot/displacement.rsi/ears.png create mode 100644 Resources/Textures/Mobs/Animals/parrot/displacement.rsi/meta.json rename Resources/Textures/Mobs/Animals/{ => parrot}/parrot.rsi/dead.png (100%) rename Resources/Textures/Mobs/Animals/{ => parrot}/parrot.rsi/icon.png (100%) rename Resources/Textures/Mobs/Animals/{ => parrot}/parrot.rsi/meta.json (100%) create mode 100644 Resources/Textures/Mobs/Animals/parrot/parrot.rsi/parrot.png rename Resources/Textures/Mobs/Animals/{ => parrot}/parrot.rsi/sit.png (100%) diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs index 56bbc225cc..78433db129 100644 --- a/Content.Server/Administration/Systems/AdminSystem.cs +++ b/Content.Server/Administration/Systems/AdminSystem.cs @@ -383,8 +383,13 @@ public sealed class AdminSystem : EntitySystem { _chat.DeleteMessagesBy(uid); + var eraseEvent = new EraseEvent(uid); + if (!_minds.TryGetMind(uid, out var mindId, out var mind) || mind.OwnedEntity == null || TerminatingOrDeleted(mind.OwnedEntity.Value)) + { + RaiseLocalEvent(ref eraseEvent); return; + } var entity = mind.OwnedEntity.Value; @@ -444,6 +449,8 @@ public sealed class AdminSystem : EntitySystem if (_playerManager.TryGetSessionById(uid, out var session)) _gameTicker.SpawnObserver(session); + + RaiseLocalEvent(ref eraseEvent); } private void OnSessionPlayTimeUpdated(ICommonSession session) @@ -451,3 +458,10 @@ public sealed class AdminSystem : EntitySystem UpdatePlayerList(session); } } + +/// +/// Event fired after a player is erased by an admin +/// +/// NetUserId of the player that was the target of the Erase +[ByRefEvent] +public record struct EraseEvent(NetUserId PlayerNetUserId); diff --git a/Content.Server/Animals/Components/ParrotListenerComponent.cs b/Content.Server/Animals/Components/ParrotListenerComponent.cs new file mode 100644 index 0000000000..9d0c8a2d01 --- /dev/null +++ b/Content.Server/Animals/Components/ParrotListenerComponent.cs @@ -0,0 +1,32 @@ +using Content.Shared.Whitelist; + +namespace Content.Server.Animals.Components; + +/// +/// Makes an entity able to listen to messages from IC chat and attempt to commit them to memory +/// +[RegisterComponent] +public sealed partial class ParrotListenerComponent : Component +{ + /// + /// Whitelist for purposes of limiting which entities a parrot will listen to + /// + /// This is here because parrots can learn via local chat or radio from other parrots. this can quickly devolve from + /// SQUAWK! Polly wants a cracker! BRAAWK + /// to + /// BRAAWK! SQUAWK! RAWWK! Polly wants a cracker! AAWK! AWWK! Cracker! SQUAWK! BRAWWK! SQUAWK! + /// This is limited somewhat by the message length limit on ParrotMemoryComponent, but can be prevented entirely here + /// + [DataField] + public EntityWhitelist? Whitelist; + + /// + /// Blacklist for purposes of ignoring entities + /// As above, this is here to force parrots to ignore certain entities. + /// For example, polly will be consistently mapped around EngiDrobes, which will consistently say stuff like + /// "Guaranteed to protect your feet from industrial accidents!" + /// If polly ends up constantly advertising engineering drip, this can be used to prevent it. + /// + [DataField] + public EntityWhitelist? Blacklist; +} diff --git a/Content.Server/Animals/Components/ParrotMemoryComponent.cs b/Content.Server/Animals/Components/ParrotMemoryComponent.cs new file mode 100644 index 0000000000..2c3771cfac --- /dev/null +++ b/Content.Server/Animals/Components/ParrotMemoryComponent.cs @@ -0,0 +1,57 @@ +using Robust.Shared.Network; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.Animals.Components; + +/// +/// Makes an entity able to memorize chat/radio messages +/// +[RegisterComponent] +[AutoGenerateComponentPause] +public sealed partial class ParrotMemoryComponent : Component +{ + /// + /// List of SpeechMemory records this entity has learned + /// + [DataField] + public List SpeechMemories = []; + + /// + /// The % chance an entity with this component learns a phrase when learning is off cooldown + /// + [DataField] + public float LearnChance = 0.4f; + + /// + /// Time after which another attempt can be made at learning a phrase + /// + [DataField] + public TimeSpan LearnCooldown = TimeSpan.FromMinutes(1); + + /// + /// Next time at which the parrot can attempt to learn something + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan NextLearnInterval = TimeSpan.Zero; + + /// + /// The number of speech entries that are remembered + /// + [DataField] + public int MaxSpeechMemory = 50; + + /// + /// Minimum length of a speech entry + /// + [DataField] + public int MinEntryLength = 4; + + /// + /// Maximum length of a speech entry + /// + [DataField] + public int MaxEntryLength = 50; +} + +public record struct SpeechMemory(NetUserId? NetUserId, string Message); diff --git a/Content.Server/Animals/Systems/ParrotMemorySystem.cs b/Content.Server/Animals/Systems/ParrotMemorySystem.cs new file mode 100644 index 0000000000..eb3429821c --- /dev/null +++ b/Content.Server/Animals/Systems/ParrotMemorySystem.cs @@ -0,0 +1,247 @@ +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.Database; +using Content.Shared.Mobs.Systems; +using Content.Shared.Popups; +using Content.Shared.Verbs; +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 : EntitySystem +{ + [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>(OnGetVerbs); + + SubscribeLocalEvent(ListenerOnMapInit); + + SubscribeLocalEvent(OnListen); + SubscribeLocalEvent(OnHeadsetReceive); + + SubscribeLocalEvent(OnTryVocalize); + } + + private void OnErase(ref EraseEvent args) + { + DeletePlayerMessages(args.PlayerNetUserId); + } + + private void OnGetVerbs(Entity entity, ref GetVerbsEvent args) + { + var user = args.User; + + // limit this to admins + if (!_admin.IsAdmin(user)) + return; + + // simple verb that just clears the memory list + var clearMemoryVerb = new Verb() + { + Text = Loc.GetString("parrot-verb-clear-memory"), + Category = VerbCategory.Admin, + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/clear-parrot.png")), + Act = () => + { + entity.Comp.SpeechMemories.Clear(); + _popup.PopupEntity(Loc.GetString("parrot-popup-memory-cleared"), entity, user, PopupType.Medium); + }, + }; + + args.Verbs.Add(clearMemoryVerb); + } + + 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); + } + } +} diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index d18b044205..e3f8070311 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -99,7 +99,19 @@ public sealed class HeadsetSystem : SharedHeadsetSystem private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref RadioReceiveEvent args) { - if (TryComp(Transform(uid).ParentUid, out ActorComponent? actor)) + // TODO: change this when a code refactor is done + // this is currently done this way because receiving radio messages on an entity otherwise requires that entity + // to have an ActiveRadioComponent + + var parent = Transform(uid).ParentUid; + + if (parent.IsValid()) + { + var relayEvent = new HeadsetRadioReceiveRelayEvent(args); + RaiseLocalEvent(parent, ref relayEvent); + } + + if (TryComp(parent, out ActorComponent? actor)) _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel); } diff --git a/Content.Server/Radio/RadioEvent.cs b/Content.Server/Radio/RadioEvent.cs index fafa66674e..49ff63f824 100644 --- a/Content.Server/Radio/RadioEvent.cs +++ b/Content.Server/Radio/RadioEvent.cs @@ -6,6 +6,12 @@ namespace Content.Server.Radio; [ByRefEvent] public readonly record struct RadioReceiveEvent(string Message, EntityUid MessageSource, RadioChannelPrototype Channel, EntityUid RadioSource, MsgChatMessage ChatMsg); +/// +/// Event raised on the parent entity of a headset radio when a radio message is received +/// +[ByRefEvent] +public readonly record struct HeadsetRadioReceiveRelayEvent(RadioReceiveEvent RelayedEvent); + /// /// Use this event to cancel sending message per receiver /// diff --git a/Content.Server/Vocalization/Components/RadioVocalizerComponent.cs b/Content.Server/Vocalization/Components/RadioVocalizerComponent.cs new file mode 100644 index 0000000000..b2414f3796 --- /dev/null +++ b/Content.Server/Vocalization/Components/RadioVocalizerComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Vocalization.Components; + +/// +/// Makes an entity able to vocalize through an equipped radio +/// +[RegisterComponent] +public sealed partial class RadioVocalizerComponent : Component +{ + /// + /// chance the vocalizing entity speaks on the radio. + /// + [DataField] + public float RadioAttemptChance = 0.3f; +} diff --git a/Content.Server/Vocalization/Components/VocalizerComponent.cs b/Content.Server/Vocalization/Components/VocalizerComponent.cs new file mode 100644 index 0000000000..0dfde9751b --- /dev/null +++ b/Content.Server/Vocalization/Components/VocalizerComponent.cs @@ -0,0 +1,30 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.Vocalization.Components; + +/// +/// Makes an entity vocalize at set intervals +/// +[RegisterComponent] +[AutoGenerateComponentPause] +public sealed partial class VocalizerComponent : Component +{ + /// + /// Minimum time to wait after speaking to vocalize again + /// + [DataField] + public TimeSpan MinVocalizeInterval = TimeSpan.FromMinutes(2); + + /// + /// Maximum time to wait after speaking to vocalize again + /// + [DataField] + public TimeSpan MaxVocalizeInterval = TimeSpan.FromMinutes(6); + + /// + /// Next time at which to vocalize + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan NextVocalizeInterval = TimeSpan.Zero; +} diff --git a/Content.Server/Vocalization/Systems/RadioVocalizationSystem.cs b/Content.Server/Vocalization/Systems/RadioVocalizationSystem.cs new file mode 100644 index 0000000000..bdb14168bb --- /dev/null +++ b/Content.Server/Vocalization/Systems/RadioVocalizationSystem.cs @@ -0,0 +1,98 @@ +using Content.Server.Chat.Systems; +using Content.Server.Radio.Components; +using Content.Server.Vocalization.Components; +using Content.Shared.Chat; +using Content.Shared.Inventory; +using Content.Shared.Radio; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Vocalization.Systems; + +/// +/// RadioVocalizationSystem handles vocalizing things via equipped radios when a VocalizeEvent is fired +/// +public sealed partial class RadioVocalizationSystem : EntitySystem +{ + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnVocalize); + } + + /// + /// Called whenever an entity with a VocalizerComponent tries to speak + /// + private void OnVocalize(Entity entity, ref VocalizeEvent args) + { + if (args.Handled) + return; + + // set to handled if we succeed in speaking on the radio + args.Handled = TrySpeakRadio(entity.Owner, args.Message); + } + + /// + /// Selects a random radio channel from all ActiveRadio entities in a given entity's inventory + /// If no channels are found, this returns false and sets channel to an empty string + /// + private bool TryPickRandomRadioChannel(EntityUid entity, out string channel) + { + HashSet potentialChannels = []; + + // we don't have to check if this entity has an inventory. GetHandOrInventoryEntities will not yield anything + // if an entity has no inventory or inventory slots + foreach (var item in _inventory.GetHandOrInventoryEntities(entity)) + { + if (!TryComp(item, out var radio)) + continue; + + potentialChannels.UnionWith(radio.Channels); + } + + if (potentialChannels.Count == 0) + { + channel = string.Empty; + return false; + } + + channel = _random.Pick(potentialChannels); + + return true; + } + + /// + /// Attempts to speak on the radio. Returns false if there is no radio or talking on radio fails somehow + /// + /// Entity to try and make speak on the radio + /// Message to speak + private bool TrySpeakRadio(Entity entity, string message) + { + if (!Resolve(entity, ref entity.Comp)) + return false; + + if (!_random.Prob(entity.Comp.RadioAttemptChance)) + return false; + + if (!TryPickRandomRadioChannel(entity, out var channel)) + return false; + + var channelPrefix = _proto.Index(channel).KeyCode; + + // send a whisper using the radio channel prefix and whatever relevant radio channel character + // along with the message. This is analogous to how radio messages are sent by players + _chat.TrySendInGameICMessage( + entity, + $"{SharedChatSystem.RadioChannelPrefix}{channelPrefix} {message}", + InGameICChatType.Whisper, + ChatTransmitRange.Normal); + + return true; + } +} diff --git a/Content.Server/Vocalization/Systems/VocalizationSystem.cs b/Content.Server/Vocalization/Systems/VocalizationSystem.cs new file mode 100644 index 0000000000..b0a2e232a4 --- /dev/null +++ b/Content.Server/Vocalization/Systems/VocalizationSystem.cs @@ -0,0 +1,111 @@ +using Content.Server.Chat.Systems; +using Content.Server.Vocalization.Components; +using Content.Shared.ActionBlocker; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Vocalization.Systems; + +/// +/// VocalizationSystem raises VocalizeEvents to make entities speak at certain intervals +/// This is used in combination with systems like ParrotMemorySystem to randomly say messages from memory, +/// or can be used by other systems to speak pre-set messages +/// +public sealed partial class VocalizationSystem : EntitySystem +{ + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + /// + /// Try speaking by raising a TryVocalizeEvent + /// This event is passed to systems adding a message to it and setting it to handled + /// + private void TrySpeak(Entity entity) + { + var tryVocalizeEvent = new TryVocalizeEvent(); + RaiseLocalEvent(entity.Owner, ref tryVocalizeEvent); + + // if the event was never handled, return + // this happens if there are no components that trigger systems to add a message to this event + if (!tryVocalizeEvent.Handled) + return; + + // if the event's message is null for whatever reason, return. + // this would mean a system didn't set the message properly but did set the event to handled + if (tryVocalizeEvent.Message is not { } message) + return; + + Speak(entity, message); + } + + /// + /// Actually say something. + /// + private void Speak(Entity entity, string message) + { + // raise a VocalizeEvent + // this can be handled by other systems to speak using a method other than local chat + var vocalizeEvent = new VocalizeEvent(message); + RaiseLocalEvent(entity.Owner, ref vocalizeEvent); + + // if the event is handled, don't try speaking + if (vocalizeEvent.Handled) + return; + + // default to local chat if no other system handles the event + // first check if the entity can speak + if (!_actionBlocker.CanSpeak(entity)) + return; + + // send the message + _chat.TrySendInGameICMessage(entity, message, InGameICChatType.Speak, ChatTransmitRange.Normal); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // get current game time for delay + var currentGameTime = _gameTiming.CurTime; + + // query to get all entities with a VocalizeComponent + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var vocalizer)) + { + // go to next entity if it is too early for this one to speak + if (currentGameTime < vocalizer.NextVocalizeInterval) + continue; + + // set a new time for the speak interval, regardless of whether speaking works + var randomSpeakInterval = _random.Next(vocalizer.MinVocalizeInterval, vocalizer.MaxVocalizeInterval); + vocalizer.NextVocalizeInterval += randomSpeakInterval; + + // if an admin updates the speak interval to be immediate, this loop will spam messages until the + // nextspeakinterval catches up with the current game time. Prevent this from happening + if (vocalizer.NextVocalizeInterval < _gameTiming.CurTime) + vocalizer.NextVocalizeInterval = _gameTiming.CurTime + randomSpeakInterval; + + // try to speak + TrySpeak((uid, vocalizer)); + } + } +} + +/// +/// Fired when the entity wants to try vocalizing, but doesn't have a message yet +/// +/// Message to send, this is null when the event is just fired and should be set by a system +/// Whether the message was handled by a system +[ByRefEvent] +public record struct TryVocalizeEvent(string? Message = null, bool Handled = false); + +/// +/// Fired when the entity wants to vocalize and has a message. Allows for interception by other systems if the +/// vocalization needs to be done some other way +/// +/// Message to send +/// Whether the message was handled by a system +[ByRefEvent] +public record struct VocalizeEvent(string Message, bool Handled = false); diff --git a/Resources/Locale/en-US/animals/parrot/parrot.ftl b/Resources/Locale/en-US/animals/parrot/parrot.ftl new file mode 100644 index 0000000000..8262324d81 --- /dev/null +++ b/Resources/Locale/en-US/animals/parrot/parrot.ftl @@ -0,0 +1,2 @@ +parrot-verb-clear-memory = Clear parrot memory +parrot-popup-memory-cleared = Parrot memory cleared diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_livestock.yml b/Resources/Prototypes/Catalog/Cargo/cargo_livestock.yml index 667b83ec3f..68b4b2dc58 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_livestock.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_livestock.yml @@ -151,7 +151,7 @@ - type: cargoProduct id: LivestockParrot icon: - sprite: Mobs/Animals/parrot.rsi + sprite: Mobs/Animals/parrot/parrot.rsi state: parrot product: CrateNPCParrot cost: 3000 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Mobs/animals.yml b/Resources/Prototypes/Entities/Markers/Spawners/Mobs/animals.yml index 5691d984b7..ea39785032 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Mobs/animals.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Mobs/animals.yml @@ -210,7 +210,7 @@ layers: - state: green - state: parrot - sprite: Mobs/Animals/parrot.rsi + sprite: Mobs/Animals/parrot/parrot.rsi - type: ConditionalSpawner prototypes: - MobParrot diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Mobs/pets.yml b/Resources/Prototypes/Entities/Markers/Spawners/Mobs/pets.yml index 0708f9a108..f590bcb508 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Mobs/pets.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Mobs/pets.yml @@ -218,6 +218,21 @@ prototypes: - MobMonkeyPunpun +- type: entity + parent: MarkerBase + id: SpawnMobPollyParrot + name: Polly the parrot Spawner + suffix: CE Pet + components: + - type: Sprite + layers: + - state: green + - state: parrot + sprite: Mobs/Animals/parrot/parrot.rsi + - type: ConditionalSpawner + prototypes: + - MobPollyParrot + - type: entity parent: MarkerBase id: SpawnMobPossumMorty diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 2a599d70be..2b837b768e 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -2174,9 +2174,9 @@ # Would be cool to have some functionality for the parrot to be able to sit on stuff - type: entity - name: parrot parent: [ SimpleMobBase, FlyingMobBase ] - id: MobParrot + id: MobParrotBase + abstract: true description: Infiltrates your domain, spies on you, and somehow still a cool pet. components: - type: MovementSpeedModifier @@ -2187,7 +2187,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: parrot - sprite: Mobs/Animals/parrot.rsi + sprite: Mobs/Animals/parrot/parrot.rsi - type: Fixtures fixtures: fix1: @@ -2217,7 +2217,29 @@ - type: Vocal sounds: Unsexed: Parrot - - type: ParrotAccent + - type: ActiveListener + - type: Vocalizer + - type: RadioVocalizer + - type: ParrotListener + - type: ParrotMemory + - type: Inventory + templateId: parrot + speciesId: parrot + # transparent displacement map to hide the headset. this should be an animated displacement map to follow the parrot + # as it bobs up and down, but this in turn needs some way to change the displacement map on inventory entities when + # the parrot dies, otherwise the visuals will be broken + displacements: + ears: + sizeMaps: + 32: + sprite: Mobs/Animals/parrot/displacement.rsi + state: ears + - type: InventorySlots + - type: Strippable + - type: UserInterface + interfaces: + enum.StrippingUiKey.Key: + type: StrippableBoundUserInterface - type: InteractionPopup successChance: 0.6 interactSuccessString: petting-success-bird @@ -2228,6 +2250,13 @@ - type: Bloodstream bloodMaxVolume: 50 +- type: entity + name: parrot + parent: MobParrotBase + id: MobParrot + components: + - type: ParrotAccent # regular parrots have a parrotaccent. Polly is special and does not + - type: entity name: penguin parent: SimpleMobBase diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 738b1ac129..86be44dd05 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -858,3 +858,21 @@ # - type: AlwaysRevolutionaryConvertible - type: StealTarget stealGroup: AnimalTropico + +- type: entity + name: Polly the parrot + parent: MobParrotBase + id: MobPollyParrot + description: An expert in quantum cracker theory + components: + - type: ParrotMemory + learnChance: 0.5 # polly is smarter + - type: Vocalizer + maxVocalizeInterval: 240 # polly is chattier + - type: Grammar + attributes: + proper: true + gender: male + - type: Loadout + prototypes: [ MobPollyGear ] + diff --git a/Resources/Prototypes/InventoryTemplates/parrot_inventory_template.yml b/Resources/Prototypes/InventoryTemplates/parrot_inventory_template.yml new file mode 100644 index 0000000000..5460d730eb --- /dev/null +++ b/Resources/Prototypes/InventoryTemplates/parrot_inventory_template.yml @@ -0,0 +1,10 @@ +- type: inventoryTemplate + id: parrot + slots: + - name: ears + slotTexture: ears + slotFlags: EARS + stripTime: 3 + uiWindowPos: 0,2 + strippingWindowPos: 0,0 + displayName: Ears diff --git a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml index 1448a07cba..13455651a2 100644 --- a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml +++ b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml @@ -103,6 +103,11 @@ jumpsuit: ClothingUniformJumpsuitJacketMonkey id: PunPunIDCard +- type: startingGear + id: MobPollyGear + equipment: + ears: ClothingHeadsetEngineering + # Emotional Support Scurret - type: startingGear diff --git a/Resources/Textures/Interface/AdminActions/clear-parrot.png b/Resources/Textures/Interface/AdminActions/clear-parrot.png new file mode 100644 index 0000000000000000000000000000000000000000..bf027c421d0e240c69bdca5c2667e60c6f946801 GIT binary patch literal 539 zcmV+$0_6RPP)Px$)k#D_RCt{2Tg#HfAP6K|^Z&o>UUsUItPez5?3ljJfYNjjjS<-#ht)8pl(G(w z!|W63w}c;yQE8H`-&RC7tJ(m2GgkA9%Dz|?ph5Zr%D)#(%vu7gWicxIVpV`;QdTvf zRRpQn)d6qh_4q)jVWij0e2va;n}z6I8+J{0LgR&cmpC?@6~|vJb(o+0SKo|tub_>9<^Y}6QL&o#x6tl zW#wgXRvmF4geiDgaXUPF1#ugUF?bP?&!_{2w?8#Ze=ks+NKo^%eIKv{0gDY1lR)X5nu>jP!}Xvf)|7Wn1UCk_W~%m5x^L{wDtnlB`a4W zMz76j>`U{l#Ze@nb#p7{RC9r{6-dd4*1i|nM}U2Y*Molw=nX!X{alv5zo(Q^DT_59 z8hZ;a3fL+0UziU>SgZaW;k&_TToh1RI-ZPI0SKoa>KXN+wO^_g*7)%LBs literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mobs/Animals/parrot.rsi/parrot.png b/Resources/Textures/Mobs/Animals/parrot.rsi/parrot.png deleted file mode 100644 index fd9079fd668d12dc3a1d92fef2e2a6a71ed478bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16667 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mobs/Animals/parrot.rsi/sit.png b/Resources/Textures/Mobs/Animals/parrot/parrot.rsi/sit.png similarity index 100% rename from Resources/Textures/Mobs/Animals/parrot.rsi/sit.png rename to Resources/Textures/Mobs/Animals/parrot/parrot.rsi/sit.png -- 2.51.2