]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Glorfcode (force say on damage/stun/crit) (#20562)
authorKara <lunarautomaton6@gmail.com>
Fri, 29 Sep 2023 01:05:36 +0000 (18:05 -0700)
committerGitHub <noreply@github.com>
Fri, 29 Sep 2023 01:05:36 +0000 (18:05 -0700)
Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
Content.Server/Damage/ForceSay/DamageForceSaySystem.cs [new file with mode: 0644]
Content.Shared/Bed/Sleep/SharedSleepingSystem.cs
Content.Shared/Damage/ForceSay/AllowNextCritSpeechComponent.cs [new file with mode: 0644]
Content.Shared/Damage/ForceSay/DamageForceSayComponent.cs [new file with mode: 0644]
Content.Shared/Damage/ForceSay/DamageForceSayEvent.cs [new file with mode: 0644]
Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs
Content.Shared/Stunnable/SharedStunSystem.cs
Resources/Locale/en-US/damage/damage-force-say.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Mobs/Species/base.yml

index 0451a4a3aa09bef34d6c47debfc2149e7046a239..988139929678855d0848081050cfe95fea2ccd0e 100644 (file)
@@ -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<MsgChatMessage>(OnChatMessage);
+        SubscribeNetworkEvent<DamageForceSayEvent>(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<ChatBox>() 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<DamageForceSayComponent>(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 (file)
index 0000000..3d33464
--- /dev/null
@@ -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;
+
+/// <inheritdoc cref="DamageForceSayComponent"/>
+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<DamageForceSayComponent, StunnedEvent>(OnStunned);
+        SubscribeLocalEvent<DamageForceSayComponent, MobStateChangedEvent>(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<DamageForceSayComponent, DamageChangedEvent>(OnDamageChanged, after: new []{ typeof(MobThresholdSystem)} );
+        SubscribeLocalEvent<DamageForceSayComponent, SleepStateChangedEvent>(OnSleep);
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var query = AllEntityQuery<AllowNextCritSpeechComponent>();
+        while (query.MoveNext(out var uid, out var comp))
+        {
+            if (_timing.CurTime < comp.Timeout)
+                continue;
+
+            RemCompDeferred<AllowNextCritSpeechComponent>(uid);
+        }
+    }
+
+    private void TryForceSay(EntityUid uid, DamageForceSayComponent component, bool useSuffix=true, string? suffixOverride = null)
+    {
+        if (!TryComp<ActorComponent>(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<ActorComponent>(uid, out var actor))
+            return;
+
+        var nextCrit = EnsureComp<AllowNextCritSpeechComponent>(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);
+    }
+}
index ce6ae4795cd472e829d741ecdcdc5060d145e87f..2ac1c372ca6754d91e368a51423da566edc7e9b6 100644 (file)
@@ -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<AllowNextCritSpeechComponent>(uid))
+            {
+                RemCompDeferred<AllowNextCritSpeechComponent>(uid);
+                return;
+            }
+
             args.Cancel();
         }
 
diff --git a/Content.Shared/Damage/ForceSay/AllowNextCritSpeechComponent.cs b/Content.Shared/Damage/ForceSay/AllowNextCritSpeechComponent.cs
new file mode 100644 (file)
index 0000000..66a5786
--- /dev/null
@@ -0,0 +1,25 @@
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Damage.ForceSay;
+
+/// <summary>
+///     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 <see cref="MobStateComponent"/> checks. The reason for this is to allow
+///     'force saying'--for instance, with deathgasping or with <see cref="DamageForceSayComponent"/>.
+///
+///     This component is either removed in the <see cref="MobStateSystem"/> speech attempt check, or after <see cref="Timeout"/>
+///     has passed. This is to allow a player-submitted forced message in the case of <see cref="DamageForceSayComponent"/>,
+///     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.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class AllowNextCritSpeechComponent : Component
+{
+    /// <summary>
+    ///     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.
+    /// </summary>
+    public TimeSpan? Timeout = null;
+}
diff --git a/Content.Shared/Damage/ForceSay/DamageForceSayComponent.cs b/Content.Shared/Damage/ForceSay/DamageForceSayComponent.cs
new file mode 100644 (file)
index 0000000..dc2617b
--- /dev/null
@@ -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;
+
+/// <summary>
+/// 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.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class DamageForceSayComponent : Component
+{
+    /// <summary>
+    ///     The localization string that the message & suffix will be passed into
+    /// </summary>
+    [DataField]
+    public LocId ForceSayMessageWrap = "damage-force-say-message-wrap";
+
+    /// <summary>
+    ///     Same as <see cref="ForceSayMessageWrap"/> but for cases where no suffix is used,
+    ///     such as when going into crit.
+    /// </summary>
+    [DataField]
+    public LocId ForceSayMessageWrapNoSuffix = "damage-force-say-message-wrap-no-suffix";
+
+    /// <summary>
+    ///     The fluent string prefix to use when picking a random suffix
+    /// </summary>
+    [DataField]
+    public string ForceSayStringPrefix = "damage-force-say-";
+
+    /// <summary>
+    ///     The number of suffixes that exist for use with <see cref="ForceSayStringPrefix"/>.
+    ///     i.e. (prefix)-1 through (prefix)-(count)
+    /// </summary>
+    [DataField]
+    public int ForceSayStringCount = 7;
+
+    /// <summary>
+    ///     The amount of total damage between <see cref="ValidDamageGroups"/> that needs to be taken before
+    ///     a force say occurs.
+    /// </summary>
+    [DataField]
+    public FixedPoint2 DamageThreshold = FixedPoint2.New(10);
+
+    /// <summary>
+    ///     A list of damage group types that are considered when checking <see cref="DamageThreshold"/>.
+    /// </summary>
+    [DataField]
+    public HashSet<ProtoId<DamageGroupPrototype>>? ValidDamageGroups = new()
+    {
+        "Brute",
+        "Burn",
+    };
+
+    /// <summary>
+    ///     The time enforced between force says to avoid spam.
+    /// </summary>
+    [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 (file)
index 0000000..a8ed928
--- /dev/null
@@ -0,0 +1,13 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Damage.ForceSay;
+
+/// <summary>
+///     Sent to clients as a network event when their entity contains <see cref="DamageForceSayComponent"/>
+///     that COMMANDS them to speak the current message in their chatbox
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class DamageForceSayEvent : EntityEventArgs
+{
+    public string? Suffix;
+}
index 340a1627d6dfeff1ec4247c719e7cefe7afb0f90..4442d87d8850d8e24b9d097888db3e09a9b04519 100644 (file)
@@ -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<MobStateComponent, AttackAttemptEvent>(CheckAct);
         SubscribeLocalEvent<MobStateComponent, InteractionAttemptEvent>(CheckAct);
         SubscribeLocalEvent<MobStateComponent, ThrowAttemptEvent>(CheckAct);
-        SubscribeLocalEvent<MobStateComponent, SpeakAttemptEvent>(CheckAct);
+        SubscribeLocalEvent<MobStateComponent, SpeakAttemptEvent>(OnSpeakAttempt);
         SubscribeLocalEvent<MobStateComponent, IsEquippingAttemptEvent>(OnEquipAttempt);
         SubscribeLocalEvent<MobStateComponent, EmoteAttemptEvent>(CheckAct);
         SubscribeLocalEvent<MobStateComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
@@ -116,6 +117,17 @@ public partial class MobStateSystem
             args.Multiplier /= 2;
     }
 
+    private void OnSpeakAttempt(EntityUid uid, MobStateComponent component, SpeakAttemptEvent args)
+    {
+        if (HasComp<AllowNextCritSpeechComponent>(uid))
+        {
+            RemCompDeferred<AllowNextCritSpeechComponent>(uid);
+            return;
+        }
+
+        CheckAct(uid, component, args);
+    }
+
     private void CheckAct(EntityUid target, MobStateComponent component, CancellableEntityEventArgs args)
     {
         switch (component.CurrentState)
index 4f1534f5443f847b11ef5e80d63875419b382f2f..4875f2f68f8d9a04b578437ac1ed529a81513b85 100644 (file)
@@ -155,6 +155,10 @@ public abstract class SharedStunSystem : EntitySystem
 
         if (!_statusEffect.TryAddStatusEffect<StunnedComponent>(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<KnockedDownComponent>(uid, "KnockedDown", time, refresh);
+        if (!_statusEffect.TryAddStatusEffect<KnockedDownComponent>(uid, "KnockedDown", time, refresh))
+            return false;
+
+        var ev = new KnockedDownEvent();
+        RaiseLocalEvent(uid, ref ev);
+
+        return true;
     }
 
     /// <summary>
@@ -271,5 +281,16 @@ public abstract class SharedStunSystem : EntitySystem
     }
 
     #endregion
-
 }
+
+/// <summary>
+///     Raised directed on an entity when it is stunned.
+/// </summary>
+[ByRefEvent]
+public record struct StunnedEvent;
+
+/// <summary>
+///     Raised directed on an entity when it is knocked down.
+/// </summary>
+[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 (file)
index 0000000..a436035
--- /dev/null
@@ -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...
index 9e362c883954a7d098a386074f8dd0ba74a3adf8..d00f93fb764a5c1127bad1160378560086a10b69 100644 (file)
   - type: Puller
   - type: Speech
     speechSounds: Alto
+  - type: DamageForceSay
   - type: Vocal
     sounds:
       Male: MaleHuman