From 80f36ea6d4f479ef30f5c4b2af7dbf5770768ce1 Mon Sep 17 00:00:00 2001 From: Kara Date: Thu, 28 Sep 2023 18:05:36 -0700 Subject: [PATCH] Glorfcode (force say on damage/stun/crit) (#20562) --- .../Systems/Chat/ChatUIController.cs | 34 +++++ .../Damage/ForceSay/DamageForceSaySystem.cs | 126 ++++++++++++++++++ .../Bed/Sleep/SharedSleepingSystem.cs | 8 ++ .../ForceSay/AllowNextCritSpeechComponent.cs | 25 ++++ .../ForceSay/DamageForceSayComponent.cs | 66 +++++++++ .../Damage/ForceSay/DamageForceSayEvent.cs | 13 ++ .../Systems/MobStateSystem.Subscribers.cs | 14 +- Content.Shared/Stunnable/SharedStunSystem.cs | 25 +++- .../Locale/en-US/damage/damage-force-say.ftl | 12 ++ .../Prototypes/Entities/Mobs/Species/base.yml | 1 + 10 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 Content.Server/Damage/ForceSay/DamageForceSaySystem.cs create mode 100644 Content.Shared/Damage/ForceSay/AllowNextCritSpeechComponent.cs create mode 100644 Content.Shared/Damage/ForceSay/DamageForceSayComponent.cs create mode 100644 Content.Shared/Damage/ForceSay/DamageForceSayEvent.cs create mode 100644 Resources/Locale/en-US/damage/damage-force-say.ftl diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs index 0451a4a3aa..9881399296 100644 --- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs +++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs @@ -16,6 +16,7 @@ using Content.Client.UserInterface.Systems.Gameplay; using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Chat; +using Content.Shared.Damage.ForceSay; using Content.Shared.Examine; using Content.Shared.Input; using Content.Shared.Radio; @@ -30,6 +31,7 @@ using Robust.Shared.Configuration; using Robust.Shared.Input.Binding; using Robust.Shared.Map; using Robust.Shared.Network; +using Robust.Shared.Random; using Robust.Shared.Replays; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -164,6 +166,7 @@ public sealed class ChatUIController : UIController _player.LocalPlayerChanged += OnLocalPlayerChanged; _state.OnStateChanged += StateChanged; _net.RegisterNetMessage(OnChatMessage); + SubscribeNetworkEvent(OnDamageForceSay); _speechBubbleRoot = new LayoutContainer(); @@ -774,6 +777,37 @@ public sealed class ChatUIController : UIController _manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel); } + private void OnDamageForceSay(DamageForceSayEvent ev, EntitySessionEventArgs _) + { + if (UIManager.ActiveScreen?.GetWidget() is not { } chatBox) + return; + + // Don't send on OOC/LOOC obviously! + if (chatBox.SelectedChannel is not + (ChatSelectChannel.Local or + ChatSelectChannel.Radio or + ChatSelectChannel.Whisper)) + return; + + if (_player.LocalPlayer?.ControlledEntity is not { } ent + || !EntityManager.TryGetComponent(ent, out var forceSay)) + return; + + var msg = chatBox.ChatInput.Input.Text.TrimEnd(); + + if (string.IsNullOrWhiteSpace(msg)) + return; + + var modifiedText = ev.Suffix != null + ? Loc.GetString(forceSay.ForceSayMessageWrap, + ("message", msg), ("suffix", ev.Suffix)) + : Loc.GetString(forceSay.ForceSayMessageWrapNoSuffix, + ("message", msg)); + + chatBox.ChatInput.Input.SetText(modifiedText); + chatBox.ChatInput.Input.ForceSubmitText(); + } + private void OnChatMessage(MsgChatMessage message) { var msg = message.Message; diff --git a/Content.Server/Damage/ForceSay/DamageForceSaySystem.cs b/Content.Server/Damage/ForceSay/DamageForceSaySystem.cs new file mode 100644 index 0000000000..3d3346403a --- /dev/null +++ b/Content.Server/Damage/ForceSay/DamageForceSaySystem.cs @@ -0,0 +1,126 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.ForceSay; +using Content.Shared.FixedPoint; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; +using Content.Shared.Stunnable; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.Players; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Damage.ForceSay; + +/// +public sealed class DamageForceSaySystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStunned); + SubscribeLocalEvent(OnMobStateChanged); + + // need to raise after mobthreshold + // so that we don't accidentally raise one for damage before one for mobstate + // (this won't double raise, because of the cooldown) + SubscribeLocalEvent(OnDamageChanged, after: new []{ typeof(MobThresholdSystem)} ); + SubscribeLocalEvent(OnSleep); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = AllEntityQuery(); + while (query.MoveNext(out var uid, out var comp)) + { + if (_timing.CurTime < comp.Timeout) + continue; + + RemCompDeferred(uid); + } + } + + private void TryForceSay(EntityUid uid, DamageForceSayComponent component, bool useSuffix=true, string? suffixOverride = null) + { + if (!TryComp(uid, out var actor)) + return; + + // disallow if cooldown hasn't ended + if (component.NextAllowedTime != null && + _timing.CurTime < component.NextAllowedTime) + return; + + var suffix = Loc.GetString(suffixOverride ?? component.ForceSayStringPrefix + _random.Next(1, component.ForceSayStringCount)); + + // set cooldown & raise event + component.NextAllowedTime = _timing.CurTime + component.Cooldown; + RaiseNetworkEvent(new DamageForceSayEvent { Suffix = useSuffix ? suffix : null }, actor.PlayerSession); + } + + private void AllowNextSpeech(EntityUid uid) + { + if (!TryComp(uid, out var actor)) + return; + + var nextCrit = EnsureComp(uid); + + // timeout is *3 ping to compensate for roundtrip + leeway + nextCrit.Timeout = _timing.CurTime + TimeSpan.FromMilliseconds(actor.PlayerSession.Ping * 3); + } + + private void OnSleep(EntityUid uid, DamageForceSayComponent component, SleepStateChangedEvent args) + { + if (!args.FellAsleep) + return; + + TryForceSay(uid, component, true, "damage-force-say-sleep"); + AllowNextSpeech(uid); + } + + private void OnStunned(EntityUid uid, DamageForceSayComponent component, ref StunnedEvent args) + { + TryForceSay(uid, component); + } + + private void OnDamageChanged(EntityUid uid, DamageForceSayComponent component, DamageChangedEvent args) + { + if (args.DamageDelta == null || !args.DamageIncreased || args.DamageDelta.Total < component.DamageThreshold) + return; + + if (component.ValidDamageGroups != null) + { + var totalApplicableDamage = FixedPoint2.Zero; + foreach (var (group, value) in args.DamageDelta.GetDamagePerGroup(_prototype)) + { + if (!component.ValidDamageGroups.Contains(group)) + continue; + + totalApplicableDamage += value; + } + + if (totalApplicableDamage < component.DamageThreshold) + return; + } + + TryForceSay(uid, component); + } + + private void OnMobStateChanged(EntityUid uid, DamageForceSayComponent component, MobStateChangedEvent args) + { + if (args is not { OldMobState: MobState.Alive, NewMobState: MobState.Critical or MobState.Dead }) + return; + + // no suffix for the drama + // LING IN MAI- + TryForceSay(uid, component, false); + AllowNextSpeech(uid); + } +} diff --git a/Content.Shared/Bed/Sleep/SharedSleepingSystem.cs b/Content.Shared/Bed/Sleep/SharedSleepingSystem.cs index ce6ae4795c..2ac1c372ca 100644 --- a/Content.Shared/Bed/Sleep/SharedSleepingSystem.cs +++ b/Content.Shared/Bed/Sleep/SharedSleepingSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.Actions; using Content.Shared.Bed.Sleep; +using Content.Shared.Damage.ForceSay; using Content.Shared.Eye.Blinding.Systems; using Content.Shared.Speech; using Robust.Shared.Network; @@ -54,6 +55,13 @@ namespace Content.Server.Bed.Sleep private void OnSpeakAttempt(EntityUid uid, SleepingComponent component, SpeakAttemptEvent args) { + // TODO reduce duplication of this behavior with MobStateSystem somehow + if (HasComp(uid)) + { + RemCompDeferred(uid); + return; + } + args.Cancel(); } diff --git a/Content.Shared/Damage/ForceSay/AllowNextCritSpeechComponent.cs b/Content.Shared/Damage/ForceSay/AllowNextCritSpeechComponent.cs new file mode 100644 index 0000000000..66a57860b2 --- /dev/null +++ b/Content.Shared/Damage/ForceSay/AllowNextCritSpeechComponent.cs @@ -0,0 +1,25 @@ +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Damage.ForceSay; + +/// +/// The reason for this component's existence is slightly unintuitive, so for context: this is put on an entity +/// to allow its next speech attempt to bypass checks. The reason for this is to allow +/// 'force saying'--for instance, with deathgasping or with . +/// +/// This component is either removed in the speech attempt check, or after +/// has passed. This is to allow a player-submitted forced message in the case of , +/// while also ensuring that it isn't valid forever. It has to work this way, because the server is not a keylogger and doesn't +/// have any knowledge of what the client might actually have typed, so it gives them some leeway for ping. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class AllowNextCritSpeechComponent : Component +{ + /// + /// Should be set when adding the component to specify the time that this should be valid for, + /// if it should stay valid for some amount of time. + /// + public TimeSpan? Timeout = null; +} diff --git a/Content.Shared/Damage/ForceSay/DamageForceSayComponent.cs b/Content.Shared/Damage/ForceSay/DamageForceSayComponent.cs new file mode 100644 index 0000000000..dc2617bc93 --- /dev/null +++ b/Content.Shared/Damage/ForceSay/DamageForceSayComponent.cs @@ -0,0 +1,66 @@ +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; + +namespace Content.Shared.Damage.ForceSay; + +/// +/// This is used for forcing clients to send messages with a suffix attached (like -GLORF) when taking large amounts +/// of damage, or things like entering crit or being stunned. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class DamageForceSayComponent : Component +{ + /// + /// The localization string that the message & suffix will be passed into + /// + [DataField] + public LocId ForceSayMessageWrap = "damage-force-say-message-wrap"; + + /// + /// Same as but for cases where no suffix is used, + /// such as when going into crit. + /// + [DataField] + public LocId ForceSayMessageWrapNoSuffix = "damage-force-say-message-wrap-no-suffix"; + + /// + /// The fluent string prefix to use when picking a random suffix + /// + [DataField] + public string ForceSayStringPrefix = "damage-force-say-"; + + /// + /// The number of suffixes that exist for use with . + /// i.e. (prefix)-1 through (prefix)-(count) + /// + [DataField] + public int ForceSayStringCount = 7; + + /// + /// The amount of total damage between that needs to be taken before + /// a force say occurs. + /// + [DataField] + public FixedPoint2 DamageThreshold = FixedPoint2.New(10); + + /// + /// A list of damage group types that are considered when checking . + /// + [DataField] + public HashSet>? ValidDamageGroups = new() + { + "Brute", + "Burn", + }; + + /// + /// The time enforced between force says to avoid spam. + /// + [DataField] + public TimeSpan Cooldown = TimeSpan.FromSeconds(5.0); + + public TimeSpan? NextAllowedTime = null; +} diff --git a/Content.Shared/Damage/ForceSay/DamageForceSayEvent.cs b/Content.Shared/Damage/ForceSay/DamageForceSayEvent.cs new file mode 100644 index 0000000000..a8ed9288a6 --- /dev/null +++ b/Content.Shared/Damage/ForceSay/DamageForceSayEvent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Damage.ForceSay; + +/// +/// Sent to clients as a network event when their entity contains +/// that COMMANDS them to speak the current message in their chatbox +/// +[Serializable, NetSerializable] +public sealed class DamageForceSayEvent : EntityEventArgs +{ + public string? Suffix; +} diff --git a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs index 340a1627d6..4442d87d88 100644 --- a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs +++ b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs @@ -1,4 +1,5 @@ using Content.Shared.Bed.Sleep; +using Content.Shared.Damage.ForceSay; using Content.Shared.Emoting; using Content.Shared.Hands; using Content.Shared.Interaction; @@ -27,7 +28,7 @@ public partial class MobStateSystem SubscribeLocalEvent(CheckAct); SubscribeLocalEvent(CheckAct); SubscribeLocalEvent(CheckAct); - SubscribeLocalEvent(CheckAct); + SubscribeLocalEvent(OnSpeakAttempt); SubscribeLocalEvent(OnEquipAttempt); SubscribeLocalEvent(CheckAct); SubscribeLocalEvent(OnUnequipAttempt); @@ -116,6 +117,17 @@ public partial class MobStateSystem args.Multiplier /= 2; } + private void OnSpeakAttempt(EntityUid uid, MobStateComponent component, SpeakAttemptEvent args) + { + if (HasComp(uid)) + { + RemCompDeferred(uid); + return; + } + + CheckAct(uid, component, args); + } + private void CheckAct(EntityUid target, MobStateComponent component, CancellableEntityEventArgs args) { switch (component.CurrentState) diff --git a/Content.Shared/Stunnable/SharedStunSystem.cs b/Content.Shared/Stunnable/SharedStunSystem.cs index 4f1534f544..4875f2f68f 100644 --- a/Content.Shared/Stunnable/SharedStunSystem.cs +++ b/Content.Shared/Stunnable/SharedStunSystem.cs @@ -155,6 +155,10 @@ public abstract class SharedStunSystem : EntitySystem if (!_statusEffect.TryAddStatusEffect(uid, "Stun", time, refresh)) return false; + + var ev = new StunnedEvent(); + RaiseLocalEvent(uid, ref ev); + _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} stunned for {time.Seconds} seconds"); return true; } @@ -171,7 +175,13 @@ public abstract class SharedStunSystem : EntitySystem if (!Resolve(uid, ref status, false)) return false; - return _statusEffect.TryAddStatusEffect(uid, "KnockedDown", time, refresh); + if (!_statusEffect.TryAddStatusEffect(uid, "KnockedDown", time, refresh)) + return false; + + var ev = new KnockedDownEvent(); + RaiseLocalEvent(uid, ref ev); + + return true; } /// @@ -271,5 +281,16 @@ public abstract class SharedStunSystem : EntitySystem } #endregion - } + +/// +/// Raised directed on an entity when it is stunned. +/// +[ByRefEvent] +public record struct StunnedEvent; + +/// +/// Raised directed on an entity when it is knocked down. +/// +[ByRefEvent] +public record struct KnockedDownEvent; diff --git a/Resources/Locale/en-US/damage/damage-force-say.ftl b/Resources/Locale/en-US/damage/damage-force-say.ftl new file mode 100644 index 0000000000..a436035114 --- /dev/null +++ b/Resources/Locale/en-US/damage/damage-force-say.ftl @@ -0,0 +1,12 @@ +damage-force-say-message-wrap = {$message}-{$suffix} +damage-force-say-message-wrap-no-suffix = {$message}- + +damage-force-say-1 = GACK! +damage-force-say-2 = GLORF! +damage-force-say-3 = OOF! +damage-force-say-4 = AUGH! +damage-force-say-5 = OW! +damage-force-say-6 = URGH! +damage-force-say-7 = HRNK! + +damage-force-say-sleep = zzz... diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 9e362c8839..d00f93fb76 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -200,6 +200,7 @@ - type: Puller - type: Speech speechSounds: Alto + - type: DamageForceSay - type: Vocal sounds: Male: MaleHuman -- 2.51.2